B 树与 B+ 树

引入

为什么会出现B树、B+树?数据库系统普遍采用B树或B+树作为索引结构(例如mysql的InnoDB引擎使用的B+树),理解不透彻B树,则无法理解数据库的索引机制;接下来将用最简洁直白的内容来了解B树、B+树的数据结构

另外,B-树,即为B树。因为B树的原英文名称为B-tree,而国内很多人喜欢把B-tree译作B-树,其实,这是个非常不好的直译,很容易让人产生误解。如人们可能会以为B-树是一种树,而B树又是一种树。而事实上是,B-tree就是指的B树,目前理解B的意思为平衡

从磁盘查找数据效率低,一般是什么原因?CPU 不能直接与磁盘交互,因为 CPU 读写的速度远远大于磁盘读写的速度,因此需要先将数据加载到内存中,然后让 CPU 与内存交互。在这个过程当中,如果数据查询效率低,只有可能是从磁盘读取到内存的 IO 效率低,影响 IO 效率的因素一个是数据量、一个是读取磁盘的次数。
在这里插入图片描述

为了提升读取速度,首先是建立索引,索引如同查字典时先找到拼音,再找到对应的页。(key-value 键值对)
在这里插入图片描述

如果获取索引呢?

第一种方式是直接线性搜索,数组实现删除移动元素的开销是很大的,链表实现,查找元素的开销很大
在这里插入图片描述

第二种方式是哈希表实现,哈希表数据量过大的时候会出现大量的冲突,冲突就会产生很长的链表,同样出现上面线性的问题

在这里插入图片描述

因此思考树结构

在这里插入图片描述

首先二叉树肯定不行,二叉树本来无序,查找需要一个一个遍历。
在这里插入图片描述

使用二叉查找树、二叉排序树、二叉搜索树,这三个是一个东西,让二叉树变得有序。详细见: 二叉搜索树 | 二叉查找树 | 二叉排序树 (Binary Search Tree,简称 BST)
在这里插入图片描述

查找效率明显提升,但是会出现如下问题:
在这里插入图片描述

为解决上述问题,设置插入时左子树和右子树高度差不超过 1,这样就解决了上述问题,如下: 详细见: 平衡二叉树(AVL)
在这里插入图片描述

但是平衡二叉树为了保持这种平衡需要进行大量旋转操作,增加了算法开销,因此进一步:尽可能保持树的平衡的同时,减少旋转操作,出现了红黑树,红黑树本质是保持黑色节点的平衡。详细见:红黑树(RBTree)
在这里插入图片描述

但是随着数据量的增大,数据量非常大时,红黑树的深度也会非常大,也就意味着IO次数也会很大!思考为什么会导致最终树的深度还是很深,每个节点最多只能有两个子节点,因此在满足有序的基础上,实现多个子节点,这就出现了 B树(B-Tree)!

B 树

由于 B 树每个节点可以有多个子节点,设置子节点数量的上限为 m,树通常称之为 m 阶 B 树或 m 叉树,满足如下条件

  • 每个节点最多只有 m 个子节点。
  • 每个子节点至多 m - 1 个关键字
  • 所有叶子都出现在同一水平(高度一致)。

因此每个节点的结构如下:

  • 第一个 n 表示该节点有 n 个关键字
  • P i P_i Pi 指向子节点, k i k_i ki 为当前节点的关键字(即查找数字中的具体数值)
    在这里插入图片描述

B 树同样满足搜索树的性质,即 k 1 < k 2 < k 3 < . . . < k n − 1 < k n k_1 < k_2 < k_3 < ... < k_{n-1} < k_n k1<k2<k3<...<kn1<kn ,且 k 1 k_1 k1 > P 0 P_0 P0 指向的子节点的所有关键字 , k 1 k_1 k1 < P 1 P_1 P1 指向的子节点的所有关键字, k 2 k_2 k2 > P 1 P_1 P1 指向的子节点的所有关键字 , k 2 k_2 k2 < P 2 P_2 P2 指向的子节点的所有关键字 …

