一、页是 InnoDB 的最小管理单元
1.1 页的类型:不止"数据页"一种
页类型由用途决定,不由表空间类型决定。
常用页类型:
| 页类型 | 用途 |
|---|---|
INDEX 页 (FIL_PAGE_INDEX,0x45BF) |
数据页/索引页:存放 B+Tree 的叶子/非叶子记录 |
| UNDO_LOG 页 | undo 日志页(撤销表空间中) |
| IBUF_BITMAP / IBUF_FREE_LIST 页 | Change Buffer 位图/空闲链表 |
| XDES 页 | 区组描述符页(管理 256 个区) |
| FSP_HDR 页 | 表空间头页(page 0) |
| INODE 页 | 段 inode 页(page 2) |
| SDI 页 | 表结构 JSON 描述(8.0,page 3) |
| BLOB 页 | 大字段溢出页 |
| COMPRESSED / ENCRYPTED 页 | 压缩页 / 加密页 |
| SYS / TRX_SYS 页 | 系统页 / 事务系统页 |
"索引即数据" :InnoDB 中数据存放在聚簇索引的叶子页里,所以数据页本质上就是索引页(INDEX page)。
1.2 所有页的统一骨架
不管什么类型,页都有统一的"三段式"结构:
┌──────────────────────────┐
│ File Header(38B) │ ← 页头
├──────────────────────────┤
│ Page Body(页主体) │ ← 按页类型不同而不同
├──────────────────────────┤
│ File Trailer(8B) │ ← 页尾
└──────────────────────────┘
二、页头与页尾:通用管理信息
2.1 File Header(页头,38 字节)逐字段
| 偏移 | 字段 | 长度 | 含义 |
|---|---|---|---|
| 0 | FIL_PAGE_SPACE_OR_CHKSUM | 4 | 校验和 |
| 4 | FIL_PAGE_OFFSET | 4 | 页号(页的身份证) |
| 8 | FIL_PAGE_PREV | 4 | 上一页页号 |
| 12 | FIL_PAGE_NEXT | 4 | 下一页页号 |
| 16 | FIL_PAGE_LSN | 8 | 最近一次修改的 LSN |
| 24 | FIL_PAGE_TYPE | 2 | 页类型 |
| 26 | FIL_PAGE_FILE_FLUSH_LSN | 8 | 文件 flush LSN(仅系统表空间 page 0 有意义) |
| 34 | FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4 | 表空间 ID |
LSN:是"Log Sequence Number"的缩写,表⽰⽇志序号。⽤⼀个任意的、不断增加的值表⽰⽇志中记录的操作对应的时间点,⽤8字节的⽆符号⻓整形表⽰,后⾯会详细介绍如何⽣成LSN的值。
2.2 页大小与 64TB 上限的推导
- 页号
FIL_PAGE_OFFSET占 4 字节 → 最多 2³² 个页(含页号 0); - 页大小默认 16KB = 2¹⁴ 字节;
- 上限 = 2³² × 2¹⁴ = 2⁴⁶ 字节 = 64TB。
加粗要点 :这就是"InnoDB 表最大 64TB"的来源------页号位宽 × 页大小 。严格说这是单个表空间 的上限;file_per_table=ON 时单表约等于 64TB。
2.3 File Trailer(页尾,8 字节)
| 字段 | 长度 | 含义 |
|---|---|---|
| 校验和(旧版 end lsn checksum) | 4 | 与页头校验和比对 |
| LSN 低 32 位 | 4 | 与页头 LSN 比对 |
页尾存的是页头校验和与 LSN 的副本 ,用于检测"页写到一半就崩溃"(页断裂/torn page)------头尾不一致即判定页损坏,由 Doublewrite 恢复。8.0 中还有独立的 dblwr 文件配合。
2.4 校验和与 LSN 的两个用途
| 字段 | 作用 |
|---|---|
| 校验和 | 页完整性检查(crc32/innodb/none,由 innodb_checksum_algorithm 控制) |
| LSN | 崩溃恢复判断:页 LSN < redo 中记录的 LSN → 需要重放该页 |
2.4 页间关系:PREV/NEXT 双向链表
FIL_PAGE_PREV/FIL_PAGE_NEXT把同一索引、同一层 的页串成双向链表;- 叶子层的链表按主键有序,范围扫描(BETWEEN、ORDER BY)就靠它顺序遍历;
- 非叶子层同样有链表,但主要用于管理,不用于范围扫描。
三、数据页(索引页)的完整布局
3.1 完整布局
16KB 数据页(INDEX page)
┌────────────────────────────────────┐ 偏移 0
│ File Header(38B) │
├────────────────────────────────────┤ 38
│ Page Header(56B,数据页头) │
├────────────────────────────────────┤ 94
│ Infimum(13B)+ Supremum(13B) │
├────────────────────────────────────┤ 120
│ User Records(用户数据区,数据行) │ ← 单链表
├────────────────────────────────────┤
│ Free Space(空闲区) │ ← PAGE_HEAP_TOP 起
├────────────────────────────────────┤
│ Page Directory(页目录,每槽 2B) │
├────────────────────────────────────┤ 16384-8
│ File Trailer(8B) │
└────────────────────────────────────┘ 16384
| 区域 | 作用 |
|---|---|
| File Header | 页通用信息(页号、链表、LSN、类型) |
| Page Header | 数据页专属管理信息(槽数、记录数、空闲区等) |
| Infimum/Supremum | 页内单链表的首尾哨兵 |
| User Records | 真实数据行(用户数据区) |
| Free Space | 未使用空间(插入时从这里分配) |
| Page Directory | 页内二分查找的槽数组 |
| File Trailer | 页完整性校验 |
四、数据行:页内怎么存
4.1 行格式与"额外信息 + 真实数据"
8.0 默认行格式 DYNAMIC(COMPACT 的升级版)。一条记录从记录头起点向右的物理布局:
COMPACT / DYNAMIC 行格式(一条记录的物理布局)
记录起点
│
▼
┌──────────────────┬─────────────┬──────────────────┬─────────────────────────────┐
│ 变长字段长度列表 │ NULL 值位图 │ 记录头(5B/40bit)│ 真实数据(列值 + 隐藏列) │
│ (按列逆序,2B/列)│ (1bit/列) │ heap_no/next_record│ DB_TRX_ID(6) DB_ROLL_PTR(7)...│
└──────────────────┴─────────────┴──────────────────┴─────────────────────────────┘
│
└── next_record ──▶ 下一条记录的"真实数据起点"
隐藏列 :
DB_TRX_ID(6B,事务 ID)、DB_ROLL_PTR(7B,回滚指针)、DB_ROW_ID(6B,无主键时才有)------它们在"真实数据"部分,不属于额外信息。
4.2 额外信息三件套
| 组成 | 大小 | 说明 |
|---|---|---|
| 变长字段长度列表 | 变长(每列 1~2B,逆序存放) | 记录 VARCHAR/TEXT 等变长列的实际长度 |
| NULL 值位图 | 变长(每可空的字段 1 bit) | 标记哪些列为 NULL,为 NULL 的列不存数据 |
| 记录头 | 固定 5 字节 = 40 bit | 行管理信息 |
4.3 记录头 40 bit 拆解
| 字段 | 位数 | 含义 |
|---|---|---|
| 预留 | 2 | 未使用 |
| n_owned | 4 | 该记录所在页目录分组 的记录数(只有组内最后一条记录保存此值) |
| deleted_flag | 1 | 删除标记(删除后进 PAGE_FREE 链表) |
| min_rec_flag | 1 | 非叶子层最小记录标记:B+Tree 每层最左记录置 1,辅助查找定位 |
| heap_no | 13 | 记录在页内的堆编号(0=Infimum,1=Supremum,数据行从 2 起) |
| record_type | 3 | 0 普通记录 / 1 非叶子目录项 / 2 Infimum / 3 Supremum |
| next_record | 16 | 下一条记录的相对偏移(单向链表指针) |
min_rec_flag:非叶子节点最小记录标记------B+Tree 非叶子层每个页的第一条(最小)目录项会被标记,查找时用来判断"是否走到该层最左",辅助路由;n_owned:页目录分组内记录数------只有每组最后一条记录才持有真实值,页目录二分查找时用它确定组边界。
4.4 next_record 为什么指向"交界处"
next_record指向下一条记录的"记录头与真实数据交界处"(即真实数据起点);- 好处:向右可直接读真实数据,不用先解析大小不确定的额外信息;向左则能回退到记录头;
- 页内记录因此形成单向链表(只能从头向尾遍历)。
4.5 Infimum / Supremum:页内链表的哨兵
| 记录 | record_type | heap_no | 大小 | 作用 |
|---|---|---|---|---|
| Infimum(最小行) | 2 | 0 | 13B(5B 头 + 8B "infimum") | 单链表头哨兵 |
| Supremum(最大行) | 3 | 1 | 13B(5B 头 + 8B "supremum") | 单链表尾哨兵 |
| 普通数据行 | 0 | 从 2 起递增 | 变长 | 真实数据 |
| 非叶子目录项 | 1 | 从 2 起递增 | 变长 | 存放"最小主键 + 子页页号" |
加粗要点 :这两个哨兵不是数据行 (无真实列),但有记录头,作为单链表的首尾,避免"第一行/最后一行"的边界判断与越界问题。
4.6 用户数据区与空闲区
- User Records(用户数据区):已分配的数据行区域;
- Free Space(空闲区) :尚未使用的空间,起点由
PAGE_HEAP_TOP记录,插入新行时从这里分配。
五、页目录:页内二分查找
5.1 分组规则
- 页内所有记录 (含 Infimum/Supremum)按主键顺序分组:
- Infimum 单独一组(n_owned=1);
- 其他每组 4~8 条(Supremum 所在组 1~8 条);
- 每组最后一条记录 在页内的地址,按主键从小到大记录进页目录;
- 页目录中每个地址叫一个槽(slot),每槽 2 字节。

