MySQL索引-B+树是如何⽣⻓的

134 阅读12分钟

Mysql索引的B+树的⽣⻓流程如下图所示:

1.B+索引树是如何⽣⻓的

1.1 ⽆索引时的数据查询

数据⻚是Mysql中数据管理的最⼩单元,既然我们要研究索引是如何⾼效查询数据的,⾸先我们肯定要搞清楚数据是如何存放的,数据⻚的结构通过上篇⽂章我们了解到⼤概是这样的:

⽽数据表中的每⾏数据就存放在数据区中,数据区中每⾏数据以单向链表的⽅式,通过指针连接起来,如下图所示:

同时每个数据⻚之间再通过双向链表的⽅式组织连接起来,如下图所示:

(1)⽆索引时的数据查询

通过以上对数据⻚以及数据⻚内部数据结构初步的分析,现在我们就可以看下,如果说要查询某张表的某⾏数据会经过什么样的流程。

数据⻚⼀开始当然是存放在磁盘中的,⼀张表对⼀般会应着多个数据⻚,查询数据时从磁盘中依次加载数据⻚到InnoDB的缓冲池中,然后对缓冲池中缓存⻚的每⾏数据,通过数据⻚的单向链表⼀个⼀个去遍历查找,如果没有找到,那么就会顺着数据⻚的双向链表数据结构,依次遍历加载磁盘中的其他数据⻚到缓冲池中遍历查询。

⼤家可以看到,像上⾯这样的查询⽅式就有点傻了,因为如果恰好你要查的数据⾏在这张表最后⼀个数据⻚的最后⼀⾏,那岂不是所有的数据⻚都要被扫描⼀遍,然后每个数据⻚中也是遍历链表,整体 的效果就是以O(n)的时间复杂度在遍历链表了,这样查询的性能肯定是不⾏的。

(2)优化数据⻚内查询效率-槽位

我们先把⽬光转移到单个数据⻚内的数据查询,假如说我们现在已经锁定数据就在某个数据⻚中了,但是我们该怎样快速的从这个数据⻚中找到我们想要的那⾏数据呢?

通过之前的分析我们可以知道,最傻的⼀种⽅式就是遍历数据⻚中的单向链表查询,⼀个节点⼀个节点去扫描,相对应的查询效率是⾁眼可⻅的低。但是如果说可以像翻书⼀样,根据⽬录来减⼩我们查 询的范围,相对应的查询效率不就上来了吗,根据这种想法,InnoDB存储引擎设计了槽位这种⽅式来组织数据⻚中的多个数据⾏,槽位信息存放在数据⻚中的数据⻚⽬录中。

槽位简单来说就是将数据⻚中的多个数据⾏分组划分,每个数据⾏组都找这个组中的主键值最⼤的那个数据⾏的地址作为槽位的信息,这样数据⻚⽬录中的⼀个个槽位不就是像是⼀个个⽬录了吗,标记好了多个数据⾏分组的位置信息,如下图所示:

这下有了数据⻚⽬录中的槽位信息,此时要查询数据⻚中的某⾏数据不就很简单了,⽐如我们要查询主键为4的那⾏数据,直接通过⼆分法以O(logn)的时间复杂度锁定数据⻚⽬录中的槽位2,因为槽位之间都是紧密连接的,可以通过槽位2找到槽位1,从槽位1末尾开始,对分组2中的数据开始遍历,因为每个分组中的数据量都很少,此时在这么⼩的范围内简单遍历下就可以快速找到主键为4的那⾏数据,时间复杂度从之前的O(n)降低到O(logn)效率还是挺可观的。

但是如果你不是通过主键去查询的,槽位此时就派不上⽤场,你还得⼀个⼀个遍历数据⻚中的单向链表去找到你想要的那⾏数据。

1.2 索引的前夕-⻚分裂

这⾥我们先来个⼩插曲,简单了解下⻚分裂,这块内容也是后⾯索引机制能够正常运⾏的基础。