例如: m = 4 的 4 阶 B 树如下:每个关键字不超过 3,且满足搜索树的性质!
在这里插入图片描述

通过上述可以看出,B 树还满足一下性质:
在这里插入图片描述

B 树的查找

如下:查找 37,比 48 小,走左边,比 25 大,走右边,完成
在这里插入图片描述

B 树的插入

针对m阶高度h的B树,插入一个元素时,首先查找在B树中是否存在,如果不存在,即在叶子结点处结束,然后在叶子结点中插入该新的元素。

  • 若该节点元素个数小于m-1,直接插入;
  • 若该节点元素个数等于m-1,引起节点分裂;以该节点中间元素为分界,取中间元素(偶数个数,中间两个随机选取)插入到父节点中;
  • 重复上面动作,直到所有节点符合B树的规则;最坏的情况一直分裂到根节点,生成新的根节点,高度增加1;

上面三段话为插入动作的核心,接下来以 5 阶 B 树为例,详细讲解插入的动作;

在这里插入图片描述
图(1)插入元素【8】后变为图(2),此时根节点元素个数为5,不符合 1<= 根节点元素个数 <=4,进行分裂**(真实情况是先分裂,然后插入元素,这里是为了直观而先插入元素,下面的操作都一样,不再赘述)**,取节点中间元素【7】,加入到父节点,左右分裂为2个节点,如图(3)

在这里插入图片描述

接着插入元素【5】,【11】,【17】时,不需要任何分裂操作,如图(4)
在这里插入图片描述

插入元素【13】
在这里插入图片描述

节点元素超出最大数量,进行分裂,提取中间元素【13】,插入到父节点当中,如图(6)
在这里插入图片描述

接着插入元素【6】,【12】,【20】,【23】时,不需要任何分裂操作,如图(7)
在这里插入图片描述

插入【26】时,最右的叶子结点空间满了,需要进行分裂操作,中间元素【20】上移到父节点中,注意通过上移中间元素,树最终还是保持平衡,分裂结果的结点存在2个关键字元素。
在这里插入图片描述

插入【4】时,导致最左边的叶子结点被分裂,【4】恰好也是中间元素,上移到父节点中,然后元素【16】,【18】,【24】,【25】陆续插入不需要任何分裂操作

在这里插入图片描述

最后,当插入【19】时,含有【14】,【16】,【17】,【18】的结点需要分裂,把中间元素【17】上移到父节点中,但是情况来了,父节点中空间已经满了,所以也要进行分裂,将父节点中的中间元素【13】上移到新形成的根结点中,这样具体插入操作的完成。
在这里插入图片描述

B 树的删除

首先查找 B 树中需删除的元素, 如果该元素在 B 树中存在,则将该元素在其结点中进行删除;删除该元素后,首先判断该元素是否有左右孩子结点,如果有,则上移孩子结点中的某相近元素(“左孩子最右边的节点”或“右孩子最左边的节点”)到父节点中,然后是移动之后的情况;如果没有,直接删除。

  • 某结点中元素数目小于 (m/2)-1, (m/2) 向上取整,则需要看其某相邻兄弟结点是否丰满;
  • 如果丰满(结点中元素个数大于 (m/2)-1),则向父节点借一个元素来满足条件;
    如果其相邻兄弟都不丰满,即其结点数目等于 (m/2)-1,则该结点与其相邻的某一兄弟结点进行“合并”成一个结点;

如图依次删除依次删除【8】,【20】,【18】,【5】

  • 关键要领,元素个数小于 2(m/2 -1) 就合并,大于4(m-1) 就分裂

在这里插入图片描述

首先删除元素【8】,当然首先查找【8】,【8】在一个叶子结点中,删除后该叶子结点元素个数为2,符合B树规则,操作很简单,咱们只需要移动【11】至原来【8】的位置,移动【12】至【11】的位置(也就是结点中删除元素后面的元素向前移动)
在这里插入图片描述

