B-树和B+树

思考并回答以下问题:

  • 数据库索引为什么要使用树结构存储呢?
  • 为什么索引没有使用二叉查找树来实现呢?
  • B+树的实现细节是什么样?B-树和B+树有什么区别?
  • 联合索引在B+树中如何存储?
  • 卫星数据,指的是索引元素所指向的数据记录,比如数据库中的某一行。怎么理解?
  • B+树的非叶子节点是没有卫星数据的,非叶子节点只起到索引的作用。是什么意思?

索引

MySQL的索引主要是基于Hash表和B+树。

要弄明白B+树,先要了解什么是B-树。需要注意的是,B-树就是B树,中间的横线并不是减号(不要读成B减树)。

二叉查找树

数据库索引为什么要使用树结构存储呢?

因为树的查询效率高,而且可以保持有序。

既然这样,为什么索引没有使用二叉查找树来实现呢?

其实从算法逻辑上来讲,二叉查找树的查找速度和比较次数都是最小的。但是,我们不得不考虑一个现实问题:磁盘IO。

数据库索引是存储在磁盘上的。当数据量比较大的时候,索引的大小可能有几个G甚至更多。

当我们利用索引查询的时候,能把整个索引全部加载到内存吗?显然不可能。能做的只有逐一加载每一个磁盘页,这里的磁盘页对应着索引树的节点。

如果我们利用二叉查找树作为索引结构,情形是什么样呢?假设树的高度是4,查找的值是10,那么流程如下:

二叉查找树的结构:

第1次磁盘IO:

第2次磁盘IO:

第3次磁盘IO:

第4次磁盘IO:

有没有从中看出,磁盘IO的次数是由什么决定?

磁盘IO的次数是4次,索引树的高度也是4。所以最坏情况下,磁盘IO次数等于索引树的高度。为了减少磁盘IO次数,我们就需要把原本“瘦高”的树结构变得“矮胖”,这就是B-树的特征之一。

二叉查找树的查找效率是最高的,如果内存能装下完整的树,最好使用二叉查找树,B树是退而求其次的方式。

B树

B树索引可以理解成是主要存储于磁盘中的数据结构。

B树是一种多路平衡查找树,它的每一个节点最多包含k个孩子,k被称为B树的阶。k的大小取决于磁盘页的大小。

下面来具体介绍一下B-树(Balance Tree),一个m阶的B树具有如下几个特征:

  • 1.根结点至少有两个子女。
  • 2.每个中间节点都包含k-1个元素和k个孩子,其中m/2 <= k <= m。
  • 3.每一个叶子节点都包含k-1个元素,其中m/2 <= k <= m(B-树叶子节点的元素数量最多可以是阶数-1。比如一颗4阶的B-树,每个节点最多包含有4个孩子、3个元素;最少包含有2个孩子,1个元素)。
  • 4.所有的叶子结点都位于同一层。
  • 5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。

我们以一个3阶B-树为例,来看看B-树的具体结构。树中的具体元素和刚才的二叉查找树是一样的。

这棵树中,重点来看(2,6)节点。该节点有两个元素2和6,又有三个孩子1,(3,5),8。其中1小于元素2,(3,5)在元素2,6之间,8大于(3,5),正好符合刚刚所列的几条特征。

查询

假如要查询的数值是5。

第1次磁盘IO:

在内存中定位(和9比较):

第2次磁盘IO:

在内存中定位(和2,6比较):

第3次磁盘IO:

在内存中定位(和3,5比较):

通过整个流程我们可以看出,B-树在查询中的比较次数其实不比二叉查找树少,尤其当单一节点中的元素数量很多时。

可是相比磁盘IO的速度,内存中的比较耗时几乎可以忽略。所以只要树的高度足够低,IO次数足够少,就可以提升查找性能。

相比之下节点内部元素多一些也没有关系。仅仅是多了几次内存交互,只要不超过磁盘页的大小即可。这就是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-树主要应用于文件系统以及部分数据库索引,比如著名的非关系型数据库MongoDB。

