MySQL「03」数据页结构

210 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第03天,点击查看活动详情

页是 InnoDB 管理存储空间的基本单位,一个页的默认大小为16KB。 页根据其类型的不同,其中存放的数据也不同。 存放数据、索引的页,称为索引页(Index),也被称为数据页,其页类型为 FIL_PAGE_INDEX。

一个数据页从结构上可以分为七部分:

  1. File Header,占38字节,所有类型的页都有的通用信息。
  2. Page Header,占56字节,数据页特有的头信息。
  3. Infimum + Supremum,占26字节,InnoDB 自动插入的,表示当前页中最小、最大的记录。
  4. User Records,页刚创建时,此处占0字节,随着用户记录的插入而逐渐增加,对应的 free space 逐渐减少。
  5. Free Space,
  6. Page Directory,当前页中的目录,用于快速索引。
  7. File Trailer,占8字节,校验页是否完整。

上述七部分内容,除1、2、7外的空间,都与用户记录相关。 在介绍这部分内容之前,先回顾一下上节介绍的记录行格式。 记录行格式中的记录头信息与上述第3、4、5条息息相关。 理解了记录头信息中的各字段含义,也就明白了用户记录在页中的组织方式。

01-记录头信息

按照 Compact 行格式,每条记录包括两部分内容,第一部分是变长字段长度列表、空值列表、记录头信息组成的元信息; 第二部分是记录的真实数据列,加上 InnoDB 额外插入的列。 更具体的信息,参考之前的文章 InnoDB 记录结构

记录头共5字节,40位,其中每为位的含义如下:

  • 0-1,预留位,未使用
  • 2,delete_mask,标记该记录是否被删除
  • 3,mini_rec_mask,B+树的每层非叶子节点中的最小记录都会添加该标记
  • 4-7,n_owned,表示当前记录所在组内拥有的记录数
  • 8-20,heap_no,表示当前记录在当前页内的位置信息
  • 21-23,record_type,表示当前记录的类型
    • 0 表示普通记录
    • 1 表示B+树非叶子节点记录
    • 2 表示最小记录
    • 3 表示最大记录
  • 24-39,next_record,表示下一条记录的相对位置

01.1-delete_mask 删除标志位

用于表示当前记录是否被删除。 值为0时,表示未删除;否则,表示已被删除。

注:想必从这里就能够发现,通过 delete 删除的记录并未实际从内存中清除,仅仅通过标志位标识该记录已删除。 通过上述描述想必也能够推测出这部分已删除记录应该会与 free space 一起,共同组成可用空间列表。

01.2-n_owned 组内记录数

这个值在后面介绍 page directory 的时候会详细介绍。

InnoDB 为了快速查找到页内的记录,而在页内建立的目录索引,即后面会学习到的 page directory。 page directory 可以简单地理解为一个数组,数组内两个元素之间的元素组成一组,而 n_owned 这个在组内最后一个元素(即组内最大元素)的头信息里表示其所在组内的记录数量。

01.3-heap_no 页内位置

InnoDB 会在数据页中插入两个固定的记录,最小记录和最大记录。 最小记录的 heap_no 值为1,最大记录的 heap_no 值是用户记录数+2。

用户记录的 heap_no 值按照其主键的大小,从2到用户记录数+2之间。

01.4-record_type 记录类型

记录类型共四类:

  1. 0 表示普通记录
  2. 1 表示 B+ 树非页节点记录,即记录的内容为索引
  3. 2 最小记录
  4. 3 最大记录,3和4这两种,对应上节提到的 InnoDB 添加的两个最大、最小记录。

01.5-next_record 指针

前面一直提到,一条记录分为两部分内容:元信息和真实数据。 next_record 的值是到下一条记录的真实数据部分的偏移。 通过 next_record 将所有的记录(包括最大、最小记录)串联在一起,形成一个有序(主键递增)的链表。 不论做增、删、改操作,InnoDB 都始终维持页内链表有序。

通过前面几个小节的介绍,相信你对数据页中第3、4、5部分已经有了大概的认识。 一个页面的大小为16KB,在表格较简单时是能够存储大量的记录的。 为了快速在页内查找到对应的记录,InnoDB 维护了一个页目录的结构。

02-页目录

页目录的主要思想是:将所有记录组成的链表(包括最大、最小记录)划分为若干个组(假设为 n 个), 每组最后一条记录的头信息中的 n_owned 字段表示其所数组有几条记录, 然后将这组最后一条记录的地址偏移量单独提取出来,形成一个长度为 n 的数组,其中每个元素称为 slot(槽)。

对于分组上,InnoDB 有如下规定:

  • 对于最小记录所在的分组只能有 1 条记录
  • 最大记录所在的分组拥有的记录条数只能在 1~8 条之间
  • 剩下的分组中记录的条数范围只能在是 4~8 条之间

有了页目录,查找一条记录的过程如下:

  1. 先通过二分法确定目标槽,并找到槽对应组中最小的记录(页目录是一个数组,可以通过偏移找到上一个槽,其 next_record 就是目标槽的最小记录)。
  2. 然后遍历目标槽组内的记录。

03-页头部信息

数据页(或索引页)中的 Page Header 部分固定占用56个字节,共存储14种信息:

  1. PAGE_N_DIR_SLOTS,占2字节,存储了 Page Directory 中槽的数量。
  2. PAGE_HEAP_TOP,占2字节,空闲空间的最小地址,即从该地址开始,之后是 free space(第5项)。
  3. PAGE_N_HEAP,占2字节,本页记录数量,包括最大、最小记录及标记为删除的记录。
  4. PAGE_FREE,占2字节,标记了删除列表开始的位置。
  5. PAGE_GARBAGE,占2字节,已删除记录占用的字节数。
  6. PAGE_LAST_INSERT,占2字节,最后插入记录的位置。
  7. PAGE_DIRECTION,占2字节,记录插入的方向。页面中记录是有序排列的,方向指主键递增方向(右边)、主键递减的方向(左边)。
  8. PAGE_N_DIRECTION,占2字节,一个方向连续插入记录的数量。
  9. PAGE_N_RECS,占2字节,本页记录数量,不包括最小、最大及已删除记录。
  10. PAGE_MAX_TRX_ID,占8字节,修改当前页的最大事务 ID
  11. PAGE_LEVEL,占2字节,当前页在 B+ 树中所处的层级
  12. PAGE_INDEX_ID,占8字节,索引 ID,即当前页属于哪个索引
  13. PAGE_BTR_SEG_LEAF,占10字节,B+树叶子段的头部信息,仅在B+树的Root页定义。
  14. PAGE_BTR_SEG_TOP,占10字节,B+树非叶子段的头部信息,仅在B+树的Root页定义。

04-文件头部、尾部

文件头部记录了所有页都通用的信息,其中比较关键的是:

  1. FIL_PAGE_OFFSET 记录了当前页号,FIL_PAGE_PREV 和 FIL_PAGE_NEXT 记录了前一个、后一个页的页号。这些信息构成了链表。
  2. FIL_PAGE_TYPE 记录了当前页面类型,例如索引页、Undo log 页等等。
  3. FIL_PAGE_SPACE_OR_CHKSUM 当前页面内容的校验和。

文件尾部主要用作校验页面信息的完整性,包括两部分内容:

  1. 前4字节存储页面信息校验和,与文件头部的 FIL_PAGE_SPACE_OR_CHKSUM 相比,可以判断页面的完整性。
  2. 后4字节表示页面被最后修改时对应的日志序列位置 LSN,通用也是用于校验页面的完整性。