目 录CONTENT

文章目录

InnoDB数据页结构

FatFish1
2025-04-28 / 0 评论 / 0 点赞 / 30 阅读 / 0 字 / 正在检测是否收录...

页是InnoDB管理存储空间的基本单位,每个页16KB

页有不同类型:比如存放表空间头部信息的页,存放 Insert Buffer 信息的页,存放 INODE 信息的页,存放 undo 日志信息的页等等

而存放数据记录的页叫索引页(INDEX),或叫数据页

数据页的基本结构

数据页的各个组成部分

名称

中文名

占用空间大小

简单描述

File Header

文件头部

38 字节

页的一些通用信息

Page Header

页面头部

56 字节

数据页专有的一些信息

Infimum + Supremum

最小记录和最大记录

26 字节

两个虚拟的行记录

User Records

用户记录

不确定

实际存储的行记录内容

Free Space

空闲空间

不确定

页中尚未使用的空间

Page Directory

页面目录

不确定

页中的某些记录的相对位置

File Trailer

文件尾部

8 字节

校验页是否完整

其中,用户记录User Records不是始终存在的,而是每次执行insert插入数据,从Free Space中申请一块空间存放新数据,当Free Space申请完了,这个页也就用完了,如果还有新值插入,就要去申请新的页了

数据在User Record中实际会按照主键排序,而非按照插入的数据

记录头信息的实际存储内容

还记得在InnoDB行中看过Compact行格式的具体格式,其中有记录头信息

http://www.chymfatfish.cn/archives/InnoDBjilu

现在建一个表:

CREATE TABLE page_demo(
	c1 INT,
	c2 INT,
	c3 VARCHAR(10000),
	PRIMARY KEY (c1)
) CHARSET=ascii ROW_FORMAT=Compact;
Query OK, 0 rows affected (0.03 sec)

其中设置了主键是C1,因此数据记录将按照C1进行排序,执行以下语句插入4条数据:

INSERT INTO page_demo VALUES(1, 100, 'aaaa'), (2, 200, 'bbbb'), (3, 300, 'cccc'),(4, 400, 'dddd');

可以看到这四行的实际存储格式(只看头记录信息和实际列,省略隐藏列等其他数据)如下:

delete_mask

首先,delete_mask为0,代表记录未被删除

倘若执行delete操作,删除主键为3的数据,则delete_mask置为1,同时数据实际上并不会真的被清楚,而是保存在实际的页中,这是因为要避免重新排序

如果再有一条主键为3的数据进入,会直接覆盖当前主键为3且delete_mask为1的数据,这样就避免了重新排序,但是也产生了碎片化

所有被删除的数据将组成一个垃圾链表,该链表中记录占用的空间被称为可重用空间

min_rec_mask

为0,代表这几条数据都不是B+树的非叶子节点的最小记录

heap_no与Infimum + Supremum

分别是2~5,而值为0和1的数据实际被页中的Infimum + Supremum占用了

Infimum + Supremum实际是两条伪记录,一条记录最小值,一条记录最大值,这两个不是真的记录,里面的数据存的分别是单词Infimum和Supremum

它们也不存在User Records区,而是存在Infimum + Supremum区

record_type

0 表示普通记录, 1 表示B+树非叶节点记录, 2 表 示最小记录, 3 表示最大记录

Infimum和Supremum分别存的2和3

next_record

它表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。

因此实际上页中的各个记录实际上是一个指针,本条通过next_record的值找到下一条

而链表的开头实际是Infimum,链表的结尾是Supremum,链表地址代表的是记录在next_Record处的地址,并不是记录中头记录信息或其他补充字段的地址,也不是该行记录的实际开头,而是最补充信息和实际值之间的地址

果删除主键为2的数据,会发生如下变化:

  • 修改记录1和2的指针值,这样就将记录2保留了下来,并且设置为对应的删除态

  • 同时supremum的n_owned值也从5变成了4

所以,不论我们怎么对页中的记录做增删改操作,InnoDB始终会维护一条记录的单链表,链表中的各个 节点是按照主键值由小到大的顺序连接起来的

而当再次插入主键为2的数据,可以之间复用老的数据空间,并且修改指针即可

页目录、n_owned、槽、二分查找

目前已经知道了页中的行记录是根据主键值由小到大做了一个单向链表

如果想根据主键值查询一条数据,最笨的办法就是排序查找,由小自大根据逐个遍历查找,如果找到了就可以直接返回,如果找着找着发现当前行主键值已经超过查询数据了,还没找到,那后面也不用找了

但是这种查找方法还是比较慢的,InnoDB是通过分组和二分查找进行优化的

InnoDB页中的数据分组就是槽,槽有如下原则:

  • 对所有正常记录进行分组,正常记录包括虚假的Infimum + Supremum,但是不包括删除数据

  • 第一组只有Infimum一条数据,Supremum所在组记录可以有1~8条,其他组记录数量为4~8条

  • 每个组的最后一条数据(即主键最大的记录)在其记录头信息的n_owned中记录该组数据条数

  • 把每个组最后一条数据的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这一串地址偏移量就是页目录,每个地址就是一个槽

