B树和B+树

1 B树

在介绍B+树之前, 先简单的介绍一下B树,这两种数据结构既有相似之处,也有他们的区别,最后,我们也会对比一下这两种数据结构的区别。

1.1 B树概念

B树也称B-树,它是一颗多路平衡查找树。二叉树我想大家都不陌生,其实,B树和后面讲到的B+树也是从最简单的二叉树变换而来的,并没有什么神秘的地方,下面我们来看看B树的定义。

  • 每个节点最多有m-1个关键字(可以存有的键值对)。

  • 根节点最少可以只有1个关键字

  • 非根节点至少有m/2个关键字

  • 每个节点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。

  • 所有叶子节点都位于同一层,或者说根节点到每个叶子节点的长度都相同。

  • 每个节点都存有索引和数据,也就是对应的key和value。

所以,根节点的关键字数量范围:1 <= k <= m-1,非根节点的关键字数量范围:m/2 <= k <= m-1

另外,我们需要注意一个概念,描述一颗B树时需要指定它的阶数,阶数表示了一个节点最多有多少个孩子节点,一般用字母m表示阶数。

我们再举个例子来说明一下上面的概念,比如这里有一个5阶的B树,根节点数量范围:1 <= k <= 4,非根节点数量范围:2 <= k <= 4。

下面,我们通过一个插入的例子,讲解一下B树的插入过程,接着,再讲解一下删除关键字的过程。

1.2 B树插入

插入的时候,我们需要记住一个规则:判断当前结点key的个数是否小于等于m-1,如果满足,直接插入即可,如果不满足,将节点的中间的key将这个节点分为左右两部分,中间的节点放到父节点中即可。

例子:在5阶B树中,结点最多有4个key,最少有2个key(注意:下面的节点统一用一个节点表示key和value)。

  • 插入18,70,50,40

  • 插入22

插入22时,发现这个节点的关键字已经大于4了,所以需要进行分裂,分裂的规则在上面已经讲了,分裂之后,如下。

  • 接着插入23,25,39

分裂,得到下面的。

更过的插入的过程就不多介绍了,相信有这个例子你已经知道怎么进行插入操作了。

1.3 B树的删除操作

B树的删除操作相对于插入操作是相对复杂一些的,但是,你知道记住几种情况,一样可以很轻松的掌握的。

  • 现在有一个初始状态是下面这样的B树,然后进行删除操作。

  • 删除15,这种情况是删除叶子节点的元素,如果删除之后,节点数还是大于m/2,这种情况只要直接删除即可。

  • 接着,我们把22删除,这种情况的规则:22是非叶子节点,对于非叶子节点的删除,我们需要用后继key(元素)覆盖要删除的key,然后在后继key所在的子支中删除该后继key。对于删除22,需要将后继元素24移到被删除的22所在的节点。

此时发现26所在的节点只有一个元素,小于2个(m/2),这个节点不符合要求,这时候的规则(向兄弟节点借元素):如果删除叶子节点,如果删除元素后元素个数少于(m/2),并且它的兄弟节点的元素大于(m/2),也就是说兄弟节点的元素比最少值m/2还多,将先将父节点的元素移到该节点,然后将兄弟节点的元素再移动到父节点。这样就满足要求了。

我们看看操作过程就更加明白了。

  • 接着删除28,删除叶子节点,删除后不满足要求,所以,我们需要考虑向兄弟节点借元素,但是,兄弟节点也没有多的节点(2个),借不了,怎么办呢?如果遇到这种情况,首先,还是将先将父节点的元素移到该节点,然后,将当前节点及它的兄弟节点中的key合并,形成一个新的节点

移动之后,跟兄弟节点合并。

删除就只有上面的几种情况,根据不同的情况进行删除即可。

上面的这些介绍,相信对于B树已经有一定的了解了,接下来的一部分,我们接着讲解B+树,我相信加上B+树的对比,就更加清晰明了了。

2 B+树

2.1 B+树概述

B+树其实和B树是非常相似的,我们首先看看相同点

  • 根节点至少一个元素
  • 非根节点元素范围:m/2 <= k <= m-1

不同点

  • B+树有两种类型的节点:内部结点(也称索引结点)和叶子结点。内部节点就是非叶子节点,内部节点不存储数据,只存储索引,数据都存储在叶子节点。
  • 内部结点中的key都按照从小到大的顺序排列,对于内部结点中的一个key,左树中的所有key都小于它,右子树中的key都大于等于它。叶子结点中的记录也按照key的大小排列。
  • 每个叶子结点都存有相邻叶子结点的指针,叶子结点本身依关键字的大小自小而大顺序链接。
  • 父节点存有右孩子的第一个元素的索引。

下面我们看一个B+树的例子,感受感受它吧!

2.2 插入操作

