MySQL InnoDB数据页
一个数据页就相当于B+树中的一个节点。页是 MySQL 中磁盘和内存交互的基本单位,也是 MySQL 是管理存储空间的基本单位。一个页一般是 16KB ,当记录中的数据太多,当前页放不下的时候,会把多余的数据存储到其他页中,这种现象称为行溢出 。
一个页由7个部分组成
名称 | 中文名 | 占用空间大小 | 简单描述 |
---|---|---|---|
File Header | 文件头部 | 38 字节 | 页的一些通用信息 |
Page Header | 页面头部 | 56 字节 | 数据页专有的一些信息 |
Infimum + Supremum | 最小记录和最大记录 | 26 字节 | 两个虚拟的行记录 |
User Records | 用户记录 | 不确定 | 实际存储的行记录内容 |
Free Space | 空闲空间 | 不确定 | 页中尚未使用的空间 |
Page Directory | 页面目录 | 不确定 | 页中的某些记录的相对位置 |
File Trailer | 文件尾部 | 8 字节 | 校验页是否完整 |
User Records 用户记录区
是存储实际数据的地方,在页的7个组成部分中,我们自己存储的记录会按照我们指定的行格式
存储到User Records
部分。但是在一开始生成页的时候,其实并没有User Records
这个部分,每当我们插入一条记录,都会从Free Space
部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records
部分,当Free Space
部分的空间全部被User Records
部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。
记录头信息
在用户数据区中,记录头信息作为行格式的一部分。有下面几部分:
delete_mask
:这个属性标记着当前记录是否被删除,占用1个二进制位,值为0
的时候代表记录并没有被删除,为1
的时候代表记录被删除掉了。
min_rec_mask
:B+树的每层非叶子节点中的最小记录都会添加该标记。
n_owned
:该组内共有几条记录
heap_no
:这个属性表示当前记录在本页
中的位置。(MySQL会在位置0和1生成最小和最大的伪记录,并不在User Records
而是在Infimum + Supremum
)
record_type
:这个属性表示当前记录的类型,一共有4种类型的记录,0
表示普通记录,1
表示B+树非叶节点记录,2
表示最小记录,3
表示最大记录。record_type
为1
的表示目录项记录
next_record
:表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。比方说第一条记录的next_record
值为32
,意味着从第一条记录的真实数据的地址处向后找32
个字节便是下一条记录的真实数据。其实是个链表
,可以通过一条记录找到它的下一条记录。但是需要注意注意再注意的一点是,下一条记录
指得并不是按照我们插入顺序的下一条记录,而是按照主键值由小到大的顺序的下一条记录。而且规定 Infimum记录(也就是最小记录) 的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum记录(也就是最大记录)
Infimum + Supremum
26 个字节 两个虚拟的伪记录,分别表示页中的最小和最大记录,占固定的 26 个字节。
Page Directory 页目录
查询某条记录,最笨的办法:从Infimum
记录(最小记录)开始,沿着链表一直往后找,总有一天会找到
InnoDB为记录设计了一个目录:
- 将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。
- 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的
n_owned
属性表示该记录拥有多少条记录,也就是该组内共有几条记录。 - 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近
页
的尾部的地方,这个地方就是所谓的Page Directory
,也就是页目录
(此时应该返回头看看页面各个部分的图)。页面目录中的这些地址偏移量被称为槽
(英文名:Slot
),所以这个页面目录就是由槽
组成的。
Slot 槽
作用:将数据页内的数据分组管理,先定位到具体的槽再查到具体数据,加速二分查找。
槽位n_owned规则
- Infimum槽:固定为1条记录
- 普通槽:4-8条记录(满后分裂)
- Supremum槽:1-8条记录(弹性变化)
槽位管理规则 当槽内记录达到8条时:
- 触发分裂
- 一般平均分配,每个槽4条记录 当槽内记录少于4条时:
- 触发合并
- 与相邻槽合并
- 合并后如果超过8条,再次触发分裂
插入示例
最初始状态,是一个空表,每个数据页中都会默认包含两条系统记录
槽1:Infimum记录(n_owned=1)
槽2:Supremum记录(n_owned=1)
插入第一条用户记录时
记录顺序:
Infimum -> R1(10) -> Supremum
槽位分布:
槽1:Infimum (n_owned=1)
槽2:R1(10), Supremum (n_owned=2)
再插入第2,3,4...7条数据,都会在槽2中,
槽1: Infimum (n_owned=1)
槽2: R1(10), R2(20), R3(30), R4(40), R5(50), R6(60), R7(70), Supremum (n_owned=8)
直到第8条数据,触发分裂。
槽1: Infimum (n_owned=1)
槽2: R1(10), R2(20), R3(30), R4(40) (n_owned=4)
槽3: R5(50), R6(60), R7(70), R8(80), Supremum (n_owned=5)
每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
详细步骤说明: 假如现在要插入R9(15) R10(65)
插入R9(15)
步骤1: 二分查找页目录槽
比较槽尾记录值:
- 槽1尾记录: Infimum < 15
- 槽2尾记录: R4(40) > 15
- 槽3尾记录: Supremum > 15
找到槽2是大于15的最小差值槽
步骤2: R9(15)会插入到槽2中
记录顺序变为:
Infimum -> R1(10) -> R9(15) -> R2(20) -> R3(30) -> R4(40) -> R5(50)...
步骤3: 槽2的n_owned从4变为5
槽1: Infimum (n_owned=1)
槽2: R1,R9,R2,R3,R4 (n_owned=5)
槽3: R5,R6,R7,R8,Supremum (n_owned=5)
插入R10(65)
步骤1: 二分查找页目录槽
比较槽尾记录值:
- 槽1尾记录: Infimum < 65
- 槽2尾记录: R4(40) < 65
- 槽3尾记录: Supremum > 65
找到槽3是大于65的最小差值槽
步骤2: R10(65)会插入到槽3中
记录顺序变为:
...R4(40) -> R5(50) -> R6(60) -> R10(65) -> R7(70) -> R8(80) -> Supremum
步骤3: 槽3的n_owned从5变为6
槽1: Infimum (n_owned=1)
槽2: R1,R9,R2,R3,R4 (n_owned=5)
槽3: R5,R6,R10,R7,R8,Supremum (n_owned=6)
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 字节 | 一个方向连续插入的记录数量 |
PAGE_N_RECS | 2 字节 | 该页中记录的数量(不包括最小和最大记录以及被标记为删除的记录) |
PAGE_MAX_TRX_ID | 8 字节 | 修改当前页的最大事务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字节
Page Header
是专门针对数据页
记录的各种状态信息,比如数据页中的数据记录数量,槽的数量等;MySQL还有其他类型的页,File Header
针对各种类型的页都通用,包括的信息如,页的编号,上个页和下个页的指针等。
名称 | 占用空间大小 | 描述 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4 字节 | 页的校验和(checksum值) |
FIL_PAGE_OFFSET | 4 字节 | 页号 |
FIL_PAGE_PREV | 4 字节 | 上一个页的页号 |
FIL_PAGE_NEXT | 4 字节 | 下一个页的页号 |
FIL_PAGE_LSN | 8 字节 | 页面被最后修改时对应的日志序列位置(英文名是:Log Sequence Number) |
FIL_PAGE_TYPE | 2 字节 | 该页的类型 |
FIL_PAGE_FILE_FLUSH_LSN | 8 字节 | 仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值 |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4 字节 | 页属于哪个表空间 |
Free Space 空闲空间
页中尚未使用的部分,大小不确定
File Trailer 页尾8个字节
用于检验页是否完整的部分,占用固定的8个字节。