mysql的索引构建(B+树)

  • Post author:
  • Post category:mysql



目录


一、前言


二、innoDB的页


2.1 页的结构


2.2单页数据存储


三、innoDB的索引(B+树)


3.1 innoDB的查询过程


3.2构建真正的B+树索引


3.3索引类型


4、总结


一、前言

mysql,一个作为java开发人员几乎无法避开的关系型数据库,把它搞懂无疑对我们的开发和面试都有着巨大的帮助。但是mysql很庞大,这次只讲mysql的索引构建(B+树),不做其他延伸。

其实在之前的工作中,也一直说想清晰地认识一下mysql的索引,也零零散散的看过一些文章,虽会使用,但其原理一直没有一个豁然开朗的那种感觉,无意间在网上找到一本书,买来一观,才对此有了一个比较清晰的认识,在此作出总结记录,留做后用。

二、innoDB的页

我们都知道,mysql默认的数据存储引擎是innoDB,而innoDB是以页作为磁盘与内存之间交互的基本单位,我们的数据也是存储在页上面的,所以要了解mysql的索引构建,首先要知道页的结构。

2.1 页的结构

注:mysql的页默认为16k,可根据实际需要进行配置大小。

innoDB数据页结构
名称 中文名 描述
File Header 文件头部 文件的通用信息
Page Header 页面头部 数据页的专有信息
Infimum+Supremum 最小记录和最大记录 默认的两个虚拟记录
User Records 用户记录 就是我们存的数据
Free Space 空闲空间 还未使用的空间
Page Directory 页目录 后面会讲,小组的最大记录的相对位置(槽)
File Trailer 文件尾部 校验页数据的完整性

以上是页的7个组成部分,列出来只是有个概念,本次不展开讲的话,只会涉及到以下几个部分。

1.File Header:存储文件的通用信息(包含一些重要属性,如:本页页号,上一页页号,下一页页号,页类型等等)。

2.Infimum+Supremum:这个是innoDB默认在每个页都会自动生成的虚拟记录(只需要知道创建了页,这两条记录也会自动创建就行,其他的后面讲)。

3.User Records:这一部分就是用来存储我们的用户数据。

4.Page Directory:innoDB会把用户数据分为n个组,每个组的最后一条为这个组的“小组长”,这个部分就是记录“小组长”的位置(下面会讲)。

2.2单页数据存储

首先,mysql页中的数据,其实是按照顺序进行存储的,也就是说,会根据某个字段的大小,从小到大对记录进行排序存储,所以mysql的设计者规定了,每页都会有一个最小记录和最大记录,这个是mysql自己默认的两条记录,也是上面我们提到的Infimum+Supremum两条虚拟记录。

那么,怎么去区分页内的这些数据呢?所以在每一行的记录中,mysql除了会存储我们用户实际的数据,还会记录一些额外的信息,如图。

这里介绍几个比较重要的属性:

记录额外信息属性
名称 描述
deleted_flag 标记该记录是否被删除
n_owned 页面记录会被分为若干组,每组有个组长,该属性记录组中所有成员数量,组员该记录为0
heap_no 表示当前记录在页面堆中的相对位置
record_type 表示当前记录类型 0-普通用户记录,1-B+树非叶子节点的目录项记录,2-Infimum记录 ,3-Supremum记录
next_record 表示下一条记录的位置

所以我们想到的,mysql页内目前存储数据的方式是如下图所示的:

这种结构在我们对mysql表进行查询的时候,我们根据查询条件,只能通过遍历的方式进行查找,但是这无疑是效率很低的一种方式。不过我们可以想到,既然记录是有序的,那么我们是不是可以通过二分查找去提高查询效率呢?很好,mysql的设计者和你想的一样,但是,当数据量很大的时候,二分查找查询所有的话,也需要一定的时间,那么有没有办法把这个数据量缩小一点呢?答案是肯定的,mysql的设计者是这么操作的,既然是数据有序,那么就把记录分成一个一个的小组,每一组规定一个小组长(组内最大的那条记录),我只记录了小组长的大小,在查询时先对小组长进行二分查找,找到了对应的小组,再对组内记录进行遍历,因为mysql设计者对小组的数据量是做了限制的(最多8条),所以我们遍历小组的代价是很小的 ,这样一来,就大大的提高了我们页内查询效率。