对于插入操作很简单,只需要记住一个技巧即可:当节点元素数量大于m-1的时候,按中间元素分裂成左右两部分,中间元素分裂到父节点当做索引存储,但是,本身中间元素还是分裂右边这一部分的

下面以一颗5阶B+树的插入过程为例,5阶B+树的节点最少2个元素,最多4个元素。

  • 插入5,10,15,20

  • 插入25,此时元素数量大于4个了,分裂

  • 接着插入26,30,继续分裂

有了这几个例子,相信插入操作没什么问题了,下面接着看看删除操作。

2.3 删除操作

对于删除操作是比B树简单一些的,因为叶子节点有指针的存在,向兄弟节点借元素时,不需要通过父节点了,而是可以直接通过兄弟节移动即可(前提是兄弟节点的元素大于m/2),然后更新父节点的索引;如果兄弟节点的元素不大于m/2(兄弟节点也没有多余的元素),则将当前节点和兄弟节点合并,并且删除父节点中的key,下面我们看看具体的实例。

  • 初始状态

  • 删除10,删除后,不满足要求,发现左边兄弟节点有多余的元素,所以去借元素,最后,修改父节点索引

  • 删除元素5,发现不满足要求,并且发现左右兄弟节点都没有多余的元素,所以,可以选择和兄弟节点合并,最后修改父节点索引

  • 发现父节点索引也不满足条件,所以,需要做跟上面一步一样的操作

这样,B+树的删除操作也就完成了,是不是看完之后,觉得非常简单!

3 B树和B+树总结

B+树相对于B树有一些自己的优势,可以归结为下面几点。

  • 单一节点存储的元素更多,使得查询的IO次数更少,所以也就使得它更适合做为数据库MySQL的底层数据结构了。
  • 所有的查询都要查找到叶子节点,查询性能是稳定的,而B树,每个节点都可以查找到数据,所以不稳定。
  • 所有的叶子节点形成了一个有序链表,更加便于查找。

 

                                                              深入浅出分析MySQL索引设计背后的数据结构

 

MySQL索引实现是在存储引擎端,不同存储引擎对索引实现方式是不同的,比如Innodb和MyISAM,下面我们重点介绍Innodb引擎索引的实现方式。

1、Innodb索引实现方式:

对于InnoDB表,数据文件ibd本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。

举例说明,下面是students表,id是主键,name上有辅助索引,有6行数据记录。

深入浅出分析MySQL索引设计背后的数据结构

 

假如在一棵5阶B+Tree(关键字范围[2,4]),它的主键索引组织结构如下:

深入浅出分析MySQL索引设计背后的数据结构

 

上图是InnoDB主键索引的B+tree,叶节点包含了完整的数据记录,像这种索引叫做聚集索引。因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL会优先自动选择一个可以唯一标识数据记录的列作为主键,比如唯一索引列,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,长度为6个字节,类型为longint。

辅助索引结构:

深入浅出分析MySQL索引设计背后的数据结构

 

对于secondary index,非叶子结点保存的是索引值,比如上面的name字段。叶子结点保存的不再是数据记录了,而是主键值。

从上面的B+Tree可以总结到:

MySQL聚集索引使得按主键的搜索非常高效的。 辅助索引需要搜索两遍索引: 第一:检索辅助索引获得主键值 第二:用主键值到主键索引中检索获得记录

 

2 Mysql的存储引擎和索引

  可以说数据库必须有索引,没有索引则检索过程变成了顺序查找,O(n)的时间复杂度几乎是不能忍受的。我们非常容易想象出一个只有单关键字组成的表如何使用B+树进行索引,只要将关键字存储到树的节点即可。当数据库一条记录里包含多个字段时,一棵B+树就只能存储主键,如果检索的是非主键字段,则主键索引失去作用,又变成顺序查找了。这时应该在第二个要检索的列上建立第二套索引。  这个索引由独立的B+树来组织。有两种常见的方法可以解决多个B+树访问同一套表数据的问题,一种叫做聚簇索引(clustered index ),一种叫做非聚簇索引(secondary index)。这两个名字虽然都叫做索引,但这并不是一种单独的索引类型,而是一种数据存储方式。对于聚簇索引存储来说,行数据和主键B+树存储在一起,辅助键B+树只存储辅助键和主键,主键和非主键B+树几乎是两种类型的树。对于非聚簇索引存储来说,主键B+树在叶子节点存储指向真正数据行的指针,而非主键。

  InnoDB使用的是聚簇索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上,若使用"where id = 14"这样的条件查找主键,则按照B+树的检索算法即可查找到对应的叶节点,之后获得行数据。若对Name列进行条件搜索,则需要两个步骤:第一步在辅助索引B+树中检索Name,到达其叶子节点获取对应的主键。第二步使用主键在主索引B+树种再执行一次B+树检索操作,最终到达叶子节点即可获取整行数据。

  MyISM使用的是非聚簇索引,非聚簇索引的两棵B+树看上去没什么不同,节点的结构完全一致只是存储的内容不同而已,主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键。表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别。由于索引树是独立的,通过辅助键检索无需访问主键的索引树。

  为了更形象说明这两种索引的区别,我们假想一个表如下图存储了4行数据。其中Id作为主索引,Name作为辅助索引。图示清晰的显示了聚簇索引和非聚簇索引的差异。

 

  我们重点关注聚簇索引,看上去聚簇索引的效率明显要低于非聚簇索引,因为每次使用辅助索引检索都要经过两次B+树查找,这不是多此一举吗?聚簇索引的优势在哪?

  1 由于行数据和叶子节点存储在一起,这样主键和行数据是一起被载入内存的,找到叶子节点就可以立刻将行数据返回了,如果按照主键Id来组织数据,获得数据更快。

  2 辅助索引使用主键作为"指针" 而不是使用地址值作为指针的好处是,减少了当出现行移动或者数据页分裂时辅助索引的维护工作,使用主键值当作指针会让辅助索引占用更多的空间,换来的好处是InnoDB在移动行时无须更新辅助索引中的这个"指针"。也就是说行的位置(实现中通过16K的Page来定位,后面会涉及)会随着数据库里数据的修改而发生变化(前面的B+树节点分裂以及Page的分裂),使用聚簇索引就可以保证不管这个主键B+树的节点如何变化,辅助索引树都不受影响。

