记录在页中的存储
一个InnoDB
数据页的存储空间大致被划分为7个部分,如下面表格所示。
名称 | 中文名 | 占用空间大小 | 简单描述 |
---|---|---|---|
File Header | 文件头部 | 38字节 | 页的一些通用信息 |
Page Header | 页面头部 | 56字节 | 数据页专有的一些信息 |
Infimun+Supremum | 26字节 | 标记该记录是否被删除 | 两个虚拟的记录 |
User Records | 用户记录 | 不确定 | 用户存储的记录内容 |
Free Space | 空闲空间 | 不确定 | 页面中尚未使用的空间 |
Page Directory | 页目录 | 不确定 | 页中某些记录的相对位置 |
File Trailer | 文件尾部 | 8字节 | 校验页是否完整 |
User Records
是用来存储用户记录的,但是在一开始生成页的时候,并没有User Records
部分,每当插入一条数据时,都会从Free Space
部分申请一个记录的大小,并将这个空间划分到User Records
部分,当Free Space
部分的空间全部被User Records
部分替代掉之后,说明这个页也用完了,再插入新的数据,就需要申请新的页了。
为了更好的管理User Records
部分的记录,还是要先研究一下行记录的记录头信息。
这个是行格式示意图。
变长字段长度列表 | NULL值列表 | 记录头信息 | 列1的值 | 列2的值 | ··· | 列3的值 |
---|
我们把其中的行格式的记录头信息拿出来单说,下面是记录头信息的各个属性的大体意思。
名称 | 大小(比特) | 描述 |
---|---|---|
预留位1 | 1 | 没有使用 |
预留位2 | 1 | 没有使用 |
deleted_flag | 1 | 标记该记录是否被删除 |
min_rec_flag | 1 | B+树中每层非叶子节点中的最小目录项记录都会添加该标记 |
n_owned | 4 | 一个页面中的记录会被分成若干个组,每个组中有一个记录是“带头大哥”,其余的记录都是“小弟”,“带头大哥”记录的n_owned 值代表该组中所有的记录条数,“小弟”记录的n_owned 值为0 |
heap_no | 13 | 表示当前记录在页面堆中的相对位置 |
record_type | 3 | 表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点的目录项记录,2表示Infimun 记录,3表示Supremum 记录 |
next_record | 16 | 表示下一条记录的相对位置 |
deleted_flag
:用来标记当前记录是否被删除,0表示没有被删除,1表示已经被删除。这些删除的记录并没有从磁盘上移除,所有被删除的记录形成一个垃圾链表,记录在这个链表中的空间称为可重用空间,之后有新记录插入到表中,它们就可能覆盖掉被删除的这些记录占用的空间。
min_rec_flag
:B+树中每层非叶子节点中的最小目录项记录都会添加该标记。
n_owned
:页中每个组的最后一个记录(也就是组内最大的那条记录),这条记录的头信息n_owned
属性表示该组内共有几条记录,组内其余记录的n_owned
值为0。
heap_no
: 我们向表中插入的数据都是存放在User Records
中,这些记录一条一条亲密无间的排列着,如下图所示:
我们把这样的结构称为堆,把一条记录在堆中的位置称为heap_no
,在页前面的记录heap_no
相对较小,在页面后面的记录的heap_no
相对较大。每重新申请一条记录的存储空间时,该条记录比物理位置在它前面的那条记录的heap_no
值大1。
每个页面中会有两条伪记录,这两条记录并不是用户自己添加的,一条代表页面中最小记录(Infimum
记录),一条代表页面中最大的记录(Supremum
记录)。
通常比较一个记录的大小是比较主键的大小。
Infimum
记录和Supremum
记录没有主键值,但是人为规定Infimum
记录是一个页面中最小的记录,Supremum
记录是一个页面中最大的记录。
Infimum
记录和Supremum
记录的heap_no
分别是0和1。
record_type
:这个属性表示当前的记录的类型,总共有4中类型:
- 0表示普通记录;
- 1表示B+树叶节点的目录项记录;
- 2表示
Infimum
记录; - 3表示
Supremum
记录;
next_record
:表示当前记录的真实数据到下一条记录的真实数据的距离,如果该值为正数,表示当前记录的下一条记录在当前记录的前面,若为负数,表示当前记录的下一条记录在当前记录的后面。
下一条记录并不是指的是插入顺序的下一条记录,而是按照主键值由小到大的顺序排列的下一条记录。
记录按照主键从小到大的顺序形成了一个链表,Supremum
记录的next_record
值为0,表示没有下一条数据了,意味着next_record
就是这个单向链表中的最后一个节点。
无论怎样增删查改,InnoDB
始终会维护记录的一个单向链表,这个链表的各个节点都是按着主键值由小到大顺序链接起来的。
页目录
现在我们知道了记录在页中是按照主键值由小到大的顺序串联成单向链表,当使我们执行下列的查询语句,是怎么查询的呢?
select * from page_demo while c1=3;
并不是从Infimum
记录开始,沿着单向链表一直往后找,而是采用类似书的目录的方法,过程如下:
- 将所有正常的记录(包括
Infimum
记录和Supremum
记录,但不包括已经移除的记录)划分为几个组。 - 每个组的最后一个记录(也就是组内最大的那条记录),这条记录的头信息
n_owned
属性表示该组内共有几条记录。 - 将每个组中的最后一条记录在页面中的地址偏移量单独提取出来,按顺序存储到页尾的地方,页目录中的这些地址偏移量称为槽,每个槽占用两个字节,页目录就是由多个槽组成的。
并且每个分组中的记录条数是有规定的:对于Infimum
记录所在的分组只能有一条记录,Supremum
记录所在的分组的记录条数只能在1-8条之间,剩下分组的条数范围在4-8条之间,步骤如下:
- 在初始情况下,一个数据页只有
Infimum
记录和Supremum
记录这两条记录,它们分属两个组,页目录中也有两个槽。 - 之后每插入一条记录,都会从页目录中找到对应的主键值比待插入记录的主键值大并且差值最小的槽,然后把该槽对应的记录的
n_ownd
值加1。 - 当一个组中的记录数等于8后,再插入一条记录,会将组中的记录拆分两个组,其中一个组中4条记录,另一个5条记录,这个拆分过程会在页目录中新增一个槽,记录这个新增分组中最大的那条记录的偏移量。
所以在一个数据页中查找指定主键值的记录时,分为以下两步。
- 通过二分法确定该记录所在分组对应的槽,然后找到该槽所在分组中主键值最小的那条记录。
- 通过记录的
next_record
属性遍历该槽所在组中的各个记录。