数据页结构的快速浏览数据页表明的这块16KB大小的存储空间能够被划分为多个部分,不一样部分有不一样的功能,各个部分如图所示:mysql
一个InnoDB数据页的存储空间大体被划分红了7个部分算法
一、记录头信息的秘密sql
咱们先建立一个表:ide
mysql> 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和c2列是用来存储整数的,c3列是用来存储字符串的spa
在page_demo表的行格式演示图中画出有关的头信息属性以及c一、c二、c3列的信息(其余信息没画不表明它们不存在啊,只是为了理解上的方便在图中省略了~),简化后的行格式示意图就是这样:3d
继续插入数据日志
mysql> INSERT INTO page_demo VALUES(1, 100, 'aaaa'), (2, 200, 'bbbb'), (3, 300, 'cccc'), (4, 400, 'dddd'); Query OK, 4 rows affected (0.00 sec) Records: 4 Duplicates: 0 Warnings: 0
如图所示code
因为这两条记录不是咱们本身定义的记录,因此它们并不存放在页的User Records部分,他们被单独放在一个称为Infimum + Supremum的部分,如图所示:blog
其表明的 heap_no 分别是 0 和 1索引
record_type 这个属性表示当前记录的类型,一共有4种类型的记录,0表示普通记录,1表示B+树叶节点记录,2表示最小记录,3表示最大记录。从图中咱们也能够看出来,咱们本身插入的记录就是普通记录,它们的record_type值都是0,而最小记录和最大记录的record_type值分别为2和3
next_record 它表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量 最大记录的next_record的值为0
若是从中删除掉一条记录,这个链表也是会跟着变化的,好比咱们把第2条记录删掉:
从图中能够看出来,删除第2条记录先后主要发生了这些变化: 第2条记录并无从存储空间中移除,而是把该条记录的delete_mask值设置为1。 第2条记录的next_record值变为了0,意味着该记录没有下一条记录了。 第1条记录的next_record指向了第3条记录。 还有一点你可能忽略了,就是最大记录的n_owned值从5变成了4,关于这一点的变化咱们稍后会详细说明的。
若是咱们再次把这条记录插入到表中的话
从图中能够看到,InnoDB并无由于新记录的插入而为它申请新的存储空间,而是直接复用了原来被删除记录的存储空间。
2、Page Directory(页目录)如今咱们了解了记录在页中按照主键值由小到大顺序串联成一个单链表,那若是咱们想根据主键值查找页中的某条记录该咋办呢
SELECT * FROM page_demo WHERE c1 = 3;
MySQL 的查询流程 将全部正常的记录(包括最大和最小的记录,不包括标记为已删除的记录)划分为几个组。 每一个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示该记录拥有多少条记录,也就是该组内共有几条记录。 将每一个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的Page Directory,也就是页目录(此时应该返回头看看页面各个部分地图)。页面目录中的这些地址偏移量被称为槽(英文名:Slot),因此这个页面目录就是由槽组成的。
比方说如今的page_demo表中正常的记录共有6条,InnoDB会把它们分红两组,第一组中只有一个最小记录,第二组中是剩余的5条记录,看下边的示意图:
注意最小和最大记录的头信息中的n_owned属性 最小记录的n_owned值为1,这就表明着以最小记录结尾的这个分组中只有1条记录,也就是最小记录自己。 最大记录的n_owned值为5,这就表明着以最大记录结尾的这个分组中只有5条记录,包括最大记录自己还有咱们本身插入的4条记录。
对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1~8 条件之间,剩下的分组中记录的条数范围只能在是 4~8 条之间。
因此 最小记录的 n_owned 是 1, 当前最大记录的 n_owned 是 5
以后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大而且差值最小的槽,而后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分红两个组,一个组中4条记录,另外一个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。
再次添加12条数据
比方说咱们想找主键值为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页定义 |
存储页的通用信息,比方说这个页的编号是多少,它的上一个页、下一个页是谁啦吧啦吧啦~ 这个部分占用固定的38个字节,是由下边这些内容组成的:
名称 | 占用空间大小 | 描述 |
---|---|---|
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字节 | 页属于哪一个表空间 |
对照着这个表格,咱们看几个目前比较重要的部分:
一个数据页能够被大体划分为7个部分,分别是 File Header,表示页的一些通用信息,占固定的38字节。 Page Header,表示数据页专有的一些信息,占固定的56个字节。 Infimum + Supremum,两个虚拟的伪记录,分别表示页中的最小和最大记录,占固定的26个字节。 User Records:真实存储咱们插入的记录的部分,大小不固定。 Free Space:页中还没有使用的部分,大小不肯定。 Page Directory:页中的某些记录相对位置,也就是各个槽在页面中的地址偏移量,大小不固定,插入的记录越多,这个部分占用的空间越多。 File Trailer:用于检验页是否完整的部分,占用固定的8个字节。