页是InnoDB管理存储空间的基本单位,每个页16KB
页有不同类型:比如存放表空间头部信息的页,存放 Insert Buffer 信息的页,存放 INODE 信息的页,存放 undo 日志信息的页等等
而存放数据记录的页叫索引页(INDEX),或叫数据页
数据页的基本结构
数据页的各个组成部分
其中,用户记录User Records不是始终存在的,而是每次执行insert插入数据,从Free Space中申请一块空间存放新数据,当Free Space申请完了,这个页也就用完了,如果还有新值插入,就要去申请新的页了
数据在User Record中实际会按照主键排序,而非按照插入的数据
记录头信息的实际存储内容
还记得在InnoDB行中看过Compact行格式的具体格式,其中有记录头信息
现在建一个表:
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个固定字节,简单看下里面存储的内容即可:
文件头部File Header
也是38个固定字节,简单看下其中内容即可:
其中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个字节的固定空间,防止写数据时写了一半停机,回来可以判定这个页是否是完整的
评论区