摘要

MySQL 的页有多种类型,本文主要讲述数据页的结构和记录管理,数据页也就是表中一行一行的数据组成的页。

前言

MySQL 的数据存储在磁盘中,每次执行 SQL 命令时需要把对应的记录从磁盘读取到内存中,这个过程叫做磁盘 IO。无论磁盘使用传统的机械硬盘,还是使用固态硬盘,磁盘 IO 相对于内存 IO 来说,速度都是较慢的。以机械硬盘为例,每进行一次随机 IO,都需要磁头移动到对应的磁道大约需要 10 ms,这个过程称为寻道,然后磁盘旋转到读取数据的位置,这个过程耗时与磁盘的转速有关,一般为 3 ms,但是内存的一次随机读取大约耗时在几百纳秒,比磁盘随机 IO 快了好几个数量级。

为了减少磁盘 IO 的次数,MySQL 将若干记录组成一个数据页,一般一个数据页的大小为 16 KB,每次磁盘 IO 最少读取一个数据页,很多时候还会把相邻的数据页一起读到内存,这种 IO 称为连续 IO,相对于随机 IO,省了寻道的时间,这个时间占磁盘 IO 的大头。

MySQL 有多种存储引擎,不同的存储引擎对页和行的结构设计有所不同,本文只讲述 InnoDB 使用的页和行。在 InnoDB 存储引擎中,页有多种用途,不同用途的页结构也会有所不同,下文主要讲述数据页的结构,其它类型的页结构比较相似,就不再展开了,感兴趣的可以自行了解。

1. 数据页的结构

数据页占用的16KB空间可以划分为多个部分,具体如下图所示。

MySQL 数据页号 mysql数据页是什么_mysql

  • File Header:文件头部,主要存储页的通用信息。
  • Page Header:页面头部,数据页独有的信息。
  • Infimum+Supremum:页面中最小记录和最大记录。
  • User Record:用户记录,即表中的数据行。
  • Free Space:页的空闲空间。
  • Page Directory:页目录,页中某些记录的相对位置。
  • File Trailer:文件尾部,校验页是否完整。

2. 记录在页中的存储

我们知道记录存储在页的User Record部分,具体存储形式如下图所示,

MySQL 数据页号 mysql数据页是什么_字段_02

2.1 记录的存储结构

记录的格式一般称为行格式,如果做过主从复制,应该对这个概念比较清楚。行格式有 4 种,分别为COMPACTREDUNDANTDYNAMICCOMPRESSED。行格式不同,记录在磁盘中的存储结构就不同,下文主要讲述COMPACT格式,其它行格式类似,不再展开。

COMPACT格式组成如下图所示,

MySQL 数据页号 mysql数据页是什么_mysql_03

2.1.1 变长字段列表

表中有些字段是可变长度的,例如varchar(20)类型,其中的 20 表示这个字段能够存储的最大字符数,变长字段列表就是记录变长字段实际使用的字节数。如果要将字符数转换为字节数需要确定表使用的字符集,例如 ascii 字符集,一个字符占用一个字节,如果是 utf8mb4 字符集,一个字符占用 4 个字节。

2.1.2 NULL值列表

表中有些字段是允许为空的,每个允许为空的列在 NULL 值列表中都会对应一个比特位,比特位为 1 时,说明对应的列存储的是 NULL;比特位为 0 时,说明对应的列存储的值非空。这样区分的好处是,值为 NULL 的列不需要分配存储空间,能够节省内存。

2.1.3 记录头

如上图所示,记录头包含如下结构:

名称

大小(比特)

描述

保留位

2

暂未使用

deleted_flag

1

删除标记,为 1 表示该记录已被删除

min_rec_flag

1

讲索引时再介绍

n_owned

4

一个页中的记录会分为多个组,组长的这个字段显示该分组包含的记录数

heap_no

13

表示当前记录是user_record区域(也称为堆)的第几条记录,即记录的编号

record_type

3

记录类型,0:普通记录,1:B+树非叶子节点记录,2:Infimum 记录,3:Supremum 记录

next_record

16

下一条记录在页面的偏移量

2.1.4隐藏列

每条记录都有三个隐藏列,

  • row_id:占用 6 个字节。记录的唯一标识。该列只是确保每条记录有一个唯一标识,如果表中设置了主键或非空的唯一索引,那么该列不会生成。
  • trx_id:占用 6 个字节。表示事物 ID。讲 MVCC 时再做详细介绍。
  • roll_pointer:占用 7 个字节。表示回滚指针。讲 MVCC 时再做详细介绍。

这三个隐藏列都是 InnoDB 自动生成的,用户不需要去维护。

2.2 记录的管理方式

通过行格式中的heap_nonext_record两个字段可以知道记录在页中以链表的方式存在,具体如下图所示(仅展示必须的字段),记录的链表结构