下一步,删除【20】,因为【20】没有在叶子结点中,而是在中间结点中找到,咱们发现它的继承者【23】(字母升序的下个元素),将【23】上移到【20】的位置,然后将孩子结点中的【23】进行删除,这里恰好删除后,该孩子结点中元素个数大于2,无需进行合并操作。

在这里插入图片描述

下一步删除【18】,【18】在叶子结点中,但是该结点中元素数目为2,删除导致只有1个元素,已经小于最小元素数目2, 而由前面我们已经知道:如果其某个相邻兄弟结点中比较丰满(元素个数大于 ceil(5/2)-1=2),则可以向父结点借一个元素,然后将最丰满的相邻兄弟结点中上移最后或最前一个元素到父节点中,在这个实例中,右相邻兄弟结点中比较丰满(3个元素大于2),所以先向父节点借一个元素【23】下移到该叶子结点中,代替原来【19】的位置,【19】前移;然【24】在相邻右兄弟结点中上移到父结点中,最后在相邻右兄弟结点中删除【24】,后面元素前移。
在这里插入图片描述

最后一步删除【5】, 删除后会导致很多问题,因为【5】所在的结点数目刚好达标,刚好满足最小元素个数(ceil(5/2)-1=2),而相邻的兄弟结点也是同样的情况,删除一个元素都不能满足条件,所以需要该节点与某相邻兄弟结点进行合并操作;首先移动父结点中的元素(该元素在两个需要合并的两个结点元素之间)下移到其子结点中,然后将这两个结点进行合并成一个结点。所以在该实例中,咱们首先将父节点中的元素【4】下移到已经删除【5】而只有【6】的结点中,然后将含有【4】和【6】的结点和含有【1】,【3】的相邻兄弟结点进行合并成一个结点。
在这里插入图片描述

也许你认为这样删除操作已经结束了,其实不然,在看看上图,对于这种特殊情况,你立即会发现父节点只包含一个元素【7】,没达标(因为非根节点包括叶子结点的元素 K 必须满足于 2=<K<=4,而此处的 K=1),这是不能够接受的。如果这个问题结点的相邻兄弟比较丰满,则可以向父结点借一个元素。而此时兄弟节点元素刚好为 2,刚刚满足,只能进行合并,而根结点中的唯一元素【13】下移到子结点,这样,树的高度减少一层。
在这里插入图片描述

磁盘 IO 与预读

磁盘 IO

计算机存储设备一般分为两种:内存储器(main memory)和外存储器(external memory)。

内存储器为内存,内存存取速度快,但容量小,价格昂贵,而且不能长期保存数据(在不通电情况下数据会消失)。

外存储器即为磁盘读取,磁盘读取数据靠的是机械运动,每次读取数据花费的时间可以分为寻道时间、旋转延迟、传输时间三个部分,寻道时间指的是磁臂移动到指定磁道所需要的时间,主流磁盘一般在5ms以下;旋转延迟就是我们经常听说的磁盘转速,比如一个磁盘7200转,表示每分钟能转7200次,也就是说1秒钟能转120次,旋转延迟就是1/120/2 = 4.17ms;传输时间指的是从磁盘读出或将数据写入磁盘的时间,一般在零点几毫秒,相对于前两个时间可以忽略不计。那么访问一次磁盘的时间,即一次磁盘IO的时间约等于5+4.17 = 9ms左右,听起来还挺不错的,但要知道一台500 -MIPS的机器每秒可以执行5亿条指令,因为指令依靠的是电的性质,换句话说执行一次IO的时间可以执行40万条指令,数据库动辄十万百万乃至千万级数据,每次9毫秒的时间,显然是个灾难。

预读

考虑到磁盘IO是非常高昂的操作,计算机操作系统做了一些优化,当一次IO时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,因为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。每一次IO读取的数据我们称之为一页(page)。具体一页有多大数据跟操作系统有关,一般为4k或8k,也就是我们读取一页内的数据时候,实际上才发生了一次IO,这个理论对于索引的数据结构设计非常有帮助。

在这里插入图片描述

