数据页
存储结构
MySQL中有不同的存储引擎,其存储方式均有所不同,以下仅以InnoDB引擎为例
- 读取方式:页 InnoDB将数据库中的数据划分为若干页,每次进行数据存取是都以页为单位,页的大小是16KB.每一个页面上以记录的形式存放着相关数据,每条记录又可分为真实数据、额外数据和隐藏列(以COMPACT行格式为例),额外数据是用来辅助存储和读取信息的,包括变长字段长度列表、NULL值列表和记录头信息。
变长字段长度列表 常见的变长字段类型包括VARCHAR、TEXT类型等,这些数据类型的长度是不固定的,为了从记录中准确读取它的内容,就需要记录它所占据的字节数。
- 变长字段的长度是按逆序存放的。例如变长字段1占据3字节,字段2占据8字节,则变长字段长度列表的存放就是0x080x03。
- 假设某个字段L是VARCHAR(m)类型,采用的字符集最多需要使用w字节来表示一个字符,且其真实占用的字节数是L。当m×w的乘积结果小于等于255,则其只需要用1个字节来存放它的长度;当乘积结果大于255且L也大于127,则需要用2两个字节来存放它的长度,否则用1个字节即可。
- 变长字段长度列表只存储值为非NULL的变长字段列。
NULL值列表 该列表仅在表中允许存储NULL值的列存在的时候存在。其值等同于对应列按整数个字节逆序存放,并且用二进制表示。例如有三个列允许NULL值存在,且仅有第二和第三列为NULL,则其对应的NULL值列表的二进制表示为00000110。由于仅有三个列,需要1个字节来存放即可,高位用0补全,同时考虑到第二和第三列为NULL,正常情况为
011,根据COMPACT行格式的存放规则,需要逆序存放,故得到的结果为00000110,即0x06。记录头信息 该部分固定由5字节组成,用于描述记录的一些属性。此处仅列出比较重要的字段,不再多做赘述,可以查找资料了解即可。
名称 大小(bit) 作用 n_owned 4 组中的领头节点记录该组中的记录条数 deleted_flag 1 标记该记录是否被删除 min_rec_flag 1 B+树中每层非叶子节点的最小的目录项 heap_no 13 当前记录在堆中的相对位置 record_type 3 当前记录的类型 next_record 16 下一条记录的位置 隐藏列 包括row_id、trx_id和roll_pointer。row_id非必须,只有表中没有定义主键,也没有不允许存储NULL值的UNIQUE键时,才会添加该字段,占用6字节。trx_id时必须的,占用6字节,属于事务id。roll_pointer也是必须的,占用7字节,用于进行事务回滚。
- 溢出列 当某条记录所占空间过大时,在记录的真实数据处只会存放该字段的部分数据,然后用20字节指向剩余数据存放页的位置。
数据页结构
数据页大小为16KB,主要可以分为七大部分,分别是
File Header\Page Header\Infimum + Supremum\User Records\Fres Space\Page Directory\File Trailer.模块 占用空间(字节) 作用 文件头 38 页的通用信息 页面头 56 数据页的专有信息 页面的最大记录和最小记录 26 两个虚拟记录 数据(用户记录) 不定 用户存储的数据 空白区域 不确定 页面中尚未使用的空间 页目录 不确定 页面中某些记录的相对位置 文件尾 8 校验页的完整性 数据相关操作
- 增 - 往用户记录空间插入一条记录
- 每个页中都有两条默认插入的记录,分别是最小记录Infimum和最大记录Supremum,记录的大小通过主键的大小来确定,这两条是默认最大和最小的,但这两条记录存放在用户记录区域的靠前部分。
- 页面中所有记录都会分组,Infimum所在的组只能有1条记录,即它自己,Supremum所在的记录数量限制在1-8条,其余组的记录数量限制在4-8条。新增的记录从最近的比它大的记录所在的组开始插入。当组中记录的数量达到9条,从中间划分,生成一个新的组,前一个组的大小为4,后一个组的大小为5。
- 删 - 从用户记录空间删除一条记录
- 被删除的记录并不会直接从磁盘中删除,而是修改deleted_flag,其值为1时表示被删除,同时修改其next_record值为1,表示没有下一条记录。
- 所有被删除的记录会组成一个垃圾链表,按照主键由小到大的顺序链接,这些链表占用的空间被称为可重用空间。
- 查 - 从用户记录空间查询一条记录
- 每一组中最后一条记录的真实数据与该页开头的偏移量称为槽,按照组的逆序排列在页目录中,且从靠近文件尾的部分开始存储,每个槽占用2个字节。