B-树的好处是,虽然查询性能不稳定,但平均的查询速度快一些(不用每次都查找到叶子节点为止)。

而大部分关系型数据库,比如MySQL,则使用B+树作为索引。

B+树

B+树是基于B-树的一种变体,有着比B-树更高的查询性能。

先回顾一下B-树的几大特征。一个m阶的B树具有如下几个特征:

  • 1.根结点至少有两个子女。
  • 2.每个中间节点都包含k-1个元素和k个孩子,其中m/2 <= k <= m。
  • 3.每一个叶子节点都包含k-1个元素,其中m/2 <= k <= m。
  • 4.所有的叶子结点都位于同一层。
  • 5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。

B+树和B-树有一些共同点,但是B+树也具备一些新的特征。

一个m阶的B+树具有如下几个特征:

  • 1.有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
  • 2.所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
  • 3.所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。

具体例子

这是什么怪树?不但节点之间含有重复元素,而且叶子节点还用指针连在一起。

这些正是B+树的特点。首先,每一个父节点的元素都出现在子节点中,是子节点的最大(或最小)元素。

在上面这棵树中,根节点元素8是子节点2,5,8的最大元素,也是叶子节点6,8的最大元素。

根节点元素15是子节点11,15的最大元素,也是叶子节点13,15的最大元素。

需要注意的是,根节点的最大元素(这里是15),也就等同于整个B+树的最大元素。以后无论插入删除多少元素,始终要保持最大元素在根节点当中。

至于叶子节点,由于父节点的元素都出现在子节点,因此所有叶子节点包含了全量元素信息。

并且每一个叶子节点都带有指向下一个节点的指针,形成了一个有序链表。

这是以空间换时间,因为子节点都包含父节点的元素。树的层数和一个节点被重复存储成线性关系。父节点元素在子节点重复出现,增加了少量空间,换来的是范围查询的便利。

B+树还具有一个特点,这个特点是在索引之外,却是至关重要的特点。那就是[卫星数据]的位置。

所谓卫星数据,指的是索引元素所指向的数据记录,比如数据库中的某一行。在B-树中,无论中间节点还是叶子节点都带有卫星数据。

B-树中的卫星数据(Satellite Information):

而在B+树当中,只有叶子节点带有卫星数据,其余中间节点仅仅是索引,没有任何数据关联。

B+树中的卫星数据(Satellite Information):

需要补充的是,在数据库的聚集索引(Clustered Index)中,叶子节点直接包含卫星数据。在非聚集索引(NonClustered Index)中,叶子节点带有指向卫星数据的指针。

B+树设计成这样子究竟有什么好处呢?

B+树的好处主要体现在查询性能上。下面我们分别通过单行查询和范围查询来做分析。

在单元素查询的时候,B+树会自顶向下逐层查找节点,最终找到匹配的叶子节点。比如我们要查找的是元素3。

第一次磁盘IO:

第二次磁盘IO:

第三次磁盘IO:

查询流程看起来跟B-树差不多。但其实有两点不同。首先,B+树的中间节点没有卫星数据,所以同样大小的磁盘页可以容纳更多的节点元素。

这就意味着,数据量相同的情况下,B+树的结构比B-树更加“矮胖”,因此查询时IO次数也更少。

其次,B+树的查询必须最终查找到叶子节点,而B-树只要找到匹配元素即可,无论匹配元素处于中间节点还是叶子节点。

因此,B-树的查找性能并不稳定(最好情况是只查根节点,最坏情况是查到叶子节点)。而B+树的每一次查找都是稳定的。

B+树的查询每次都查到叶子节点,所以IO次数稳定。试想一个数据库的查询,有时候执行10毫秒,有时候执行100毫秒,肯定是不太合适的。还不如每次都执行30毫秒。

B-树的范围查找过程

下面我们再来看看范围查询。B-树如何做范围查询呢?只能依靠繁琐的中序遍历。比如我们要查询范围为3到11的元素:

自顶向下,查找到范围的下限(3):

中序遍历到元素6:

