数据页
存储结构
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,正常情况为
0
1
1
,根据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相等。