- 按照二分法,low = 槽0, high = 槽n,先计算中间的槽的主键值(对应该组中最大的记录的主键值),然后修改low或者high。直到确认目标主键在哪一组中,遍历该组即可。(每一组最多只有8条记录,遍历速度很快)
- 每一组中最后一条记录的真实数据与该页开头的偏移量称为槽,按照组的逆序排列在页目录中,且从靠近文件尾的部分开始存储,每个槽占用2个字节。
- 增 - 往用户记录空间插入一条记录
记录头信息
- n_owned : 每个页面中的记录会分为若干个组,组内最大的记录会记录该组内有多少条记录
- heap_no : 存放在页面前面的记录heap_no值偏小,每新生成一条记录的存储空间,其值都比物理位置在它前面的那条记录的heap_no值大1。heap_no从2开始,因为0和1分别指代了Infimum和Supremum。
- record_type : 0表示普通记录,1表示B+树非叶子节点的目录项记录,2表示Infimum,3表示Supremum。
- next_record : 当前记录的数据部分到下一条记录的数据部分。注意每条记录由记录头部分和数据部分组成,这个指向的是数据部分的开头,这样的好处是向后可以遍历数据,向前可以遍历该记录的记录头。当这个值为正数,说明下一条记录(指按主键排列的下一条记录,不是插入顺序的下一条记录)在当前记录的后面,反之在前面。规定Infimum记录的下一条记录就是当前页面主键值最小的记录,Supremum记录的next_record值为0,表示没有下一条记录。
页面头
页面头是页结构的第二部分,占用56字节,其各个部分的作用如下表所示。名称 占用空间(字节) 作用 PAGE_N_DIR_SLOTS 2 页目录中的槽数量 PAGE_HEAP_TOP 2 空闲空间起始地址 PAGE_N_HEAP 2 前1比特表示本记录是否为紧凑型的记录,后15bit表示本页中记录的数量,包括Infimum和Supremum,以及删除的记录) PAGE_FREE 2 已删除的记录链表中起始记录所在的偏移地址 PAGE_GARBAGE 2 已删除的记录占用的字节数 PAGE_LAST_INSERT 2 最后插入的记录的位置 PAGE_DIRECTION 2 记录插入的方向(新插入的记录的主键比上一条大或者小,前者认为是向右,否则向左) PAGE_N_DIRECTION 2 一个方向连续插入的记录数量(仅记录最新值) 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+树的根页面中定义 文件头
文件头是页结构的第一部分,占用38字节,其各个部分的作用如下表所示。名称 占用空间(字节) 作用 FIL_PAGE_SPACE_OR_CHKSUM 4 在MySQL4.0.14之后的版本中,该属性表示页的校验和 FIL_PAGE_OFFSET 4 页号 FIL_PAGE_PREV 4 上一页的页号 FIL_PAGE_NEXT 4 下一页的页号 FILE_PAGE_LSN 8 页面被最后修改时对应的LSN值(Log Sequence Number,日志序列号) FIL_PAGE_TYPE 2 该页的类型 FIL_PAGE_FILE_FLUSH_LSN 8 仅在系统表空间的第一个页中定义,代表文件至少被刷新到了对应的LSN值 FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID 4 页属于哪个表空间 其中FIL_PAGE_TYPE表示某个页面的类型,包括日志页、溢出页等,其可能的值见下表。
类型名称 值 类型 FIL_PAGE_TYPE_ALLOCATED 0x0000 新分配未使用 FIL_PAGE_UNDO_LOG 0x002 undo日志页 FIL_PAGE_INODE 0x003 存储段信息 FIL_PAGE_IBUF_FREE_LIST 0x004 Change Buffer空闲列表 FIL_PAGE_IBUF_BITMAP 0x005 Change Buffer的属性 FIL_PAGE_TYPE_SYS 0x006 系统数据 FIL_PAGE_TYPE_TRX_SYS 0x007 事务系统数据 FIL_PAGE_TYPE_FSP_HDR 0x008 表空间头部信息 FIL_PAGE_TYPE_XDES 0x009 存储区的属性 FIL_PAGE_TYPE_BLOB 0x00A 溢出页 FIL_PAGE_INDEX 0x45BF 索引页,也就是我们说的数据页 文件尾
- 文件尾是页的最后一部分,占用8个字节。主要用于进行页面的校验。
- 文件尾的前4个字节是该页面的校验和,正常情况下其值等于文件头中的校验和。如果该页面被修改,会重新计算这两个部分的校验和。当该页面被刷新到磁盘中时,由于文件头在前,其值会被先写入磁盘。如果刷新页面的过程中发生了断电,重启服务后只需比较页面中的两个校验和即可,如果两个校验和相等,说明该页刷新成功,否则说明该页在刷新过程中发生了错误。
- 文件尾的后四个字节表示最后修改时对应的LSN的值,正常情况下与文件头中的FILE_PAGE_LSN相等。