任意打开某个文档可以看到占用空间都是 4K 的整数倍,与实际大小不同,比如实际大小为 11.9KB,那占用空间就是 12KB(4K 的 3 倍)
在这里插入图片描述

在这里插入图片描述

B 树的索引方式

如下图:比如要找索引 28 对应的数据,走磁盘 3,再走磁盘 8,获取索引 28 对应的 data 即可,这里注意每个关键字索引都对应了一个 data 数据,这个数据是要占据很大一部分空间的!(下图中每个磁盘 i i i,称为一个磁盘块)
在这里插入图片描述

假设规定一个磁盘块占 16K,而一个 data 占据 1K,并假设每个节点存储子节点地址的比如上图中的 p 1 , p 2 , p 3 , . . . p_1, p_2, p_3, ... p1,p2,p3,... 以及索引值比如 16, 34 都不占空间,那么一个 16K 的磁盘块能够存储的 16 个 data,那么三层的 B 树最多能够存储的 data 数量约等于 16 * 16 * 16 = 4096 条,如果数据增加,就只能加层。 从这个分析中可以看到, B 树索引的结构内部节点中的 data 占据了很多的空间,导致有限空间的磁盘块内,所能使用的关键字数目不多,那么是否可以在内部节点不放 data,从而产生更多的索引值呢? B+树产生!

B+ 树

通过上述 B 树的局限可以知道, B+ 树非叶子结点不再存储数据,而是存储指针和索引记录。假设磁盘块仍然为 16K,每个索引值占用 10byte,指针指向的地址不占用空间,因此一个磁盘块就能够产生 16000/10 = 1600 个索引(1K = 1000byte),两层最多能产生:1600 * 1600 = 2560000 个数据索引,这是非常大的!!!!

在这里插入图片描述

B+ 树的特征

  • 有 m 个子树的中间节点包含有 m 个元素(m 阶 B+ 树),每个元素不保存数据,只用来索引;
  • 所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。
  • 所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。

B+ 树的查找

从根节点开始,对根节点关键字使用二分查找, 向下逐层查找,最终找到匹配的叶子节点

比如要查找 10,找到磁盘 2,从磁盘 2 找到索引 10,但不是叶节点,通过 10 的索引 p 2 p_2 p2 找到叶节点磁盘 5,从而找到 10 对应的 data
在这里插入图片描述

比如要查找 5
在这里插入图片描述

  • 从根节点开始,5<8 则从左分支向下查找,对应磁盘一 次IO
  • 找到2 5 8 节点,5>2 && 5<=5 则从 5 对应的分支进行查找,最终定位到 5,查找结束

B+ 树插入

  • 从叶子节点开始插入 ,且不能破坏关键字自小而大的顺序;
  • 由于 B+树中各结点中存储的关键字的个数有明确的范围,做插入操作可能会出现结点中关键字个数超过阶数的情况,此时需要将该结点进行“分裂”;
  • 如果插入的关键字是比该节点位置的关键字要大,则需要向父节点更新更新关键字为插入的最大值,直到更新到根节点为止

以 3 阶 B+ 树为例,对于 3 阶 B+ 树:上溢:> 3; 下溢:< 2

若被插入关键字所在的叶子结点,其含有关键字数目小于阶数 M,则直接插入结束;

插入 5 8 12
在这里插入图片描述

插入 15 ,此时导致关键字个数 > 3 产生上溢;此时需要分裂:

在这里插入图片描述

  1. 将该节点中的 8 与 15 上移动到父节点,确保父节点 8 关键字对应分支的关键字的都比 8 小,关键字对应的分支节点关键字都比 15 小。 此时没有则创建父节点 8 15 作为根节点
  2. 将该节点分裂成【5 8】 【12 15】两部分
  3. 将叶子节点用指针进行连接

注意: 只有是当前父节点为null时才上移动2个节点作为父节点,父节点不为空时则上移动中间节点

