Mysql之B+树索引详解(3)——聚簇索引

前沿

上一篇Mysql之B+树索引详解(2)——数据页结构中介绍了存储用户数据的数据页一些基本构成和关键字段。在此基础上,我们应能明白如何在一个数据页内快速查找数据,但如何定位到数据所在的数据页呢?这就是本章要回答的问题。本章着重介绍这些数据页在Mysql中是如何组织的,也就是Mysql的聚簇索引的构成。

数据页 or 索引页?

Mysql中用页作为基本单位来管理存储空间,页大小默认为16KB。所谓以页作为存储基本单位,是说Mysql每次存储、读取磁盘时,至少要读取、写入一个页的大小,不能部分读取,也不能部分写入。从操作系统的文件系统角度,系统IO并不关心页中写入了什么数据,其认为所有的页都是一样的,但从Mysql的软件视角来看,必须对页进行分类,不同的页用不同的结构存储不同的数据,这也是为何每个页都有一个File Header结构。File Header中除了上篇介绍的fil_page_offset、fil_page_prev、fil_page_next三个字段外,还有一个字段:fil_page_type,8字节,其表明当前的页在功能上的分类,存储用户数据和索引的页的取值都是:file_page_index,也就是我们常说的索引页。这表明Mysql使用同一个页类型来存储用户数据和索引数据,也就是说索引数据页和数据页结构几乎是一样的,只有一些细微差别,各位看官在阅读本章时可以结合上一篇进行对比。

使用索引页 索引 数据页

数据页中的数据按主键根据比较规则从小到大排列,并进行分组,所有的数据页根据根据File Header中的fil_page_prev、fil_page_next组成一个双向链表,并且前一个数据页中的所有数据的主键 小于 后一个数据页中的所有数据主键。这样一个有序的双向链表结构似乎已经可以使用二分查找进行快速检索了。但二分查找的前提是所有数据是紧密排列,例如页号为1~100,我们可以快速读取50号页判断最小最大主键值来决定向前还是向后检索。但实际上的页号并不是连续的,上一篇介绍表空间时说过,页号是在表空间的偏移量,除了数据页,还有很多其他数据也需要申请页来存储,这就导致申请用于存储数据的页号并不连续。
为了使用二分查找,我们需要记录页的总数N,然后从数据页链表的头部开始遍历N/2个数据页才能找到中间的页,这样的查找效率接近O(N),不够高效。或者我们可以把所有的数据页号使用紧密排列的方式单独存储起来,在这个基础上进行二分查找,但随着数据页的增加,不一定有能够满足这种连续存储的空间,因此Mysql最终采用了聚簇索引的方式来对数据页链表进行索引。

聚簇索引

具体来说,将每个数据页的页号和该页中的最小记录的主键称为page_no和key,分配一个索引页(结构与数据页相同),页中的User Records部分的一条记录存储一个数据页的key和page_no(key:page_no,这里称为目录项),所有目录项仍然按照主键从小到大排列,并且目录项同样具有记录头信息,通过其中的next_record字段将所有目录项构成单向链表。如下图所示:
在这里插入图片描述
图中最下层是数据页构成的双向链表,页30则是分配的索引页,其中的每条目录项,都指向下层某一个数据页。索引页大小也为16KB,但由于只存储主键和页号,因此其可以存储的目录项条数远高于一个数据页可以存储的用户数据行数。并且索引页中的目录项同样有序、分组,并使用Page Directory进行目录。也就是说,在一个索引页中查找某条id = 6的记录的耗时和在数据页中查找性能是一致的。假设一条用户数据大小100B,16KB的页中可以存储16KB / 100B = 164条记录(这里粗略假设16KB全部用于存储数据,实际不是)。假设使用long型的自增主键,一个目录项大小 = 主键大小8B+页号大小4B= 12B,则一个索引页可以覆盖16BK / 12B = 1365个数据页,可以覆盖 1365 * 164 = 223,860条用户数据。也就是说,数据量增加了1364倍,查询耗时却只增加了1倍,大大提升了查询效率。

索引页的索引页

