前言
本节介绍Mysql中InnoDB引擎中存储数据的基本结构:数据页,来说明数据和索引是如何存储和组织的,了解了数据页的结构,我们就能明白数据如何存储的,如何组织成一颗B+树,也就明白为什么推荐使用自增Id作为主键是插入、读取性能最高的主键方案。
数据页结构
数据页是InnoDB管理存储空间的基本单位,一个页的大小默认是16KB,划分为多个部分,每个部分存储特殊的数据,数据页结构如下:
用户的数据就存放在User Records
部分,Free Space
代表还未使用的空间,Page Directory
是对数据的目录索引,稍后会介绍。可以看到,实际用于存储用户数据的空间大小为16KB-128B = 16256B,其中的真正用于存放数据的空间还要更小一点,不过不用在意这些细节,我们重点关注数据在里面是如何组织的。
数据行是如何存的
我们首先看下一条数据在User Records
中存了哪些信息,这里我们不去讨论各种行记录格式细节,基本大同小异,只关注重点的记录头信息:
记录头信息结构:
我们重点介绍其中的heap_no和next_record字段:
字段 | |
---|---|
heap_no | 在一个数据页中,存入的数据紧密排列,这个结构称为堆heap,每条记录都有一个heap_no,代表该条记录在堆中的位置。 |
next_record | 下一条记录距离当前记录的距离,单位:字节数 |
heap_no
那么heap_no
是如何确定的呢?从0开始,按插入顺序分配吗?并不是,Mysq中规定,每个数据页中有固定的两条记录:heap_no = 0 的 Infimum
记录和heap_no = 2 的 Supermum
记录。Infimum代表该数据页中最小的记录, Supermum代表该数据页中最大的记录。这里的最小最大是什么意思呢?就是根据主键来比较记录的大小,如果主键是自增的数字,那就比较数字大小,如果主键是自定义的字符串或者其他类型,那就根据指定的字符集和比较规则来确定大小(关于字符集和比较规则的内容参考第一篇文章:Mysql之B+树索引详解(1)——比较规则),Infimum和 Supermum则是人为规定的最大最小,这样所有记录在数据页中就是从小到大有序排放,并且按顺序分配heap_no。这里补充一点关于主键的情况:优先使用用户指定的主键,如果没有指定,则选择非NULL的唯一索引字段,如果也没有,则生成一个隐藏列row_id作为主键。
next_record
next_record中
的值并不是heap_no
,heap_no
是序号,代表相对顺序,而next_record存的是下一条记录相对当前记录的偏移量,单位是字节数。例如某记录的next_record = 16,则表示在数据页中从当前记录开始往后再读16个字节,就是下一条记录的内容。User Records
中最后一条记录的next_record指向了Supermum记录。由此,数据页 中的用户记录、Infimum、 Supermum
构成了一个单向链表结构。
Page Directory
通过上面的内容,我们已经知道,在数据页的User Records
中存放了多条用户数据,这些数据以记录的方式,按照主键大小紧密排列,并通过记录头中的next_record
字段形成了单链表。现在假如要查询某条数据:select * from t where id = 6;
,我们暂时不讨论如何定位数据页的问题,这部分内容在后续文章介绍。假设我们已经知道了目标数据所在的数据页中,而且页中存了多条数据,那么我们如何去找到我们要的id=6
的数据呢?显而易见的方式可以从Infimum
记录开始,通过next_record
向后遍历,直到找到或者找不到目标数据,复杂度为O(n),这个效率如何呢?按实践中的经验,一行数据大小通常在200B左右,再算上记录头信息的5B,一个数据页可以存储 16256B / (200+5)B 约= 79
条记录,显然这个数量级在O(N)复杂度下还是较高,我们需要更高效的查找方式,那就是构建目录,并存放在Page Directory
中。
简单来说,Mysql把数据页中的记录(包含Infimum和Supermum记录,不包括已删除记录)按照顺序进行分组,每组包含1~8条记录,并记录每组中最大的那条记录相对于该数据页起始位置的偏移量,称为槽Slot
,所有的Slot紧密排列,存储在Page Directory
中。
如下图所示,所有Slot存储的都是该分组内最大记录的偏移量。
基于User Records 和 Page Directory结构,如何查询数据
现在要查询select * from t where id = 6;
,假设我们已经定位到数据页,下面要检索数据了。Page Header
中存储了该页有多少个Slot,由于这些Slot是按记录顺序紧密排列的,因此可以随机访问,使用二分查找法,找到中间的Slot读取对应的记录并比较主键大小,不断进行,最终能定位到一个主键值>6且离6最近的Slot.id = 8,但由于记录是单链表组织的,我们没法从后往前查找,因此需要从前一个Slot对应的分组开始顺序遍历,由于每组的记录数在1~8之内,对性能影响不大,因此顺序查找到目标记录即可。
File Header
通过上面的内容,我们的大致明白了一行记录在数据页的User records
中是如何存储、关联的,在数据页中是如何查找数据的,接下来我们要研究数据页之间是怎么组织的,如何找到数据页,这就需要先解释清楚File Header
中的结构,这里只介绍比较重要的三个内容,如下表:
名称 | 作用 |
---|---|
fil_page_offset | 唯一页号,4字节 |
fil_page_prev | 上一个页号 |
fil_page_next | 下一个页号 |
显然,页与页之间形成了双向链表结构。那么页号是如何产生的呢?它存储的是什么形式的内容呢?要回答这个问题,首先要解释下表空间这个概念。
表空间
表空间,TableSpace,是Mysql对数据文件进行管理的逻辑概念,每个表都有一个表空间,表空间对应到1个或多个实际的磁盘文件(.idb),这些文件中存储了某个表中的所有数据和索引。可以认为,一旦创建了一个表,Mysql就分配了一个对应的表空间,并在磁盘上创建.idb文件来存储实际数据。页(不仅是数据页,还包括其他类型如事务页、日志页等)则是表空间里面的最小存储管理单元,可以认为表空间是页的大池子,假如表空间位1GB,页大小为16KB,则该表空间一共有65536个页,所谓的页号,就是该页距离表空间其实位置的偏移量,例如表空间的0~ 16KB是0号页,16KB~ 32KB是1号页。
因此,所谓的页号就是0~n的数字,用4字节表示,它代表了某个页距离表空间的起始偏移量。假如要读取页号=5的数据页,mysql只需要从该表空间往后从5*16KB的地方开始,就能读取到该16KB的数据页了。
File Trailer
File Trailer
中包含了该数据页的校验和,而且File Header中也存在一份校验和,正常情况两个校验和应该是一致的,代表该数据页数据完整。当写入新数据时,在内存中计算出校验和并更新到Header和Trailer中,将该数据页标记为脏页,然后刷新到磁盘中。如果一切顺利,该数据页完整的存入磁盘,那么在下次读取出来之后,Header和Trailer中的校验和应该是一致的。如果写入磁盘时发生故障,因为Header部分会先写入磁盘,就导致前后校验和不一致,说明发生了数据错误,通常Mysql会发生Crush,需要人工介入进行数据修复。
总结
本文重点介绍了数据页的结构,并没有大而全的对各个细节进行陈列,而是关注数据如何存储、如何查询,数据页如何组织等问题进行了探讨分析,希望读者有所收获。本文内容参考了大量互联网数据,未一一注明来源,如有涉及版权问题,请与我联系。