插入 9 20,根据根节点找到对应的叶子节点进行插入,没有产生上溢,直接插入,
但 9 > 叶子节点的 8,20 > 15 , 所以需要向上更新父节点关键字的大小,直到更新到根节点为止
在这里插入图片描述

继续插入 7 则会导致上溢出,则需要分裂

  1. 将 7 所在的节点中间值 mid=7 上移动到父节点
  2. 对应的节点进行分裂成左右 2 部分,右边到保留成当前 this 对象,左边的的则创建新的节点进行保存
  3. 检查上移动后父节点关键字是否存在上溢,有则需要继续上移动分裂

在这里插入图片描述

25 则插入 20 对应的子分支,该节点产生上溢出 ,需要分裂

  1. 提取该节点的中间节点mid=15上移动到父节点20的前面插入
  2. 将该节点分裂成【12 15】【20 25】左右字节点,加入父节点的孩子节点分支中
  3. 将叶子节点使用指针进行连接形成链表
  4. 插入的25 关键字大于该节点最大值20.所以需要向上更新父节点的最大值为25,直到根节点为止

在这里插入图片描述

检查该父节点关键字 >3 产生上溢,所以需要对该父节点继续分裂

注意此时上溢节点没有父节点即 parent=null, 所以我们需要上移动提取 9 25 两个节点作为其父节点同时也作为新的根节点,对溢出节点进行分裂【7 9】【20 25】
在这里插入图片描述

B+树删除

B+ 树的删除可能会触发下溢出,即节点关键字个数 <2,此时需要修复

  • 找到存储有该关键字所在的结点时,由于该结点中关键字个数大于⌈M/2⌉(2), 删除操作不会破坏 B+树,则可以直接删除。比如删除4 关键字不会下溢,可以直接删除
    在这里插入图片描述

删除 关键字8 ,导致该节点关键字不足,产生下溢,需要修复

  1. 看兄弟节点是否富余,如果富余则向兄弟节点借关键字,这里兄弟节点不富余
  2. 兄弟节点不富余则和兄弟节与当前节点合并(哪边有兄弟节点则和 合并哪边)–这里和左兄弟节点合并
  3. 合并后需要将左兄弟节点的指针置空,并从其父节点的孩子集合中删除对应分支
  4. 删除父节点上左节点合并后对应的关键字
  5. 如果删除最大值,则需要向上更新父节点最大值为该节点倒数第二位,即删除节点的前一个进行更新替换,直到更新到根节点为止

在这里插入图片描述
导致父节点关键字不足产生下溢出,则需要递归对父节点关键字进行处理

右兄弟不足,需要继续将右兄弟与之合并
在这里插入图片描述

合并后会发现根节点少了一个子节点,没关系,我们直接拿根节点的子节点即右子节点作为新的根节点即可,此时B+树的高度比原来降低了1高度
在这里插入图片描述

继续删除12 ,我们发现12对应节点的左兄弟节点关键字富余,可以向左兄弟节点借关键字9 放入15的前面,并更新左兄弟节点对应的父节点关键字为7 即可完成修复。
在这里插入图片描述

继续 删除25 删除的是该节点分支的最大值,所以需要向上更新父节点最大值为20
删除后导致该节点关键字个数不足2个产生下溢出,所以需要修复。 其左兄弟关键字个数不足,所以需要合并。
在这里插入图片描述

其他

为什么说 B+ 树比 B 树更适合数据库索引?

1)B+ 树的磁盘读写代价更低

B+ 树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对 B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说 IO 读写次数也就降低了;

2)B+ 树查询效率更加稳定

由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当;

3)B+ 树便于范围查询(最重要的原因,范围查找是数据库的常态)

B 树在提高了IO性能的同时并没有解决元素遍历的效率低下的问题,正是为了解决这个问题,B+ 树应用而生。B+ 树只需要去遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而 B 树不支持这样的操作或者说效率太低;

范围查找即:某个数据以及周围的数据均需要一起带上

References

B树、B+树详解

终于把B树搞明白了(一)_B树的引入,为什么会有B树

经典树结构——B+树的原理及实现

B+树详解

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值