索引页的大小也只有16KB,数据足够多的时候,一个索引页存不下所有的数据页,这个时候就再创建一个索引页,继续对剩余的数据页进行索引,例如上图的页32。可以想象,随着数据持续增加,索引页不断增长,最后索引页的数量也增加到需要更高效的查询方式的数量级时,索引页的索引页就诞生了。
前面说过,索引页中的目录项是按主键从小到大排序的,只要新增的索引页也遵循这个规则,并且保证前一个索引页中的所有目录项的主键都小于 后一个索引页中的目录项主键,那么这些索引页 就构成了有序结构,可以使用File Header中的fil_page_prev、fil_page_next字段将这些索引页串联起来构成一个双向链表。假如我们把索引页中的目录项看做是用户数据,这个新的索引页双向链表数据页双向链表并无二致。因此利用同样的办法,在索引页双向链表基础上,再创建一个索引页,用来索引(这里是动词)这些索引页。显然,新的索引页里存储的目录项仍然包含两部分内容:主键key和被索引页号page_no,也就是上图中的页33的形式。由此,所有的数据页、索引页、索引页的索引页最终组成了一个B+树,也就是Mysql的聚簇索引

  • 叶子节点存放所有用户数据,节点内按主键从小到大排序,节点间同样有序,即前一个节点的所有记录的主键均小于后一个节点。
  • 非叶子节点存放被索引的页的最小主键值和页号,同一层非叶子结点间同样满足有序。
    仍然按照上述的计算例子,一个数据页可以存储164条记录,一个索引页可以索引1365个数据页,那么:
  • 一个索引页 -> 1365数据页 -> 164条记录可以存储 223,860条数据;
  • 一个索引页的索引页 -> 1365个索引页 -> 1365 * 1365个数据页 -> 1365 * 1365 * 164条记录可以存储 3亿+ 条数据;
    当然实践中一条数据的大小不止100B,而且16KB的页不会全部用来存储数据,一个主流结论是3层B+树索引能存储2~3千万条数据。这里主要是想说明基于B+树的聚簇索引的高效:数据量按1365倍指数级增长但查询耗时却是系数=1的线性增加

查找过程示例

以上图为例,假设我们现在要查询id = 100的数据,它应该位于页9的第二条记录中。根据前面2篇和本篇的内容,我们大致梳理下查询过程:
1、读取根页33的数据到内存,读取其Page Directory数据,根据其Slot记录的主键和目标主键100对比,定位到某个分组进行遍历,找到第一条目录项:key=1,page_no =30
2、读取页30的数据到内存,根据Page Directory的Slot定位到第三条目录项:key=12,page_no = 9
3、读取页9数据到内存,重复上述查找过程,最终定位到数据记录:(100,9,x)

遗留问题

1、B+树是从下往上生长,还是从上往下生长?
从上往下生长,在创建表时(Mysql自动创建聚簇索引)根页就会自动创建,并且不会再改变,后续插入数据时就申请数据页存储,并使用根页进行索引,当达到阈值时,就会触发页分裂,生成更多的数据页和索引页,后续篇章会进行介绍。
2、数据页和索引页是同样的结构,那么Mysql如何区分哪些是数据页,哪些是索引页?
Mysql并不区分哪些是数据页,哪些是索引页,因为两个页的区别只在于其存储的数据,索引页存储的事目录项,数据页存储的是用户数据。而且两个页都是根据主键进行组织的,也就是查找方式是一样的,都是按照Page Directory -> Slot -> 记录的顺序定位。在定位到某条数据时,在数据的记录头中有一个属性名为:record_type,这个字段会标明当前记录的类别,具体取值如下:

含义
0用户数据
1目录项,表明该记录指向了某一个数据页
2Infimum记录,页中的最小记录
3Supremum记录,页中的最大记录

Mysql不断在页中查找数据,如果该记录key<100,且record_type == 1,表明该条记录是一个目录项,应该继续向下读取页继续查找,否则应该向右遍历,直到找到或者找不到记录。
之所以将record_type保留到本篇来介绍,是因为本系列遵循i+1的讲述风格,并不会一开始过多铺陈一些细节,而是按照问题、解决方案、优化的思路,力图将一个问题的来龙去脉介绍清楚,在此过程中会不断补充一些细节,保证故事的完整性。

总结

本篇介绍了聚簇索引的构成和查询流程,Mysql基于B+树构建的聚簇索引具有极其高效的查询效率,但正如“所有命运赠送的礼物,早已在暗中标好了价格”,聚簇索引提供了高效的查询性能,势必在写入数据时要费一番功夫,才能为维持查询的体面,例如如何维持页中的数据有序,如何保证页间的数据有序等问题。下一篇将介绍数据插入、删除时,聚簇索引将会如何变化来应对上述问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值