B-树读作B树, 中间的’-'不是减号.
MySQL数据库的索引的数据结构主要是Hash表或B+树.
先引入问题, 数据库索引为什么使用树结构存储?
因为树的查询效率高, 而且可以保持有序. 二叉树的时间复杂度是O(logN), 查找和比较次数都是最小的, 但是并没有使用二叉树作为索引的数据结构.
没有使用二叉树的原因是因为磁盘IO, 数据库引擎是存储在磁盘上的, 当数据量比较大的时候, 索引的大小可能有几个G甚至更多. 当我们利用索引查询的时候, 不可能将整个索引全部加载到内存中去, 只能逐一加载每一个磁盘页, 这里的磁盘页对应的索引树的节点.
如果使用二叉树作为索引结构, 那么磁盘IO的数次就会非常频繁, 如果二叉树的高很大, 最坏情况下, 磁盘IO次数就是索引树的高度.
所以, 为了减少磁盘IO次数, 就需要把原本"瘦高"的树结构变得"矮胖". 这就是B-树的特征之一.
B树是一种多路平衡查找树, 它的每一个节点最多包含k个孩子, k被称为B树的阶. k的大小取决于磁盘页的大小.
B-树
下面来具体介绍一下B-树(Balance Tree),一个m阶的B树具有如下几个特征:
- 根节点至少有两个子女.
- 每个中间节点都包含k-1个元素和k个孩子, 其中m/2 <= k <= m
- 每一个叶子节点都包含k-1个元素, 其中m/2 <= k <= m
- 所有的叶子节点都位于同一层
- 每个节点中的元素从小到大排列, 节点当中k-1个元素正好是k个孩子包含的元素的值域划分.
上图是一个三阶B-树
重点分析(2, 6)节点, 该节点有两个元素2和6, 又有三个孩子(1), (3, 5), (8). 其中1小于元素2, 元素3和5正好在元素2和6之间, 8大于元素2, 6, 正好符合刚才所列的特征.
假定要查找5, 第一次磁盘IO在9, 第二次磁盘IO读到(2, 6), 第三次磁盘IO读到(3, 5),找到5. 整个流程下来, B-树在查询中的比较次数其实不比二叉查找数少, 尤其当单一节点中的元素数量很多时.
可是相比磁盘IO的速度, 内存中的比较耗时几乎可以忽略. 所有只要树的高度足够低, IO次数足够少, 就可以提升查找性能.
向相比之下节点内容元素多一些也没有关系, 仅仅是多了几次内存交互, 只要不超过磁盘页的大小即可. 这就是B-树的优势之一.
B-树的插入和删除
B-树插入新节点的过程比较复杂, 而且分成很多种情况. 这里举例最典型的例子, 加入我们要插入的值4.
自顶向下查找4的节点位置, 发现4应当插入到节点元素3和5之间.
节点3, 5已经是两元素节点, 无法再增加. 父亲节点2, 6也是两元素节点, 也无法再增加. 根节点9是单元素节点, 可以升级为两元素节点. 于是拆分节点3, 5与节点2, 6, 让根节点9升级为两元素节点4, 9. 节点6独立为根节点的第二个孩子.
插入一个元素后, 让整个B树的这么多子节点都发生了连锁改变. 看起来会很麻烦, 但也正因为如此, 让B-树能够始终维持多路平衡. 这也是B-树的一大优势: 自平衡.
现在来删除元素11:
自顶向下查找元素11的节点位置.
删除节点11后, 节点12只有一个孩子, 不符合B树规范. 因此找出12, 13, 15三个节点的中位数13, 取代节点12, 而节点12自身下移成为第一个孩子(这个过程成为左旋)
B-树的应用
B-树主要应用于文件系统以及部分数据库引擎索引, 比如非关系型数据库MongoDB.
而大部分关系型数据库, 比如MySQL, 则使用B+树作为索引.
B+树
B+树是基于B-树的一种变体, 有着比B-树更高的查询性能.
B+树和B-树也有一些共同点, 但是B+树也具备一些新的特征.
一个m阶的B+树具有如下几个特征:
- 有k个子树的中间节点包含有k个元素(B树中是k-1个元素), 每个元素不保存数据, 只用来索引, 所有数据都保存在叶子节点.
- 所有的叶子节点中包含了全部元素的信息, 及指向含这些元素记录的指针, 且叶子节点本身依关键字的大小自小而大顺序链接.
- 所有的中间节点元素都同时存在于子节点, 在子节点元素中是最大(或最小)元素.
举个例子
B+树的几个特点. 首先, 每一个父节点的元素都出现在子节点中, 是子节点的最大(或最小)元素.
在上面这棵树中, 根节点元素8是子节点2, 5, 8的最大元素, 也是叶子节点6, 8的最大元素.
根节点元素15是子节点11, 15的最大元素, 也是叶子节点13, 15的最大元素.
至于叶子节点, 由于父节点的元素都出现在子节点, 因此所有叶子节点包含了全量元素信息.
而且每一个叶子节点都带有指向下一个节点的指针, 形成了一个有序链表.
B+树还具有一个特点, 这个特点是在索引之外, 确实至关重要的特点. 那就是"卫星数据"的位置.
B-树中的卫星数据(Satellite Information):
而在B+树当中, 只有叶子节点带有卫星数据, 其余中间节点仅仅是索引, 没有任何数据关联.
B+树中的卫星数据(Satellite Information):
需要补充的是, 在数据库饿聚集索引(Clustered Index)中, 叶子节点直接包含卫星数据. 在非聚集索引(NonClustered Index)中, 叶子节点带有指向卫星数据的指针.
B+树这样设计的好处, 好处主要体现在查询性能上. 下面我们分别来通过单行查询和范围查询来做分析.
在单元素查询的时候, B+树会自顶向下逐层查找节点, 最终找到匹配的叶子节点. 比如要查找元素3;
第一次磁盘IO 根节点;
第二次磁盘IO (2, 5, 8)节点
第三次磁盘IO (3, 5)节点
整个流程和B-树差不多, 但有两点不同. 首先, B+树的中间节点没有卫星数据, 所以同样大小的磁盘页可以容纳更多的节点元素.
这意味着, 数据量相同的情况下, B+树的结构比B-树更加"矮胖", 因此查询时IO次数也更少.
其次, B+树的查询必须最终查找到叶子节点, 而B-树只要找到匹配元素即可, 无论匹配元素处于中间节点还是叶子节点.
因此, B-树的查找性能并不稳定(最好情况是只查根节点, 而最坏情况是查找到叶子节点). 而B+树的每一次查找都是稳定的.
如果是范围查询, B-树只能频繁地使用中序遍历, 查询范围3到11的元素:
B-树需要不断的执行中序遍历.
而B+树只要在链表上做遍历即可.
综合起来, B+树相比B-树的优势有三个: IO次数更少、查询性能稳定、范围查询简便.
B+树的插入和删除,过程与B-树差不多.
总结
B+树的特征:
-
有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
-
所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
-
所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。
B+树的优势:
-
单一节点存储更多的元素,使得查询的IO次数更少。
-
所有查询都要查找到叶子节点,查询性能稳定。
-
所有叶子节点形成有序链表,便于范围查询。