我们都知道⼀个数据⻚就是16KB⼤⼩,当⼀个数据⻚中的数据⾏⾜够多时就会重新创建⼀个数据⻚继续写数据⾏,如果说我们没有⽤到索引还好,但是如果我们要在表中创建索引,那么对多个数据⻚中的数据就有约束了。 如果新创建的数据⻚中的数据⾏的主键值,存在它上⼀个数据⻚的主键值还⼩的情况,这种情况是不被允许的,如下图所示:

如果出现上图的情况,多个数据⻚之间的主键就⽆序了,⽽索引机制的实现是要基于多个数据⻚主键的⼤⼩是依次递增的,所以此时就会出现⻚分裂的情况。

其实⻚分裂⽬的也很明确,就是调整下不同数据⻚的数据顺序,使得最终按顺序创建的索⻚之间,后⼀个数据⻚中的每⼀个数据⾏的主键值都要⼤于上⼀个数据⻚,当然⼀个数据⻚当然是按照单向链表的⽅式依次递增的,⻚分裂流程如下图所示:

我们可以看到⻚分裂主要就是调整了下数据⻚之间数据⾏的数据的顺序,使得多个数据⻚之间的主键值是按照顺序来存放的,在这样有序的数据中,⾼效查询才变得可能。

频繁的出现⻚分裂情况,毕竟⻚分裂要涉及到数据的移动,在性能上也是会有损耗的,这也警示我们减少⻚分裂的出现概率是⾮常有必要的,在设计表结构时我们可以尽量使⽤主键⾃增⻓的⽅式,⽽不是⽤很难保证主键顺序的⾃定义创建主键的⽅式,使⽤主键⾃增⻓⽅式,能⼤⼤避免说数据⻚之间主键⼤⼩出现顺序错乱的问题,减少⻚分裂发⽣的概率。

1.3 从主键⽬录到索引⻚

查询⼀⾏数据,在物理层⾯就是定位到哪⼀个数据⻚中的哪⼀⾏数据。在数据⻚中定位数据的问题,在之前我们已经通过槽位的⽅式、优化了在单个数据⻚中查询的效率,现在我们要解决的是如何在⼤量的数据⻚中定位数据⻚的效率问题,这就是索引的⽬标。

(1)主键⽬录

InnoDB存储引擎⼀开始是使⽤主键⽬录的⽅式,将数据⻚号和数据⻚最⼩的主键值作为⼀

条记录,如下图所示:

这样的话,我们要查哪⼀条数据就不⽤扫描⼀个数据⻚内的所有数据再扫描下⼀个了,直接通过id去主键⽬录看⼀下,通过⼆分查找定位到具体哪个数据⻚,然后数据⻚内部通过定位槽位,遍历那个槽位对应数据⾏分组找到具体的⼀⾏数据。

(2)索引⻚

现在有⼀个问题就是,每张表对应的数据⻚都有很多,主键⽬录就会有⼤量的数据、就有可能放不下,这时InnoDB设计者们就想存放⽬录数据也是数据啊,为什么不可以使⽤数据⻚来呢,就这样主键⽬录的信息就被移到数据⻚来了,⽽这些数据⻚就被称为索引⻚,如下图所示:

从这⾥我们可以知道数据⻚肯定不是简单只存放数据表中的数据的。好了,现在主键⽬录由于容量有限,我们把主键⽬录信息移动到了数据⻚中形成了索引⻚,但同样的问题不还是会出现吗,⼀个数据⻚的⼤⼩也才16KB,索引⻚本身的容量也是有限的,容量不够了该怎么办呢?

为了解决索引⻚容量不够的问题,索引⻚会重新创建和升级,先把超出容量的数据放到⼀个新的索引⻚中,然后再加⼀层索引⻚,如下图所示:

由上图我们可以看到,新的⼀层索引⻚35它存放的就不是最⼩主键对应的数据⻚⽬录了,⽽是最⼩主键对应的索引⻚⽬录了,以此类推如果索引⻚35这⾥容量也不够呢,那就继续往上⼀层扩展啊,最终效果看起来就像下⾯⼀样:

⼤家看出来了吗,由索引⻚⼀层⼀层组成的结构不就是我们经常说的索引树吗,⽽这棵树在mysql中称之为B+索引树。

树这种数据结构天然可以使⽤⼆分法查询,所以现在如果我们要查询⼀条数据,从树的根节点开始通过⼆分法查找,以O(logn)的时间复杂度锁定数据⻚,然后在数据⻚中同样使⽤⼆分法锁定槽位,在槽位中简单遍历下不就找到数据了吗,相⽐于没有索引的场景,速度那可是相当快了。

2.聚簇索引、普通索引和覆盖索引

关于索引有⼀些常⻅的名词我们需要加以区分。

⾸先聚簇索引就是像我们上⾯看到的⼀棵树⼀样,它的叶⼦节点是⼀个个数据⻚,这些数据⻚中存放的都是数据表中每⼀⾏的完整数据,所以说如果B+树是以完整数据的数据⻚为叶⼦节点的,我们把这个索引树称为聚簇索引;如果⼀个索引的索引树,叶⼦节点不是以数据⻚为叶⼦节点的,就称为⼆级索引或普通索引。

聚簇索引和普通索引最⼤的区别就是,聚簇索引的叶⼦节点存放了数据⾏的完整数据,⽽⼆级索引叶⼦节点只有数据的部分字段。

⽽覆盖索引本身并不是⼀种索引,⽽是⼀种查询数据的⽅式,⽐如我们对表table中的字段name建⽴了索引,然后我们执⾏查询如:select name from table where name like '张%',此时直接从name字段对应的B+树种查询到对应的⼀批name值,然后直接就返回就⾏了,也就是说我们想要的字段name它本来就在索引上,我们直接通过⼆分法⾼效的从树上直接摘下来就⾏了,⽽这种查询⽅式就称为覆盖索引。

当然相⽐于覆盖索引⽅式,如果查询改为:select * from table where name like '张%',这就不是覆盖索引了,因为此时你不光要从索引树上找到具体的name,还要利⽤id值回表查询所有的字段。

3.索引的优缺点分析

索引的优点当然就是⾼效查询数据,索引将遍历链表的O(n)的查询时间复杂度优化为了O(logn)的时间复杂度。

但是索引的缺点也是很明显的,⾸先在时间⻆度上,它必须要求主键是要按顺序增⻓的,⽆序的主键会带来频繁的⻚分裂,影响效率;对数据库表的增删改操作的同时也需要维护索引,这部分的维护也是⼀块性能损耗点;在空间⻆度上:索引相关的数据和实际数据⼀样都是要占内存空间的。

所以索引虽然能够提⾼查询效率,但是同时也要承担它给我们的系统带来的性能损耗,从这点上来看索引并不是建的越多越好。

4.三个维度设计好索引

下⾯我们从以下三个维度优化下索引的设计

(1)⾸先我们从时间⻆度上

我们需要为了避免频繁的⻚分裂,需要尽可能使⽤主键⾃增⻓等⽅式,保证新增的数据⻚中的数据⾏的主键都是递增,避免不必要的⻚分裂带来的性能损耗和拖慢查询效率。

另外选择合适的字段作为索引字段也很重要,需要选择基数较⼤的字段,也就是⼀个字段可能出现的值⽐较多,这样我们在B+树中查询时,才能最⾼效的发挥出⼆分法查询的威⼒,如果建⽴索引的字段基数⽐较⼩可能查询时⼆分查找就会退化成时间复杂度为O(n)的线性查询了。

(2)空间的⻆度上

因为索引数据本身也是要占空间的,可以选择字段⻓度较⼩的作为索引字段,这样整棵B+树不⾄于那么占空间。

但是如果⾮得要以⻓字段作为索引也不是不⾏,可以采⽤折中的以字段的前缀作为索引,这样的索引也称为前缀索引,但是这样可能只能⽤在模糊查询上了,⽤在group by和order by上就不太适合了。

(3)作⽤范围上

当然我们设计索引的⽬的,当然是为了更好的⽤上索引,索引在设计时,尽可能让where、group by、order by这些语句都能⽤上索引。