那么既然要记录小组长的位置,就需要用到一个地方进行存储,这时候就用到我们上面讲的页中的Page Directory(页目录)部分来进行存储,而页目录中这些存储小组长的地址偏移量被称为槽(Slot)。

所以最终mysql的数据记录的存储结构是这样的:

三、innoDB的索引(B+树)

在介绍索引之前,我们还要回顾一下,在上一节我们还有一个没有讲的部分(File Header),这个部分我们暂时只要知道,这个部分存储了页的一些通用属性信息,包括页号,上一页页号,下一页页号,页类型等,这些属性把mysql的记录数据的页,连成了一个双向链表,使各数据页不需要在连续的物理空间存储,我们也可以根据这些属性寻找到下一页或者上一页,同时,这些页也是有序的,和页内记录顺序是一样的,结构如下图:

接下来,为了更清晰一点说明索引,我们把页的结构也进行简化,只保留页号和数据记录部分,如下图:

所以,我们通过上面的学习,能知道mysql的各个数据页是形成了一个有序的双向链表,而每页里面的数据记录,是按照指定的字段值,从小到大排序,形成了一个单向链表。

3.1 innoDB的查询过程

在我们要探寻索引的查询的时候,我们先来思考一个问题,当我们数据量很少(一个页就足够存储)的情况下,我们是怎么查询的。

分为2种情况:

1、以主键(索引键)为搜索条件:这个查找过程我们应该已经很熟悉了(不熟悉看上一节),我们可以在页目录中,用二分法快速定位到对应的槽,然后根据槽找到对应的小组,遍历组内记录,即可查询成功。

2、不以主键(索引键)为搜索条件:这种情况就只能从最小记录一个个遍历到最大记录进行对比,很显然,这种情况查询的效率很低。

然而,大多数时候,我们的数据都不可能只用一个页就能存储的下来,所以我们会有很多的数据页去存储这些记录,那么在很多的数据页中,我们又应该怎么去查询呢?

可以分为2个步骤:

1、定位到记录所在的页。

2、然后根据上面讲的页内的查找方法进行查找。

第2步我们已经会了,那么剩下的主要工作就在第1步上了,在多数据页且没有索引的情况下,无论我们是否根据主键查找,我们都无法快速定位到记录所在的页,所以只能从第一页沿着双向链表一直往下找,这种方式不用多说,肯定非常耗时,mysql肯定不会选择这种方式啦,那么我们就要找寻一种高效的方式去进行查找。

首先,我们来构建几个存储数据的页,这些页里存储了一些记录,我们用他们的第一个值来进行排序,这些值为:【1,3,4,5,8,10,12,20,100,209,220,300】。如上图 ,这里注意,页号并不是从小到大排列的,mysql虽然会尽量把页面相邻,但是因为页并不是连续的存储空间,所以他们只是通过每页的文件头的上一页、下一页的属性进行连接,建立链表关系,这里不用纠结页号,把它们当成一个名字就行。既然目前我们在多页查找中无法快速定位这些页,那我们就要想一些办法去定位记录页,我们通过之前能知道这些页形成的链表其实也是有序的,既然有序,我们又同样想到了二分,那就把这些页的编号和最小值跟我们上面讲到的记录槽的方式一样记录下来,然后进行二分查找,不就能快速定位到对应的记录页了吗。当然可以,那我们就把页内最小的那个数据的值和页号对应记录下来,形成一个目录,这样得到的目录还有一个名字,叫索引!!!所以我们的页的数据结构就变成了这个样子:

那么我们现在的查询就应该是这么一个流程了(比如我们现在需要查找键值为20的记录):

1、先从目录项(索引)中根据二分法快速定位到数据记录应该在目录项3,因为12≤20≤209,所以它对应的页号是页9。

2、然后通过上面说到的页内查找的方式进行查询。

到这里,其实我们的构建的简易索引结构就已经产生了,当然还剩最后一个问题,我们应该怎么去存储这些目录项(索引)呢?

3.2构建真正的B+树索引