到这里,再来分析本文开头提出的问题:

问题1、为什么Innodb表需要主键? 1)innodb表数据文件都是基于主键索引组织的,没有主键,mysql会想办法给我搞定,所以主键必须要有;

2)基于主键查询效率高;

3)其他类型索引都要引用主键索引; 问题3、为什么不建议Innodb表主键设置过长?

因为辅助索引都保存引用主键索引,过长的主键索引使辅助索引变得过大;

 

 

 

深入浅出分析MySQL索引设计背后的数据结构

 

在上面的例子中:将下面数字插入到一棵5阶B-Tree中:[3,14,7,1,8,5,11,17,13,6,23,12,20,26,4,16,18,24,25,19]

插入这些无序数据一共经历了6次分裂,对于磁盘索引文件而言,每次分裂都是很昂贵的操作;

如果将以上数据排好序,再次插入是不是效果会好,我试验了下,虽然每次都是插入到最右结点,涉及迁移数据量会少,但是分裂的次数依然挺多,需要7次分裂。

每次分裂都是按照50%进行,这样存在明显的缺点就是导致索引页面的空间利用率在50%左右;而且对于递增插入效率也不好,平均每两次插入,最右结点就得进行一次分裂。那Innodb是如何进行改进的呢?

Innodb其实只是针对递增/递减情况进行了改进优化,不再采用50%的分裂策略,而是使用下面的分裂策略:

对于递增/递减索引插入操作:

1、插入新元素,判断叶子结点空间是否足够,如果足够,直接插入

2、如果叶子结点空间满了,判断父结点空间是否足够,如果足够,将该新元素插入到父结点中;如果父结点空间满了,则进行分裂。

比如下面一棵5阶B+Tree:

深入浅出分析MySQL索引设计背后的数据结构

 

现在连续插入10,11,14,15,17,采用优化后分裂策略的分步图例如下:

【第一步】:插入10

深入浅出分析MySQL索引设计背后的数据结构

 

由于最右结点还有空间,直接插入即可。

【第二步】:插入11

深入浅出分析MySQL索引设计背后的数据结构

 

插入11时,由于最右结点空间已满,如果使用50%分裂策略,则需要分裂操作了,但是使用优化后的分裂策略,当该结点空间已满,还要判断该结点的父结点是否满了,如果父结点还有空间,那么插入到父结点中,所以11插入到父结点中了,同时形成一个子结点。

【第二步】:插入14,15,17

深入浅出分析MySQL索引设计背后的数据结构

 

优化后的分裂策略仅仅针对递增/递减情况,显著的减少了分裂次数并且大大提高了索引页面空间的利用率。

如果是随机插入,可能会引起更高代价的分裂概率。所以InnoDB存储引擎会为每个索引页维护一个上次插入的位置变量,以及上次插入是递增/递减的标识。InnoDB能够根据这些信息判断新插入数据是否满足递增/递减条件,若满足,则采用改进后的分裂策略;若不满足,则进行50%的分裂策略。

到此,我们可以回答本文开头提出的另一个问题了:

问题2:为什么建议InnoDB表主键是单调递增?

如果InnoDB表主键是单调递增的,可以使用改进后的B+tree分裂策略,显著减少B-Tree分裂次数和数据迁移,从而提高数据插入效率。

不仅如此,它还大大提高索引页空间利用率。

 

详细解释:https://www.cnblogs.com/williamjie/p/11081081.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值