文档首页/ 云数据库 TaurusDB/ 最佳实践/ InnoDB索引页面解析
更新时间:2026-07-03 GMT+08:00
分享

InnoDB索引页面解析

背景介绍

在生产环境中,InnoDB表数据被大量删除后,原本连续存储的数据页之间会留下空闲页,导致表文件不再紧凑。然而,这些碎片页是如何产生的?能否被新插入的数据复用?能否被其他表复用?

本章节将深入剖析 InnoDB 物理文件管理机制,从原理层面解答上述疑问。

页面

InnoDB的物理文件体系主要包括系统表空间文件(ibdata*)、用户表空间文件(*.ibd)、独立的undo表空间文件(默认为undo_*)、临时表空间文件(temp_*.ibt)、redo日志文件(#ib_redo*)以及Double write文件(默认为#ib_16384_*.dblwr)等。

在这些文件中,除了redo日志文件外,其余文件都是由InnoDB标准页面(Page)构成。页(Page)是 InnoDB 中管理存储空间的基本单位,通常情况下,一个页面大小为16KB,可以通过设置innodb_page_size参数,在初始化数据库时,将页面大小调整为4KB到64KB之间的任意大小。表中的数据以记录的形式存储于页中。当查询的数据不在缓冲池(Buffer Pool)中时,InnoDB 会将该数据所在的整个页加载至 Buffer Pool 中;同理,将 Buffer Pool 中的脏页刷入磁盘时,同样以页为单位进行刷盘操作。

在InnoDB存储引擎中,存在多种页面类型,各自承担着不同的功能角色。包括:

  • 表空间首页(FIL_PAGE_TYPE_FSP_HDR):用于记录表空间的元信息。
  • 用户数据存储页面(FIL_PAGE_INDEX):用于存放用户表的数据和主键索引。
  • 回滚段页面(FIL_PAGE_UNDO_LOG):用于存储undo日志,支持事务的回滚。
  • 段索引节点页面(FIL_PAGE_INODE):用于管理数据文件中的段信息。
  • 区信息页面(FIL_PAGE_TYPE_XDES):用于维护区的元数据。
  • Insert Buffer 位图页面(FIL_PAGE_IBUF_BITMAP):跟踪各页是否有待处理的Insert Buffer 条目。

上述页面类型各异,但它们都共享相同的页面结构,包括通用页头(Fil Header)和通用页尾(Fil Trailer),其中记录的详细信息如下所示:

表1 页面结构

结构

名称

含义

字节数

通用页头

FIL_PAGE_SPACE_OR_CHKSUM

校验值。

4

FIL_PAGE_OFFSET

页面编号。

4

FIL_PAGE_PREV

前页编号。

4

FIL_PAGE_NEXT

后页编号

4

FIL_PAGE_LSN

最后修改的LSN。

8

FIL_PAGE_TYPE

页面类型。

2

FIL_PAGE_FILE_FLUSH_LSN

仅在系统表空间的第0页定义,表示文件至少被刷新到了对应的LSN值。

8

FIL_PAGE_SPACE_ID

表空间编号。

4

页面内容

...

...

...

通用页尾

FIL_PAGE_END_LSN_OLD_CHKSUM

页尾校验值 (4)。

4

LSN的低4字节。

4

我们将重点介绍与用户数据直接相关的FIL_PAGE_INDEX类型,该类型通常被称为索引页类型。

索引页特点

索引页具备以下结构特点:

  • 快速定位数据:页目录保存每组最大记录偏移量,通过二分查找与链表遍历的两步法即可快速命中目标。
  • 数据完整性与恢复:页面从磁盘读取的时候会检查一次数据页内容校验,确保崩溃后能按序恢复数据,保障数据不丢失且不被损坏。
  • 空间管理优化:空闲空间链表有效重组碎片空间,最大化提升页内空间利用率。
  • 顺序访问优化:逻辑相邻的记录被存储在同一页内,这种布局更适合顺序扫描操作,从而显著提高读取效率。

上述结构特点协同优化了 InnoDB 的查询性能,同时确保了数据的可靠性。

索引页格式

如下图所示,索引页包括七个部分:文件头部、页面头部、最大最小记录、用户记录、空闲空间、页目录、文件尾部。

图1 索引页格式
表2 索引页格式

名称

中文名

含义

File Header

文件头部

页的一些通用信息,例如当前页号。

Page Header

页面头部

数据页专有的一些信息,例如该页中记录的数量。

Infimum + Supremum

最小和最大记录

两个虚拟的行记录,分别代表页内最小/最大记录。

User Records

用户记录

实际存储的表数据。

Free Space

空闲空间

页中尚未使用的空间。

Page Directory

页目录

页中每个分组内的最大记录的相对页首的地址偏移量。

File Trailer

文件尾部

校验页是否完整。

  • 文件头部(File Header)

    文件头描述了一些针对各种页都通用的一些信息,如下表所示。

    表3 文件头

    名称

    描述

    占用空间大小

    FIL_PAGE_SPACE_OR_CHKSUM

    页的校验和(checksum值)。

    4字节

    FIL_PAGE_OFFSET

    页号。

    4字节

    FIL_PAGE_PREV

    上一个页的页号。

    4字节

    FIL_PAGE_NEXT

    下一个页的页号。

    4字节

    FIL_PAGE_LSN

    页面最新一次修改对应的日志序列号(LSN)。

    8字节

    FIL_PAGE_TYPE

    页面类型。

    2字节

    FIL_PAGE_FILE_FLUSH_LSN

    仅在系统表空间的第0页定义,代表文件至少被刷新到了对应的LSN值。

    8字节

    FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID

    页归属的表空间space_id。

    4字节

    以下介绍几个较为重要的部分:

    • FIL_PAGE_OFFSET:该页的页号,表示该页面在数据文件中的逻辑偏移量。
    • FIL_PAGE_PREV,FIL_PAGE_NEXT:分别保存前一页、后一页的页号,把物理位置不连续的页串联成B+树的同一层逻辑相邻的页。在表空间第0页上,这两个字段被重载为FIL_PAGE_SRV_VERSION 和 FIL_PAGE_SPACE_VERSION。

      一方面,大表难以一次拿到连续空间,只能分片分配;另一方面,频繁的页分裂与回收又会让逻辑相邻的页在磁盘上彼此远离。这两个指针正是用来在逻辑层面保持“索引有序”的链表结构,使得查询可以在物理不连续的页间快速跳转。

    • FIL_PAGE_LSN:当前数据页最新被修改的LSN,Redo Log幂等性依赖此字段。在进行崩溃恢复时,如果发现Redo Log的LSN小于等于该值,就不再应用该Redo Log。
  • 页面头部(Page Header)
    表4 页面头部

    名称

    占用空间

    描述

    PAGE_N_DIR_SLOTS

    2字节

    页面目录中的目录数量。一个新建的空数据页包含2个目录,它们分别指向最大记录和最小记录。其中,第一个目录永远指向最小记录,最后一个目录则永远指向最大记录。

    PAGE_HEAP_TOP

    2字节

    还未使用的空间最小地址。大于这个地址的且小于数据目录的空间都是未分配的Free Space,可以被后续使用。

    PAGE_N_HEAP

    2字节

    bit 15是紧凑格式标志位,剩余15位(bit 14-0)表示堆中记录数量。(包括最小最大记录以及标记为删除的记录)。

    PAGE_FREE

    2字节

    第一个已经被Purge物理删除的记录地址(各个已删除的记录通过next_record也会组成一个单链表,这个单链表中的记录可以被重新利用)。

    PAGE_GARBAGE

    2字节

    已删除记录占用的字节数。

    PAGE_LAST_INSERT

    2字节

    最后插入记录的位置。

    PAGE_DIRECTION

    2字节

    记录插入的方向。

    PAGE_N_DIRECTION

    2字节

    当前方向连续插入的记录数量,方向改变后清零。

    PAGE_N_RECS

    2字节

    该页中记录的数量,不包括最大和最小记录。与PAGE_N_HEAP不同,如果记录被标记为delete_marked,这个值就会递减。

    PAGE_MAX_TRX_ID

    8字节

    修改当前页的最大事务ID,该值仅在二级索引中定义。

    PAGE_LEVEL

    2字节

    当前页在B+树中所处的层级, 0即叶子节点。

    PAGE_INDEX_ID

    8字节

    索引ID,表示当前页属于哪个索引。

    PAGE_BTR_SEG_LEAF

    10字节

    B+树叶子段的头部信息,仅在B+树的Root页定义。

    PAGE_BTR_SEG_TOP

    10字节

    B+树非叶子段的头部信息,仅在B+树的Root页定义。

  • 最小最大记录(Infimum and Supremum Records)

    这是两条伪记录,在DYNAMIC行格式下,均由5字节的记录头信息和8字节的固定含义组成。由于这两条记录并非用户数据,因此被单独存放在Infimum+Supremum部分。

    根据TaurusDB规定,最小记录是该数据页逻辑上最小的记录,所有用户记录都大于它;最大记录是数据页中最大的记录,所有用户记录都小于它。这两条记录在数据页被创建时生成,且不能被删除。

    图2 最小最大记录示意图
  • 用户记录(User Records)

    用户的所有记录均以行格式存储在此处。在页面初次生成时,这部分内容为空;每次插入记录时,都会从Free Space中划分相应空间至User Records。默认情况下,记录之间并无间隙,但若重用已删除记录的空间,则可能会产生空间碎片。

    记录在页面上按照插入顺序排序,每个记录均包含一个指向下一条记录的指针(next_record),从而构成单向链表。用户可从数据页的最小记录开始遍历,直至最大记录。在聚簇索引中,记录按主键顺序存储。在二级索引中,记录按索引键+主键排序。

    User Records包含所有正常记录及所有已删除的(标记为delete_marked)记录,其中已被Purge物理删除的记录已被移入PAGE_FREE链表,不再被正常访问。

    图3 用户记录示意图
  • 空闲空间(Free Space)

    从PAGE_HEAP_TOP开始到最后一个数据目录之间的空间即为空闲空间,这些空间会被重置为0。当用户需要插入记录时,首先会尝试在被删除记录的空间中查找可用空间;若未找到合适的空间,则从这里进行分配。空间分配给记录后,需要递增PAGE_N_RECS和PAGE_N_HEAP的值。

  • 页目录(Page Directory)

    为了加速页内的查询效率,避免每次查询都对单链表进行遍历,TaurusDB把页内所有有效记录(含 Infimum/Supremum,不含已删除记录)划分成若干组进行管理。对于被标记删除,但尚未被Purge物理删除的记录,其仍保留在 Page Directory 中,并继续挂在 next_record 逻辑链表上,待后续统一清理。

    • 每个组的最后一条记录(即组内最大记录)作为owner_rec,它的 n_owned 字段保存本组记录数。
    • 将owner_rec的地址偏移量按索引顺序收集起来,形成页目录(Page Directory),存放在页尾,从高地址向低地址增长。
  • 数据页尾部(File Trailer)

    这个部分处于数据页最后的位置,只有8个字节。前4个字节代表页的校验和,后4个字节存储FIL_PAGE_LSN的低位四字节。

相关文档