槽和insert

根据槽的划分逻辑可知,当没有任何数据时,页里面还是有Infimum+Supremum的,因此初始有2个组

之后插入数据,经历的流程如下:

  • 在页目录的各个地址中找到比本记录主键大,并且差值最小的槽,任何把对应槽的对应记录(即最大那条)的n_owned值加1,表示本组中增加了一条记录,直到n_owned=8

  • 如果n_owned已经到8了,会将这个组拆成两个,一个组4条,另一组5条,新生成的组会在页目录中生成一个新槽,用来记录这个新组最大记录的偏移量

这也就是为什么Supremum组可能有1~8,而其他组只能是4~8的原因

槽和二分查找

现在有了槽的序号了,而且是按从小到大排序的,select语句就可以直接做二分查找了,以上图中的数据为例,查询主键为6的数据,步骤如下:

  • 确定归属分组

    • 计算中间槽的index,获取该槽的主键值,判断应该向左查还是向右查,上图有0~4,5个槽,中间槽即2,对应主键值为8,因此向左查

    • 再次计算中间槽,即1,主键值为4,槽1最大主键为4,槽2为8,可确定主键6的数据在槽2对应的分组

  • 遍历归属分组找到对应记录

    • 先找归属分组的最小记录,可以根据上一个槽记录的地址找,因为记录是单链表,每个槽记录的地址的next_record就是下一个槽最小记录的地址

    • 遍历归属分组,做排序查找,直到找到或一定找不到返回结果

页面头部Page Header

56个固定字节,简单看下里面存储的内容即可:

名称

占用空间

描述

PAGE_N_DIR_SLOTS

2

在页目录中的槽数量

PAGE_HEAP_TOP

2

还未使用的空间最小地址,也就是说从该地址之后就是 Free Space

PAGE_N_HEAP

2

本页中的记录的数量(包括最小和最大记录以及标记为删除的记录)

PAGE_FREE

2

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

PAGE_GARBAGE

2

已删除记录占用的字节数

PAGE_LAST_INSERT

2

最后插入记录的位置

PAGE_DIRECTION

2

记录插入的方向,假如新插入的一条记录的主键值比上一条记录的主键值大,我们说这条记录的插入方向是右边,反之则是左边

PAGE_N_DIRECTION

2

一个方向连续插入的记录数量,假设连续几次插入新记录的方向都是一致的, InnoDB 会把沿着同一个方向插入记录的条数记下来,一旦方向改变,这个值就直接清零

PAGE_N_RECS

2

该页中记录的数量(不包括最小和最大记录以及被标记为删除的记录)

PAGE_MAX_TRX_ID

2

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

PAGE_LEVEL

2

当前页在B+树中所处的层级

PAGE_INDEX_ID

8

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

PAGE_BTR_SEG_LEAF

10

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

PAGE_BTR_SEG_TOP

10

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

文件头部File Header

也是38个固定字节,简单看下其中内容即可:

名称

占用空间

描述

FIL_PAGE_SPACE_OR_CHKSUM

4

页的校验和(checksum值),是通过算法转换的超长字符串的值,用于提升比较性能

FIL_PAGE_OFFSET

4

页号

FIL_PAGE_PREV

4

上一个页的页号

FIL_PAGE_NEXT

4

下一个页的页号

FIL_PAGE_LSN

8

页面被最后修改时对应的日志序列位置

FIL_PAGE_TYPE

2

该页的类型

FIL_PAGE_FILE_FLUSH_LSN

8

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

FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID

4

页属于哪个表空间

其中FIL_PAGE_TYPE是页类型,其值包括:

  • FIL_PAGE_TYPE_ALLOCATED |0x0000|最新分配,还没使用

  • FIL_PAGE_UNDO_LOG |0x0002|Undo日志页

  • FIL_PAGE_INODE |0x0003|段信息节点

  • FIL_PAGE_IBUF_FREE_LIST |0x0004|Insert Buffer空闲列表

  • FIL_PAGE_IBUF_BITMAP |0x0005|Insert Buffer位图

  • FIL_PAGE_TYPE_SYS |0x0006|系统页

  • FIL_PAGE_TYPE_TRX_SYS |0x0007|事务系统数据

  • FIL_PAGE_TYPE_FSP_HDR |0x0008|表空间头部信息

  • FIL_PAGE_TYPE_XDES |0x0009|扩展描述页

  • FIL_PAGE_TYPE_BLOB |0x000A|BLOB页

  • FIL_PAGE_INDEX |0x45BF|索引页

而FIL_PAGE_PREV和FIL_PAGE_NEXT则相当于两个指针值,使得InnoDB中的所有页构成了一个双向链表

File Trailer

用于校验页的完整性的,包括8个字节的固定空间,防止写数据时写了一半停机,回来可以判定这个页是否是完整的

0

评论区