上小节其实我们已经把索引的雏形已经搭建好了,但是还面临着最后一个问题,我们该怎么去存储这些目录项(索引)呢?

当然,我们可以使用类似数组的方式去存储这些目录项,但是又会面临一些其他问题,比如:

1、innoDB使用页作为存储的基本单位,默认16k,也就是最多只能保证16k的连续存储空间,当目录项越来越多的时候,就无法存储得下了。

2、我们操作mysql时,经常会涉及到增删改查,假设我们把某一页的数据都删除,那么这个目录项也就没有意义了,所以我们就需要把该目录项都删除,然后把后面的目录项都向前移动一个位置,这个显然很耗时间;或者把这个记录不删除冗余下来,但是又很耗费内存。

所以我们通过上面就明显发现,使用类似数组的方式去存储这些目录项不是一个好的方法。那么mysql是怎么去存储这些目录项的呢?mysql的设计者发现,这些目录项其实长的和用户记录也差不多嘛,那么我们干嘛不使用相同的页对这些目录项进行存储呢?但是还有个问题,怎么去区分这些目录项(索引)和普通用户记录,这时候我们就需要回到上面的一个属性了,我们在上面(2.2、单页数据存储)讨论数据的额外信息的时候,有一个record_type属性,这个属性为1的时候,就代表该条数据是目录项(索引)。

所以接下来,我们就创建一个页来对这些目录项进行存储了,如图:

那么当记录多到一个页存不下这些目录项了,那么我们要怎么办呢?当然是再添加一个目录页(好像是废话),那当目录页也很多了,查询又很慢了呢?那我们就再加一层索引页,来对这些索引页进一步进行归纳出一层索引,最后我们innoDB构建出来的B+树的索引就变成这样了:

那么我们现在的查询流程应该是这样的:

1、在根目录页(最顶层的页)通过页内的查找方式,找到对应的中间层的索引页的页号。

2、然后不断的在中间层的索引页中通过页内查找的方式找到中间层的索引页(可能有多层中间索引页),直至找到对应的叶子节点的数据页的页号。

3、在叶子节点的页中通过页内查找的方式,找到对应的数据(完成查询)。

然后我们现在再把模型简化一下就成为这样了:

我们把这个模型倒过来看,是不是就像一棵树了(上面是树根,下面是叶子节点),这其实就是B+树(矮壮型的树形结构)的结构模型了,在这个模型中,我们把数据都放在叶子节点的页(数据页)中了,剩下的非叶子节点的页,就是索引页。到此,innoDB的B+树索引就已经完全构建完成了。

ps:在这里再提一嘴,面试经常问到的,为什么使用B+树这种结构,因为通过上面的构建B+树的过程,我们可以知道,B+树其实是一种矮壮型的树形结构,这就意味着他的层级较小,存储的叶子节点更多,这样的话就可以大大的降低查询时检索的次数,提高查询效率。

3.3索引类型

在讲完索引的结构了之后,就剩最后的索引类型了,总的来说,可以分为聚簇索引和非聚簇索引,这两个的差别主要在于:聚簇索引在叶子结点的数据页中,存储的数据是以主键(没有设置主键mysql会默认维护一个row_id字段为每行的唯一标识)大小为排列顺序,存储的数据为全量的用户数据,所以,以主键为查询条件查找数据可以直接找到数据之后,拿到所有的字段(很快);非聚簇索引同样是以索引键的大小为排列顺序,不同的是,非聚簇索引的叶子节点的页里面,只存储索引键的值和主键的值,所以,以非聚簇索引键为条件进行查询,找到叶子节点的数据之后,再通过叶子节点的id,再去聚簇索引中查询一遍,才能拿到所有字段,这一操作也叫回表。

4、总结

通过上面,已经把innoDB搜索引擎默认使用的B+树的索引结构一步一步构建完成了,至于其中可能涉及到其他操作的一些原理,留待后续再补充更新。

ps:说明一下,本篇博文所有的插图,都是来自《MySQL 是怎样运行的:从根上理解 MySQL》这一书中的,是一本很不错的书籍,也是本人在网上浏览信息的时候看到的,现在也推荐一下,如有侵权,联系删除。



版权声明:本文为vatxiongxiaohui原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。