MySQL 数据页号 mysql数据页是什么_mysql_04


如图所示,有两条虚拟的记录 Infimum 和 Supremum 分别在链表的头部和尾部,表示该页中存储的最小记录和最大记录,这两条虚拟记录的heap_no固定为 0 和 1。链表中的其它记录也是按照由小到大的顺序排列的。

记录包含多个字段,如何比较记录的大小?
假设表中只有一个主键没有其它索引,那么就是通过比较主键的大小来比较记录的大小。如果没有主键还能比较隐藏列row_id的大小。

3. Page Directory(页目录)

如果存在如下SQL,其中id为主键,

select * from t where id = 7;

按照常规思路,我们需要遍历页中的所有记录,才能找到满足id = 7的记录,这样查询的效率太低了。这时可能有人会说,链表中的记录是有序的,为什么不能使用二分查找呢?虽然我们知道链表的最小值和最大值,但是不知道中间值,这是由链表的特性决定的,不能快速定位到某条记录。然而,如果能把链表转成数组,就能愉快的使用二分查找了。

InnoDB 将链表的记录进行分组,每组最大的记录的n_owned记录了组内的记录数量。每个组内最大记录的地址偏移量保存到页目录中,这些偏移量称为,每个槽占用两个字节。具体组织形式如下图所示,

MySQL 数据页号 mysql数据页是什么_sql_05

槽 0 只有一条记录,因为 InnoDB 规定了 Infimum 所在的分组只能有一条记录,即 Infimum 记录独自成组。Supremum 记录所在的分组可以有 1-8 条记录,其余分组可以有 4-8 条记录。多个槽指向的记录构成了一个有序数组,当插入一条新记录时,只需要使用二分搜索找到应该插入的槽,然后遍历槽中的记录,即可找到插入的位置。相对于遍历整页的记录,这种查询方式的效率要高很多。

4. Page Header(页面头部)

页面头部主要记录了整个页的统计信息。主要包含如下内容,

  • PAGE_N_DIR_SLOTS:占用 2 字节,表示页目录中槽的数量。
  • PAGE_HEAP_TOP:占用 2 字节,表示页中Free Space区域的首地址。
  • PAGE_N_HEAP:占用 2 字节,表示第 1 位本页的记录是否为紧凑型记录,剩余 15 位表示本页的记录条数。
  • PAGE_FREE:占用 2 字节,每个被标记删除的记录会组成一个链表,PAGE_FREE 记录该链表的头节点的偏移量。
  • PAGE_GARBAGE:占用 2 字节,已删除记录占用的字节数。
  • PAGE_LAST_INSERT:占用 2 字节,最后插入记录的位置。
  • PAGE_DIRECTION:占用 2 字节,记录插入的方向。新纪录插入到页中,会有两个方向,如果比上次插入的记录大,则算是右边插入;反之,则是左边插入。
  • PAGE_N_DIRECTION:占用 2 字节,一个方向连续插入的记录数量。反应了最近一系列插入记录的单调性。
  • PAGE_N_RECS:占用 2 字节,本页用户创建的记录数量。
  • PAGE_MAX_TRX_ID:与事务相关,后面再讲。
  • PAGE_LEVEL:与索引相关,后面再讲。
  • PAGE_INDEX_ID:与索引相关,后面再讲。
  • PAGE_BTR_SEG_LEAF:与索引相关,后面再讲。
  • PAGE_BTR_SEG_TOP:与索引相关,后面再讲。

5. File Header(文件头部)和 File Ttrailer(文件尾部)

不同类型的页,其结构也不相同,但都有文件头部和尾部。头部主要记录各种页的信息,以及页与页之间的关系。主要有如下内容,省略不用的字段,

  • FIL_PAGE_OFFSET:占用 4 字节,表示页号。
  • FIL_PAGE_PREV:占用 4 字节,表示上一页的页号。
  • FIL_PAGE_NEXT:占用 4 字节,表示下一页的页号。
  • FIL_PAGE_LSN:MVCC 相关,后面再讲。
  • FIL_PAGE_TYPE:占用 2 字节,表示该页的类型。
  • FIL_PAGE_FILE_FLUSH_LSN:MVCC 相关,后面再讲。
  • FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID:页属于的表空间。

通过FIL_PAGE_PREVFIL_PAGE_NEXT通过这两个字段,可以看出页与页之间也组成链表结构,已知页内的记录是有序的,可以猜测若干页组成的链表也是有序的,这是实现索引的基础。

文件尾部主要用于页的完整性校验。

思考题

一个页的大小为 16 KB,里面包含了固定头信息和记录信息。如果记录中某个字段占用的字节数超过了 16 KB,那么 InnoDB 会如何处理这条记录呢?