我们都是知道MySQL的索引存储使用的是B+树的结构,想要搞懂MySQL的索引,我们需要先搞清楚B+树,而B+树是B树演变而来的,所以我们又必须先搞懂B树。
我们通常见到的二叉树、红黑树这些是二叉的,就是一个节点可以有两个子节点,而B树是多叉的,就是一个节点能有多个子节点。
B树
- 定义:
(源自百度百科)
B树是一种平衡的多叉树,一棵m阶B树(balanced tree of order m)是一棵平衡的m路搜索树。它或者是空树,或者是满足下列性质的树:
1、根结点至少有两个子女;
2、每个非根节点所包含的关键字个数 j 满足:┌m/2┐ - 1 <= j <= m - 1;
3、除根结点以外的所有结点(不包括叶子结点)的度数正好是关键字总数加1,故内部子树个数 k 满足:┌m/2┐ <= k <= m ;
4、所有的叶子结点都位于同一层。
注:┌m/2┐是向上取整的意思
在B树中有个阶的概念,或者叫度,也就是一个节点能存的数据个数,但是一个节点最多存m-1个数据,当达到m个时,需要将节点拆分,中间的节点上提,当前节点分成两个子节点。
对于B树插入过程不太清楚的,可以看一个旧金山大学的一个数据结构演示动画网站。
https://www.cs.usfca.edu/~galles/visualization/Algorithms.html (里面有B树、B+树、红黑树等等很多数据结构的演示动画)
以一个5阶的为例,我之前已经依次插入了1~16的数据,在插入17之前如下
- B树查询
按照B树的定义,一个5阶的B树,一个节点最多可以有4个数据,此时根节点就是有四个数据,而这四个数据其实可以分为5个段,分别为:x<3、3<x<6、6<x<9、9<x<12、12<x 。此时也正好表明5阶树的节点可以有5个子节点。
此时假如我们需要查找一个数据8,首先在根节点中比较,发现没有8,而且6<8<9,所以8应该在6 9 之间指向的节点,所以就到7 8 这个节点中查询,如果找到那正好,如果找不到,例如8.5,那这个树上肯定是不存在的,因为按照B树的限制要求,它只有可能在这个节点,不可能在其他节点。
- B树新增
现在我们在此基础上再插入17,看一下树的变化。
插入17后,最后一个节点就变成了5个数据,此时应该把中间节点15抽出来放到父节点中,同时此节点拆分成两个,分别为13 14和16 17,而父节点12和15之间的指针执行13 14节点,15后的指针执行16 17 节点。
这是插入本该结束的,但是父节点原本4个数据,现在从子节点中抽上来了1个数据,导致父节点有5个数据了,此时就应该继续拆分父节点。中间节点9抽到父节点中,6和9之间的指针跟着3 6节点,具体指向不变,9和12之间的指针跟着12 15节点,具体指向不变。9最终成为根节点。
- B树删除
此时如果我们删除任何一个叶子节点上的数据,都会导致不满足B树特点的第二条。第二条规定5阶的B树非根节点最少有2个数据(ceil(5/2)-1 = 2)。此时就会产生节点合并。以删除13为例。
删除13后,14就需要向前一个兄弟节点合并,但是14是大于12的,要想合并过去,12必须下来,12下来后节点就变成10 11 12 14,15之前的指针就指向了新节点,但是15这个节点又不满足要求了,此时15的父节点9就需要下来,9下来后无根节点,所以根节点的两个子节点就需要合并成新的根节点,合并后正好也满足要求。至此删除结束。
B+树
- 定义
(源自百度百科)
B+树是B树的一种变形形式,B+树上的叶子结点存储关键字以及相应记录的地址,叶子结点以上各层作为索引使用。一棵m阶的B+树定义如下:
(1)每个结点至多有m个子女;
(2)除根结点外,每个结点至少有[m/2]个子女,根结点至少有两个子女;
(3)有k个子女的结点必有k个关键字。
B+树的查找与B树不同,当索引部分某个结点的关键字与所查的关键字相等时,并不停止查找,应继续沿着这个关键字左边的指针向下,一直查到该关键字所在的叶子结点为止。
我们同样以5阶为例,同样依次插入1~16个数据,B+树的存储形式如下
比较B树和B+树的存储,我们不难发现以下不同
1、B+树插入的数据都在叶子节点,非叶子节点是叶子节点的冗余。
2、B+树叶子节点间有指针指向兄弟节点。
其实还有一个比较明显的特征,B+树的叶子节点从左至右是有序的,当然B树也是这样,只不过B树不是全部数据。
以下子两种情况比较好的体现出B+树的优势
1、取出树中的全部数据
2、取出一段数据,例如大于5小于10的数据
B+树只需要找到最左侧节点,依次向后取即可,而B树是需要向上会查父节点的。Mysql使用B+树应该是有这方面的原因。
- B+树查询
B+树的查询根B树类似,只不过B树找到即结束,而B+树只有在叶子节点找到才结束。当在非叶子节点找到时,是向左还是向右需要根据插入时拆分的原则确定,并没有找到明确的规定,必须拆到左边或者右边。百度百科上说是左边,但是大部分文章都是向右边,并且旧金山大学的动画演示中实现也是向右。
- B+树插入
在上图1~16的基础上,我们再插入17,此时最后一个节点存数据为5个,需要拆分,中间值15上移到父节点,以15拆分,13 14组成新的节点,15 16 17组成新的节点。
- B+树删除
B+树的删除与B树的删除类似,也是容易出现违法第二条的情况,此时就需要做一些合并。以不同的情况分别说明,都是基于上图分析的。
情况一
假如我们删除16或者17,这时应该是最简单的一种情况,因为删除后依然满足B树的特征,并且在非叶子节点也不存在,无需调整非叶子节点。
情况二
假如我们删除15,删除后最后一个节点依然满足B+树特征要求,但是在非叶子节点也存在15,所以我们需要将非叶子节点中的15也删除掉,删除后父节点中就缺少了一个指向最后一个节点的数据,所以将16提到父节点中
此处,我个人还是有个异议,假如我们将叶子节点和非叶子节点中的15都删除掉,然后将16 17合并到左兄弟节点13 14中,也是满足B+树的特征的。这样实现的好处就是节省空间,但坏处就是节点合并伴随着数据移动。
情况三
假如我们删除5,5 6节点就剩余6,需要向左兄弟节点合并,父节点中的5也需要删除,删除后就剩余3,它无左兄弟节点,需要向父类借数据,7下来后,无根节点,需要将右子树的最小数据上提为根节点。
情况四
假如我们删除7,叶子节点7 8删除后只剩8,此节点是根节点的右子树,不能向左合并,需要向右合并。根节点7也需要删除,删除后根节点只能是右子树的最小值,否则就违背了树的基本原则,所以此时根只能是8
B+树的应用
上面的图都是理论上的实现,在实际的数据结构中,大概如下
MySQL索引存储选择B+树
1、查询效率稳定,数据只在叶子节点,每次查询都必须到叶子节点才算结束。
2、范围查询或者全表扫描方便,叶子节点上的数据是有序的,并且有前后指针
3、因为非叶子节点是不存储数据的,占用空间很小,操作系统从磁盘上读取数据时是按页读的,一次将一页加载到内存,这样如果一次将一页的非叶子节点数据加载到内存,这样获取到的指针要比带数据时获取到的指针多的多。所以读到数据时发生的IO次数要少。
假如key占8byte,p占8byte,一行数据data占100byte,而操作系统一页是1kb的话,采用B树结构时一页读1024/(8+8+100)约等于8,而采用B+树时1024/(8+8)=64。这样就相当于采用B树时我们只能用8阶的,而B+树确是64阶,从而存储同样的数据B+树的高要远小于B树,也就是IO次数要远小于B树。虽然B树找到key相等的就可以停止查找(最快O(1)),而B+树总是要找到叶子节点,但从整体情况来看B+树性能还是要比B树好。
很多文章都说MongoDB采用的是B树,理由就是很少进行范围查找,针对单条查询的时候,找到即停止,即所需数据就在当前查询到的json里。根据上面的对比,这个理由并不成立,数据量大时,B+树肯定是比B树找的快,MongoDB应该要选用B+树的。有人说在MongoDB的专场问过引擎作者,作者说是B+树实现的,不知是否可信。