MySQL支持多种存储结构,我们常用的就是InnoDB,前面我们在 前面的 " MySQL 的 NULL 值是怎么存放的 " 一文中已经知道记录是按照行来存储的,数据读取并不是以行为单位进行读取的,因为这样效率非常低,InnoDB 是以数据页为单位进行读取的。
也就是说,当用户只需要读取一条记录的时候,并不是将这个记录从磁盘中读出,而是以页为单位,将记录所在的页整体读入内存。
InnoDB 数据页默认大小是16KB。
数据页结构分为7个部分,如下图:
数据页7个部分作用说明
字段 |
说明 |
File header 文件头部 |
页的一些通用信息 |
page header 页面头部 |
页的状态信息 |
infimum + supremum 最大、最下记录 |
两个虚拟的伪记录,页中最大、最小记录 |
User records 用户记录 |
用户存储行的记录内容 |
Free space 用户空间 |
页中还没有被使用的空间 |
Page Directory 页目录 |
页中某些记录相对的位置(对记录索引作用) |
FileTrailer 文件尾部 |
校验页是否完整 |
在 File Header 中有两个指针,分别指向上一个数据页和下一个数据页,连接起来的页相当于一个双向的链表,如下图所示:
采用链表的结构是让数据页之间不需要是物理上的连续的,而是逻辑上的连续。
数据页的主要作用是存储记录,也就是数据库的数据,所以重点说一下数据页中的 User Records 是怎么组织数据的。
用户插入数据的时候,记录会按照行格式存储到 User records 部分,在一开始的时候,没有这一部分,每当插入一条记录的时候,都会从Free space 部分划分到 User records 。
当 Free space 这部分的空间完全 被 User records这部分替换之后,也就代表着这个数据页用完了。
过程如下:
数据页中的记录按照 主键 顺序组成单向链表,单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。InnoDB是这样的吗? 答案:肯定不是啊!
我们平时在查找一本书中的某个内容的时候,会怎么做呢?一般会先去看目录,找到对应的页码,然后就可以去对应的页码页看内容了。InnoDB也实现了类似这样一个目录。
目录创建过程如下
举个例子
如下图5 个槽的编号分别为 0,1,2,3,4,我想查找主键为 11 的用户记录:
看到第三步的时候,可能有的同学会疑问,如果某个槽内的记录很多,然后因为记录都是单向链表串起来的,那这样在槽内查找某个记录的时间复杂度不就是 O(n) 了吗?
这点不用担心,InnoDB 对每个分组中的记录条数都是有规定的,槽内的记录就只有几条:
从图可以看到,页目录就是由多个槽组成的,槽相当于分组记录的索引。然后,因为记录是按照「主键值」从小到大排序的,所以我们通过槽查找记录时,可以使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到对应的记录,无需从最小记录开始遍历整个页中的记录链表。
上面都是在说一个数据页中的记录检索,因为一个数据页中的记录是有限的,且主键值是有序的,所以通过对所有记录进行分组,然后将组号(槽号)存储到页目录,使其起到索引作用,通过二分查找的方法快速检索到记录在哪个分组,来降低检索的时间复杂度。
当我们需要存储大量的记录时,就需要多个数据页,这时我们就需要考虑如何建立合适的索引,才能方便定位记录所在的页。
为了解决这个问题,InnoDB 采用了 B+ 树作为索引。磁盘的 I/O 操作次数对索引的使用效率至关重要,因此在构造索引的时候,我们更倾向于采用“矮胖”的 B+ 树数据结构,这样所需要进行的磁盘 I/O 次数更少,而且 B+ 树 更适合进行关键字的范围查询。
InnoDB 里的 B+ 树中的每个节点都是一个数据页,结构示意图如下:
通过上图,我们看出 B+ 树的特点:
我们再看看 B+ 树如何实现快速查找主键为 6 的记录,以上图为例子:
可以看到,在定位记录所在哪一个页时,也是通过二分法快速定位到包含该记录的页。定位到该页后,又会在该页内进行二分法快速定位记录所在的分组(槽号),最后在分组内进行遍历查找。