页结构

由于 MySQL 的真实数据是存储在磁盘, 因此在读写数据是会涉及磁盘 IO, 为了更高效率的读取, MySQL 设计页结构, 每次交互以页为单位读取到内存. 页的大小一般为 16KB

一个数据页可以被大致划分为7个部分

File Header,表示页的一些通用信息,占固定的38字节。

Page Header,表示数据页专有的一些信息,占固定的56个字节。

Infimum + Supremum,两个虚拟的伪记录,分别表示页中的最小和最大记录,占固定的26个字节。

User Records:真实存储我们插入的记录的部分,大小不固定。

Free Space:页中尚未使用的部分,大小不确定。

Page Directory:页中的某些记录相对位置,也就是各个槽在页面中的地址偏移量,大小不固定,插入的记录越多,这个部分占用的空间越多。

File Trailer:用于检验页是否完整的部分,占用固定的8个字节。

data page 默认mysql mysql page size_主键

data page 默认mysql mysql page size_mysql page_02

上述的行格式即为 User Records的一部分

新生成的页并没有 User Records, 当插入数据时, 会从 Free Space 申请, 如果 Free Space 的空间全部被替换掉, 则说明该页满了, 需要申请新的页

Page Directory (页目录)

data page 默认mysql mysql page size_主键_03

从 select * from table wher id = 1 讲起

比较笨的方式会从最小记录开始遍历往下找, 知道找到 next_record = 0

MySQL 的优化点在于新增了一个目录, 利用二分查找优化

通过二分法确定该记录所在的槽(实际是找到确定的槽, 遍历该槽的上一个槽或者当前槽)

通过记录的next_record属性遍历该槽所在的组中的各个记录

data page 默认mysql mysql page size_data page 默认mysql_04

因为把16条记录的全部信息都画在一张图里太占地方,让人眼花缭乱的,所以只保留了用户记录头信息中的n_owned和next_record属性,也省略了各个记录之间的箭头,我没画不等于没有啊!现在看怎么从这个页目录中查找记录。因为各个槽代表的记录的主键值都是从小到大排序的,所以我们可以使用所谓的二分法来进行快速查找。5个槽的编号分别是:0、1、2、3、4,所以初始情况下最低的槽就是low=0,最高的槽就是high=4。比方说我们想找主键值为6的记录,过程是这样的:

计算中间槽的位置:(0+4)/2=2,所以查看槽2对应记录的主键值为8,又因为8 > 6,所以设置high=2,low保持不变。

重新计算中间槽的位置:(0+2)/2=1,所以查看槽1对应的主键值为4,又因为4 < 6,所以设置low=1,high保持不变。

因为high - low的值为1,所以确定主键值为6的记录在槽2对应的组中。此刻我们需要找到槽2中主键值最小的那条记录,然后沿着单向链表遍历槽2中的记录。但是我们前边又说过,每个槽对应的记录都是该组中主键值最大的记录,这里槽2对应的记录是主键值为8的记录,怎么定位一个组中最小的记录呢?别忘了各个槽都是挨着的,我们可以很轻易的拿到槽1对应的记录(主键值为4),该条记录的下一条记录就是槽2中主键值最小的记录,该记录的主键值为5。所以我们可以从这条主键值为5的记录出发,遍历槽2中的各条记录,直到找到主键值为6的那条记录即可。由于一个组中包含的记录条数只能是1~8条,所以遍历一个组中的记录的代价是很小的。

所以在一个数据页中查找指定主键值的记录的过程分为两步:

通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录。

通过记录的next_record属性遍历该槽所在的组中的各个记录。

行格式

行格式主要分为四种类型Compact、Redundant、Dynamic和Compressed. 主要理解 Compact

Compact 行格式

data page 默认mysql mysql page size_字段_05

行格式主要分为记录的额外信息, 记录的真实数据

变长字段长度列表

变长字段长度列表存储的是变长类型的真实数据的占用字节数(逆序).

如果该表没有变长类型, 则无变长字段长度列表

变长字段长度列表只保存 NOT NULL列, 如果该列允许为 NULL, 则不会保存

举例: 字段 0 int 0, 字段1 vachar(10) NOT NULL 'A', 字段2 vachar(10) default NULL 'B', 字段3 vachar(10) NOT NULL 'AA'

变长字段长度列表: 02 01

可通过 变长字段反推字段的长度

NULL 值列表

NULL 值列表只统计哪些字段允许为 NULL的值状态(逆序)

如果该表没有允许 NULL 列, 则无 NULL 值列表

MySQL规定NULL值列表必须用整数个字节的位表示,如果使用的二进制位个数不是整数个字节,则在字节的高位补0

二进制位的值为1时,代表该列的值为NULL

二进制位的值为0时,代表该列的值不为NULL

举例: 字段 0 vachar(10) NOT NULL 'A', 字段1 vachar(10) default NULL 'B', 字段2 vachar(10) default NULL NULL, 字段3 vachar(10) default NULL NULL

逆序后: 字段3 - 1, 字段2 - 1, 字段1 - 0

NULL 值列表: 00000110

可通过 NOT NULL 字段反推哪些字段值为 NULL, 哪些字段值不为 NULL

记录头

这一部分牵扯的内容较多

大致可以看一下删除标识(删除是并非真正删除, 只是修改表示), 下一条记录位置(B+树特性)

data page 默认mysql mysql page size_data page 默认mysql_06

data page 默认mysql mysql page size_data page 默认mysql_07

除了这些详细信息, 还会有 MySQL 自动添加的隐藏列

实际上这几个列的真正名称其实是:DB_ROW_ID, DB_TRX_ID, DB_ROLL_PTR, 为了美观才写成了row_id、transaction_id和roll_pointer

row_id: 有主键使用主键, 没有主键有唯一键使用唯一键, 没有则自动生成

transaction_id: 事务 ID

roll_pointer: 回滚指针

data page 默认mysql mysql page size_data page 默认mysql_08

行溢出

因为一个页占用 16KB, 16 * 1024 = 16384 字节, 而 varchar 最多可以占用65535, 不包括隐藏列和记录头信息

但是在我们使用 varchar 时, 会设置变长字段长度列表和 NULL 值列表

对于 NOT NULL, 只能使用65533字节 (两个字节用来表示长度)

对于非 NOT NULL, 只用使用 65532 字节 (两个字节用来表示长度, 一个字节用来表示 NULL 标识)

data page 默认mysql mysql page size_字段_09

对于行溢出的情况, 真实的数据只用存储前 768 字节, 后面放的是溢出页地址

行溢出的节点

综上所述, 我们知道在数据的占用字节数超过某个阈值就会发生行溢出, 那么行溢出的计算方式分析如下

ps: 截图来源于MySQL 是怎样运行的:从根儿上理解 MySQL(小孩子 4919)

data page 默认mysql mysql page size_data page 默认mysql_10

其他

delete_mask

这个属性标识该条记录是否已删除

如果 MySQL 设计删除一条则执行磁盘删除, 会导致磁盘IO增加, MySQL 为了优化, 使用一个标识表示该记录已被删除, 如果有新的记录来, 会覆盖该条记录

原理: 删除数据时, 标识该记录已删除, 并且放入垃圾链表, 新的记录插入时, 先从垃圾链表中替换

如果想要优化空间, 则执行 optimize table 'name' 即可