5.2 查找过程与复杂度
- 先二分找槽:在槽数组上二分,确定目标主键落在哪个组;
- 再组内顺序找:组内最多 8 条,平均比较 4 次;
- 严格复杂度 :
O(log₂(n/8) + 8),远优于全页顺序查找的O(n);
5.3 插入/删除对页目录的影响
- 插入:在目标组内按主键插入,组记录数超 8 时分裂组 ,更新槽与
n_owned; - 删除:
deleted_flag置 1,记录进PAGE_FREE链表(垃圾链表),PAGE_GARBAGE累加字节数;空间由后续插入复用(Purge 后真正回收)。
六、数据页头 Page Header
6.1 全字段对照表
| 官方字段 | 长度 | 含义 |
|---|---|---|
PAGE_N_DIR_SLOTS |
2 | 页目录槽数量 |
PAGE_N_HEAP |
2 | 堆中记录总数(含 Infimum/Supremum,最高位标记是否有碎片) |
PAGE_N_RECS |
2 | 有效记录数(不含 Infimum/Supremum) |
PAGE_GARBAGE |
2 | 已删除记录占用的字节数 |
PAGE_N_DIRECTION |
2 | 连续同方向插入的次数 |
PAGE_HEAP_TOP |
2 | 空闲空间起始偏移 |
PAGE_LAST_INSERT |
2 | 最后插入记录的偏移 |
PAGE_FREE |
2 | 删除链表头 |
PAGE_INDEX_ID |
8 | 该页所属索引 ID |
PAGE_LEVEL |
2 | 0 = 叶子层,>0 为非叶子层 |
PAGE_MAX_TRX_ID |
8 | 页内最大事务 ID(仅二级索引页有意义) |
PAGE_BTR_SEG_LEAF |
10 | 叶子段的段头(指向 INODE 中的段 inode) |
PAGE_BTR_SEG_TOP |
10 | 非叶子段的段头 |
PAGE_DIRECTION |
2 | 最后插入方向(左/右) |
6.2
| 疑问 | 解释 |
|---|---|
| 同方向连续插入的行数 | PAGE_N_DIRECTION:连续同方向插入的次数。顺序插入时不断累加,用于判断插入模式,优化页分裂策略 |
| 叶子段头信息 | PAGE_BTR_SEG_LEAF(10B):记录叶子段的段头 (space id + page no + offset),指向 INODE 页里的段 inode,供给叶子段分配新页用 |
| 非叶子段头信息 | PAGE_BTR_SEG_TOP(10B):同上,指向非叶子段的 inode,供非叶子层分配新页用 |
七、页与页之间的关系
7.1 三种关系并存
| 关系 | 载体 | 作用 |
|---|---|---|
| 页内:单向链表 | 记录头 next_record |
页内按主键顺序遍历记录 |
| 页间:双向链表 | File Header 的 PREV/NEXT |
同一索引同一层的页前后相连,叶子层支持范围扫描 |
| B+Tree 父子 | 非叶子页的目录项(record_type=1) | 记录"最小主键 + 子页页号",构成树形索引 |
7.2 页内单链表 vs 页间双链表
| 对比项 | 页内记录 | 页间 |
|---|---|---|
| 指针 | next_record(16 bit 相对偏移) |
FIL_PAGE_PREV/NEXT(4B 页号) |
| 方向 | 单向(头→尾) | 双向 |
| 有序性 | 按主键顺序 | 叶子层按主键顺序,非叶子层按页号/主键范围 |
| 目的 | 页内遍历、插入删除 | 范围扫描、层级管理 |
7.3 叶子页链表与范围查询
WHERE id BETWEEN 10 AND 20 的执行路径:
B+Tree 定位到起始叶子页 → 沿叶子层 NEXT 链表顺序扫描 → 直到超过 20 停止
这也是"叶子层双向链表"存在的真正价值------避免每次都回到根页重新定位。
八、总结
- 页类型由用途决定,不止数据页:INDEX、UNDO、IBUF、XDES、FSP_HDR、INODE、SDI、BLOB 等;
- 64TB 上限:页号 4B → 2³² 页 × 16KB = 64TB;
- 页三段:File Header(38B)+ Page Body + File Trailer(8B);页尾是页头校验和/LSN 的副本,防页断裂;
- 数据页布局:File Header 38 + Page Header 56 + Inf/Sup 26 + User Records + Free Space + Page Directory + File Trailer 8;
- 记录头 40 bit:heap_no、record_type、next_record、deleted_flag、min_rec_flag、n_owned;
- 哨兵:Infimum(heap_no=0, type=2)、Supremum(heap_no=1, type=3);数据行 type=0 从 heap_no=2 起,非叶子目录项 type=1;
- 页目录:Infimum 单独一组,其余 4~8 条一组,槽记录组末地址;查找 O(log n + 8);
- 页间:同层双向链表 + B+Tree 父子关系;页内单向链表。
一张图串起来
表空间(.ibd)
└─ 区组(256 区)
└─ 区(64 页)
└─ 页(16KB)
├─ File Header(38B)
├─ Page Header(56B)
├─ Infimum/Supremum(26B)
├─ User Records(数据行:额外信息 + 真实数据,next_record 单链表)
├─ Free Space
├─ Page Directory(槽:二分查找)
└─ File Trailer(8B)