MySQL存储引擎-InnoDB数据页
MySQL一个数据页默认16kb,MySQL为了不同目的涉及了很多类型的数据页,如undo页、Change Buffer页等等。我们这里只关心存放数据的页,即索引(INDEX)页。一个数据页的存储空间大致被划分为7部分,分别为:
1、FIle Header 文件头 38字节
2、Page Header 页面头 56字节
3、Infimum + Supermum 页面中的最小记录和最大记录 26字节
4、User Records 用户记录 不确定
5、Free Space 空闲空间 不确定
6、Page Directory 页目录 不确定
7、File Trailer 文件尾 8字节
一、记录在页中的存储
在一个新生成的页中,没有User Records部分,每有记录插入,都会从Free Space部分申请一个记录大小,然后划分到User Records部分。当Free Space空间用完后,再有记录插入就需要申请新的页了。我们这里再创建一个示例表:
create table page_demo( c1 int, c2 int, c3 varchar(10000), primary key(c1) ) charset = ascii row_format = compact; insert into page_demo values(1,100,'aaaa'),(2,200,'bbbb'),(3,300,'cccc'),(4,400,'dddd');
这里我们使用 hexdump 命令来查看一下数据的存储格式。
我们使用表格和十进制数据来分析一下每行记录是怎么表示的。 因为该示例表有主键,因此没有隐藏列 rowid。注:int类型占4个字节。 保留个疑问,我们可以看到c2列就是 100、200、300、400的有符号整数。 但是c1列比较奇怪,这里可以验证和主键有关系。
正常列就是按照数字进行存储的。至于主键列是怎么存储的,此处保留一个问题。
以下表:记录头信息的后6列是对记录头信息的拆分, 其中包含两个1字节的预留位未画出。
行号 | 变长字段 | NULL值列表 | 记录头信息 | delete_flag | min_rec_flag | n_owned | heap_no | record_type | nex_record | trx_id | roll_pointer | c1列 | c2列 | c3列 |
第一行 | 04 | 00 | 00 00 10 00 20 | 0 | 0 | 0 | 2 | 0 | 32 | 80 00 00 01 00 00 | 13 94 79 79 f0 00 00 | 01 72 01 10 | 80 00 00 64 | 61 61 61 61 |
第二行 | 04 | 00 | 00 00 18 00 20 | 0 | 0 | 0 | 3 | 0 | 32 | 80 00 00 02 00 00 | 13 94 79 79 f0 00 00 | 01 72 01 1c | 80 00 00 c8 | 62 62 62 62 |
第三行 | 04 | 00 | 00 00 20 00 20 | 0 | 0 | 0 | 4 | 0 | 32 | 80 00 00 03 00 00 | 13 94 79 79 f0 00 00 | 01 72 01 28 | 80 00 01 2c | 63 63 63 63 |
第四行 | 04 | 00 | 00 00 28 ff 91 | 0 | 0 | 0 | 5 | 0 | -111 | 80 00 00 04 00 00 | 13 94 79 79 f0 00 00 | 01 72 01 34 | 80 00 01 90 | 64 64 64 64 |
注:负数用补码表示。 -111的原码为 1 000 0000 0110 1111, 反码1 1111111 1001 0000,补码1 1111111 1001 0001,即16进制的ff91。
如表所示:delete_flag标志记录是否被删除,值为1代表被删除了。一条记录删除之后还是存在于磁盘的。打个删除标记加入垃圾链表,这部分空间就可以复用了。
min_rec_flag B+ 树每层非页节点最小目录项记录会添加该标记
heap_no 表示一条记录在堆中的相对位置,插入的4条记录分别为 2、3、4、5。 0和1是两条伪记录,一个代表最小即可 Infimum , 一个代表最大记录 Supermum。这两条记录的构造比较简单,都是5字节的记录头信息和8字节的固定单词组成,从上图页可以看出。另外:heap_no值一旦分配就不会发生改动了
record_type: 记录类型, 1表述非叶节点的目录项记录、 2表示Infimum记录、 3表示Supermum记录、0表示普通记录。
next_record: 表示当前真实数据到下一条真实数据的距离,32表示往后数32个字节。正数表示下一条记录在上一条记录的后面,负数表示下一条记录在当前记录的前面。最后一条记录的下一条指向的是Supermum记录。 下一条记录是按照主键值由小到大顺序排列的下一条记录。 如果删除第2条记录,那它的next_record会变为0,第一条就指向了第3条,还是形成单向链表。
二、页目录(Page Directory)
现在我们知道了数据是按照主键从小到大串联的单向链表。但是查找的时候为了提升查询速度,InnoDB设计了页目录。过程如下:
1、降所有的正常记录(包括Infimum和Supermum)进行分组,不包括已经移除到垃圾链表的即可
2、每个组的最后一条记录为大哥,其余的为小弟。大哥记录头信息中的 n_owned 属性表示该组共有几条记录
3、将每个组中最后一条记录在页面中的地址偏移量 (即该记录真实数据与页面第0个字节的距离)提取出来,按顺序存储到靠近页尾部的地方,这个地方就是Page Directory。 页目录中这些地址偏移量称为槽(Slot),每个槽占2字节,页目录就是多个槽组成的。
规定,Infimum记录独占1个分组,Supermum记录所在的分组的记录条数只能在 1~8之间,其他分组的条数需要在 4 ~ 8之间。各个槽之间是挨着的,所以,在一个数据页查找指定主键值的记录,过程分两步
1、通过二分法确定该记录所在分组对应的槽,然后找到该槽所在分组中主键值最小的那条记录。
2、通过记录的next_record属性遍历该槽所在组中的各个记录
三、页面头部(Page Header)
为了得到存储在数据页中的记录的状态信息,如数据页中存储了多少条记录,Free Space在页面中的地址偏移量,页目录中存储了多少槽等。InnoDB在数据页中定义了一个Page Header部分,占用固定的56字节。 具体用途如下:
状态名称 | 占用空间大小(字节) | 描述 |
PAGE_N_DIR_SLOTS | 2 | 页面中的槽数量 |
PAGE_HEAD_TOP | 2 | 还未使用的空间最小地址, 也就是说从该地址之后就是Free Space |
PAGE_N_HEAP | 2 | 第1位表示本记录是否为紧凑型记录,剩余15位表示本页的堆中的记录数量 (包括Infimum和Supermum以及标记为已删除的记录) |
PAGE_FREE | 2 | 各个已删除的记录通过next_record组成一个单向链表,这个单向链表中的记录所占用的存储空间可以被重新利用; PAGE_FREE表示该链表头节点对应记录在页面中的偏移量 |
PAGE_GARBAGE | 2 | 已删除记录占用的字节数 |
PAGE_LAST_INSERT | 2 | 最后插入记录的位置 |
PAGE_DIRECTION | 2 | 记录插入的方向 |
PAGE_N_DIRECTION | 2 | 一个方向连续插入的记录数量 |
PAGE_N_RECS | 2 | 该页中用户记录的数量 (不包括Infimum和Supermum以及标记为已删除的记录) |
PAGE_MAX_TRX_ID | 8 | 修改当前页的最大事务id, 该值仅在二级索引页面中定义 |
PAGE_LEVEL | 2 | 当前页在B+树中所处的层级 |
PAGE_INDEX_ID | 8 | 索引ID,表示当前页属于哪个索引 |
PAGE_BTR_SEG_LEAF | 10 | B+树叶子节点段的头部信息,仅在B+树的根页面中定义 |
PAGE_BTR_SEG_TOP | 10 | B+树非叶子节点段的头部信息,仅在B+树的根页面中定义 |
PAGE_DIRECTION: 如果新插入一条记录的主键比上一条记录的主键值大,我们称为记录的插入方向为右边,反之为左边。PAGE_DIRECTION就是用来表示最后一条记录的插入方向
PAGE_N_DIRECTION:如果连续几次插入新记录的方向都是一致的,InnoDB会把沿着同一方向插入的记录条数记下来,用PAGE_N_DIRECTION表示。
四、文件头(File Header)