页结构与行格式
InnoDB 把数据和索引都组织在固定大小(默认
16KB)的页面里。理解页内布局——FIL
头、记录目录、Infimum/Supremum 哨兵、聚簇索引即数据——是读
EXPLAIN、分析页分裂、理解 VARCHAR
溢出存储的前提。本文对照 PostgreSQL
页面布局 的 8KB Page +
HeapTupleHeaderData,说明 InnoDB
的设计分叉。
源码:MySQL
8.0.36,storage/innobase/include/fil0fil.h、rem0rec.h、page0page.cc。
一、InnoDB 页的逻辑结构
1.1 一页里有什么
flowchart TB
subgraph page [16KB InnoDB Index Page]
FIL[FIL Header 38B]
IDX[Index Header]
INF[Infimum Record]
REC[User Records 链表]
SUP[Supremum Record]
FREE[Free Space]
DIR[Page Directory Slots]
TRA[FIL Trailer 8B]
end
FIL --> IDX --> INF --> REC --> SUP --> FREE --> DIR --> TRA
| 区域 | 作用 |
|---|---|
| FIL Header | 页号、表空间 ID、LSN、页类型 checksum |
| Index Header | 索引层信息:记录数、方向、层级、根标志 |
| Infimum / Supremum | 虚拟最小/最大记录,User Records 挂在其间 |
| User Records | 实际行数据,按主键顺序单向链表 |
| Page Directory | 稀疏槽位,二分查找入口 |
| FIL Trailer | 末尾 checksum,与 header 呼应 |
PG 用 linp 行指针数组 + ItemId
管理变长 tuple;InnoDB 用 单向链表 + Page
Directory——插入时可能重排
slot,但链表顺序保持键序。
1.2 页类型
| 类型 | 常量 | 用途 |
|---|---|---|
| FIL_PAGE_INDEX | 索引页 | 聚簇/二级 B+Tree 节点 |
| FIL_PAGE_UNDO_LOG | Undo 页 | 事务回滚段 |
| FIL_PAGE_INODE | Inode 页 | 段管理 |
| FIL_PAGE_IBUF_FREE_LIST | Change buffer | 延迟二级索引 |
| FIL_PAGE_TYPE_ALLOCATED | 新分配 | 尚未格式化 |
表空间文件(.ibd)由大量
FIL_PAGE_INDEX 组成 B+Tree。
二、FIL Header 关键字段
fil0fil.h 定义页头(38 字节,简化):
| 偏移 | 字段 | 含义 |
|---|---|---|
| 0 | FIL_PAGE_SPACE_OR_CHKSUM |
校验和 / 表空间 |
| 4 | FIL_PAGE_OFFSET |
页在表空间内偏移 |
| 8 | FIL_PAGE_PREV / NEXT |
B+Tree 兄弟页指针 |
| 24 | FIL_PAGE_LSN |
页最后修改 LSN |
| 34 | FIL_PAGE_TYPE |
页类型 |
FIL_PAGE_LSN 与 redo log
对应——崩溃恢复时判断页是否需要 redo apply。PG 页头有
pd_lsn(PageHeaderData),语义类似。
三、行记录格式(ROW_FORMAT)
3.1 隐藏列
每行用户数据外,InnoDB 自动附加:
| 列 | 作用 |
|---|---|
DB_TRX_ID |
最后修改该行的事务 ID |
DB_ROLL_PTR |
指向 undo 记录,MVCC 回滚链 |
DB_ROW_ID |
无显式主键时 InnoDB 生成的 6 字节 row id |
PG 用
xmin/xmax/ctid 存在
HeapTupleHeaderData;InnoDB 用 undo 链
+ trx id——这是 MVCC 方案的根本分叉(第 7 章)。
3.2 COMPACT / DYNAMIC(8.0 默认 DYNAMIC)
8.0 默认 ROW_FORMAT=DYNAMIC(与
COMPACT 记录头相同,溢出策略不同):
记录头(5 字节变长) 包含:
deleted_flag:删除标记(purge 前)min_rec_flag:B+Tree 最小记录n_owned:Page Directory 拥有记录数heap_no:页内序号record_type:普通 / Infimum / Supremumnext_record:链表下一记录相对偏移
变长列长度列表 + NULL 位图 + 列数据 紧随其后。
[变长长度列表][NULL 位图][记录头 5B][trx_id][roll_ptr][列1][列2]...
源码:rem0rec.h 中
rec_get_offsets() 解析记录。
3.3 溢出列(OFF-page)
当变长列超过页内阈值(约 768 字节 半页规则,视行大小与格式而定),DYNAMIC/COMPACT 将大列存到 溢出页(linked list of pages),聚簇索引叶只留 20 字节前缀。
PG 用 TOAST 外存(第
02 章 PG);InnoDB 用 overflow page
链——SELECT 大字段可能触发随机 IO。
四、聚簇索引与二级索引
4.1 聚簇索引 = 表数据
InnoDB 必须有聚簇索引:
- 若定义
PRIMARY KEY,用它聚簇 - 否则选第一个
NOT NULL UNIQUE - 都没有则隐式
DB_ROW_ID聚簇
叶节点页存完整行(除溢出部分);非叶节点存 主键 + 子页指针。
4.2 二级索引
二级索引叶节点存:
(二级索引键列..., 主键列...)
回表(lookup clustered index)=
用主键再查聚簇索引。覆盖索引(Using index)=
所需列全在二级索引中,无需回表。
flowchart LR
SEC[Secondary Index Leaf] -->|主键| CLU[Clustered Index Leaf]
CLU --> ROW[完整行]
PG 的 Index-Only Scan 需要 visibility map;InnoDB 二级索引叶 不含 trx id 可见性信息——可能需要回表判断可见性(第 7 章)。
4.3 页分裂
插入导致叶页满 → 分裂为两页,父节点可能级联分裂。分裂时 LSN 更新、redo 记录结构变更——高并发插入顺序主键(自增) vs 随机主键(UUID)的页分裂模式是生产调优经典话题(第 11 章)。
五、Page Directory 与查找
页内记录 单向链表 按键序排列,但链表扫描慢。Page Directory 在页尾存 稀疏 slot(通常每 4–8 条记录一个 slot),查找时:
- 二分 directory slot 定位区间
- 在区间内沿链表扫描
B+Tree 层:从根节点向下,每层一次二分 + 页内查找。与 PG
B-Tree 的 /_bt_search 类似,但页内布局不同(PG B-Tree
第 14 章)。
六、实验:查看表空间与页大小
需 MySQL 8.0 实例(本环境未安装——请本地验证)。
6.1 表空间元数据
SELECT space, name, space_type, file_size, fs_block_size
FROM information_schema.INNODB_TABLESPACES
WHERE name LIKE '%your_table%';file_size 以页为单位增长;默认页大小
innodb_page_size=16384。
6.2 行格式
SHOW TABLE STATUS LIKE 'your_table'\G
-- Row_format: Dynamic
SELECT * FROM information_schema.INNODB_TABLES
WHERE name = 'your_db/your_table';6.3 十六进制页 dump(高级)
社区工具 innodb_ruby 或 debug 编译 MySQL 可 dump 单页。生产环境只读副本上操作:
# 示意:从 .ibd 提取 page 0(需确认 offset 与页号)
xxd -s $((page_no * 16384)) -l 16384 table.ibd | head -20页头前 4 字节常为 checksum;偏移 24 附近可见 LSN 片段。勿在生产可写实例上随意 dd 正在写入的文件——应用快照或 offline copy。
七、PG 对照小结
| 维度 | PostgreSQL | InnoDB |
|---|---|---|
| 默认页大小 | 8KB | 16KB |
| 行头 | HeapTupleHeaderData (xmin/xmax/ctid) |
5B 记录头 + trx_id/roll_ptr |
| MVCC 存储 | 多版本行内 | undo 链外置 |
| 大字段 | TOAST | 溢出页 |
| 聚簇 | 堆表无强制聚簇 | 聚簇索引即表 |
| 页内查找 | ItemId + line pointer | 链表 + Page Directory |
八、工程坑点
无显式主键。隐式 DB_ROW_ID
聚簇 + 二级索引都变大——所有二级索引叶多 6 字节 row
id,且插入分布可能更随机。
UUID
主键。随机插入导致页分裂频繁、Buffer Pool
污染——顺序 ID 或 UUID_TO_BIN
有序化是常见方案。
ROW_FORMAT=REDUNDANT。老格式仍占用更多空间——8.0 应使用 DYNAMIC。
忽略溢出列 IO。SELECT * 大
TEXT 列可能每行多几次随机读——应用层只选需要的列。
九、边界
- 不展开
COMPRESSED页格式位图压缩细节 - 不覆盖 MyISAM 静态/动态行格式
- MariaDB 部分页结构扩展(如 instant ADD COLUMN)以各自版本文档为准
上一篇:InnoDB 架构与线程模型
下一篇:Buffer Pool 与 LRU(第 03 篇,规划中)
参考资料
- MySQL 8.0 Reference Manual, InnoDB Row Formats
- MySQL 8.0 Source,
storage/innobase/include/rem0rec.h,fil0fil.h - MySQL 8.0 Source,
storage/innobase/page/page0page.cc - 《MySQL 技术内幕:InnoDB 存储引擎》第 4–5 章(对照 8.0 标注过时处)
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【MySQL InnoDB 内核】InnoDB 架构与线程模型
InnoDB handler 边界、Master/Purge/IO/Page Cleaner 线程、内存布局与 srv0srv.cc 启动路径。
【MySQL InnoDB 内核】Buffer Pool 与 LRU:frame、flush 列表与 young/old 分区
Buffer Pool 实例、LRU 年轻/年老分区、flush 列表、buf_page_get 路径与 read-ahead。
【MySQL InnoDB 内核】Redo Log 内部机制:LSN、mtr 与组提交
Redo log buffer、LSN、checkpoint、mtr 记录类型与 innodb_flush_log_at_trx_commit 语义。
【MySQL InnoDB 内核】Undo Log 与事务回滚
Undo segment、insert/update undo、purge 线程与长事务 history list 堆积机制。