InnoDB 页结构详解:页类型、页头页尾、数据行与页目录

一、页是 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 停止

这也是"叶子层双向链表"存在的真正价值------避免每次都回到根页重新定位。


八、总结

  1. 页类型由用途决定,不止数据页:INDEX、UNDO、IBUF、XDES、FSP_HDR、INODE、SDI、BLOB 等;
  2. 64TB 上限:页号 4B → 2³² 页 × 16KB = 64TB;
  3. 页三段:File Header(38B)+ Page Body + File Trailer(8B);页尾是页头校验和/LSN 的副本,防页断裂;
  4. 数据页布局:File Header 38 + Page Header 56 + Inf/Sup 26 + User Records + Free Space + Page Directory + File Trailer 8;
  5. 记录头 40 bit:heap_no、record_type、next_record、deleted_flag、min_rec_flag、n_owned;
  6. 哨兵:Infimum(heap_no=0, type=2)、Supremum(heap_no=1, type=3);数据行 type=0 从 heap_no=2 起,非叶子目录项 type=1;
  7. 页目录:Infimum 单独一组,其余 4~8 条一组,槽记录组末地址;查找 O(log n + 8);
  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)
相关推荐
bksczm1 小时前
InnoDB 行格式详解:DYNAMIC / COMPACT / COMPRESSED / REDUNDANT
java·开发语言·数据库
余槐i1 小时前
MySQL 8.0 联合索引走错:为什么加了索引查询反而从 200ms 变成 8s
java·spring boot·mysql·docker·性能优化
字节跳动开源1 小时前
OpenViking连接业务系统,LLM Wiki一键编译
数据库·开源
进击的荆棘2 小时前
MySQL——数据库基础
数据库·mysql
Decay__2 小时前
22_MySQL 运维自动化平台 —— 部署编排 + 监控 + 巡检 + 告警 + 工单闭环一体化
运维·数据库·sql·mysql·架构
FungLeo3 小时前
成为全栈·基础补充·数据库索引:为什么加了索引还是慢
mysql·postgresql·sqlite·sql优化·数据库索引·成为全栈
codeejun3 小时前
每日一Go·MySQL-11、覆盖索引 & 回表原理
javascript·数据库·mysql
CoderYanger3 小时前
Java EE 进阶:8.MyBatis 操作数据库(入门)(第一弹)
java·数据库·spring·面试·职场和发展·java-ee·mybatis
XLYcmy4 小时前
信息检索报告数字经济赋能城市低碳治理的路径研究——基于“传统数据库 + 生成式人工智能”双轮驱动的检索实践 检索实施与结果分析 Web of Science
数据库·ai·llm·prompt·ssci·deepseek·wos