中序遍历到元素8:

中序遍历到元素9:

中序遍历到元素11,遍历结束:

B-树的范围查询确实很繁琐。反观B+树的范围查询,则要简单得多,只需要在链表上做遍历即可:

B+树的范围查找过程

自顶向下,查找到范围的下限(3):

通过链表指针,遍历到元素6,8:

通过链表指针,遍历到元素9,11,遍历结束:

比B-树的中序遍历要简单得多。

综合起来,B+树相比B-树的优势有三个:

  • 1.IO次数更少;
  • 2.查询性能稳定;
  • 3.范围查询简便。

至于B+树的插入和删除,过程与B-树大同小异。

总结

联合索引就是把多个索引值拼接成一个字符串A,字符串A像普通索引一样保存在索引树中。

最后我们来总结一下, B+树的特征和优势:

B+树的特征:

  • 1.有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
  • 2.所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
  • 3.所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。一开始就要规定好,要么元素在子节点都是最大的要么都是最小的,不能有的是最大的,有的是最小的。

B+树的优势:

  • 1.单一节点存储更多的元素,使得查询的IO次数更少。
  • 2.所有查询都要查找到叶子节点,查询性能稳定。
  • 3.所有叶子节点形成有序链表,便于范围查询。

在节点占用空间恒定的情况下,B+树单个节点的元素更多。比如同样节点大小,B树的每个节点有100个元素,B+树的每个节点就可以装下1000个元素。这样的好处就是树的高度降低了。

为什么MongoDB索引选择B-树,而MySQL选择B+树

一、B-树和B+树的区别

很明显,我们要想弄清楚原因就要知道B-树和B+树的区别。

1、B-树

B-树是一种自平衡的搜索树,形式很简单:

这就是一颗B-树。针对我们这个问题的最核心的特点如下:

  • (1)多路,非二叉树。
  • (2)每个节点既保存索引,又保存数据。
  • (3)搜索时相当于二分查找。

2、B+树

B+树是B-树的变种。

最核心的特点如下:

  • (1)多路非二叉
  • (2)只有叶子节点保存数据
  • (3)搜索时相当于二分查找
  • (4)增加了相邻接点的指向指针。

从上面我们可以看出最核心的区别主要有俩,一个是数据的保存位置,一个是相邻节点的指向。就是这俩造成了MongoDB和MySQL的差别。为什么呢?

3、B-树和B+树的区别

(1)B+树查询时间复杂度固定是logn,B-树查询复杂度最好是O(1)。

(2)B+树相邻接点的指针可以大大增加区间访问性,可使用在范围查询等,而B-树每个节点key和data在一起,则无法区间查找。

(3)B+树更适合外部存储,也就是磁盘存储。由于内节点无data域,每个节点能索引的范围更大更精确。

(4)注意这个区别相当重要,是基于(1)(2)(3)的,B-树每个节点即保存数据又保存索引,所以磁盘IO的次数很少,B+树只有叶子节点保存,磁盘IO多,但是区间访问比较好。

有了他们的区别之后,现在我们再来解释这个原因就好多了。

二、原因解释

想要解释原因,我们还必须要了解一下MongoDB和MySQL的基本概念。

1、MongoDB

MongoDB是文档型的数据库,是一种NoSQL,它使用类Json格式保存数据。比如之前我们的表可能有用户表、订单表、购物篮表等等,还要建立他们之间的外键关联关系。但是类Json就不一样了。

我们可以看到这种形式更简单,通俗易懂。那为什么MongoDB使用B-树呢?

MongoDB使用B-树,所有节点都有Data域,只要找到指定索引就可以进行访问,无疑单次查询平均快于MySQL。

2、MySQL

MySQL作为一个关系型数据库,数据的关联性是非常强的,区间访问是常见的一种情况,B+树由于数据全部存储在叶子节点,并且通过指针串在一起,这样就很容易的进行区间遍历甚至全部遍历。

这俩区别的核心如果你能看懂B-树和B+树的区别就很容易理解。

0%