我的秋招--B+树数据库相关知识--面试被卡的死死的

本文详细介绍了B树和B+树的概念、特性及在数据库系统中的应用。B树通过多叉结构降低树深,减少磁盘IO,而B+树所有数据都存储在叶子节点,利于区间查询。插入和删除操作在B树中可能导致节点分裂或合并,B+树则优化了查询效率。B+树在数据库索引中广泛应用,因其适合大容量数据存储和高效范围查找。
摘要由CSDN通过智能技术生成

二叉和多叉

在B树当中有一个非常巧妙的设计,就是每一个节点的孩子个数是元素的数量+1。并且和二叉搜索树一样,存在大小顺序的关联。

image-20201216214322038

如图,根节点有两个元素3和9,并且有3个孩子节点,刚好对应了3个区间。分别是小于3的,在3和9中间的以及大于9的,那么根据我们要查找的元素的大小,我们很容易判断究竟应该选择哪一个分支。而且节点中的元素是有序的,我们可以使用二分查找进行高效搜索。

既然二叉搜索树也可以完成节点的高效增删改查,为什么又需要搞出这个多叉搜索树呢?和二叉搜索树相比,它究竟有什么得天独厚的优点呢?

B树主要用在各大存储文件系统和数据库系统当中。在这些场景下数据总量很大,我们不可能将它们都存储在内存当中。所以为了解决这个问题,我们会在树节点当中存储孩子节点的在磁盘上的地址。在需要访问的时候通过磁盘加载将孩子节点的信息读取到内存当中。也就是说在数据库当中我们遍历树的时候也伴随着磁盘读取。

磁盘的随机读写是非常耗时的。显然,树的深度越大,磁盘读写的次数也就越多,那么带来的IO开销也就越大。所以为了优化这个问题,才设计出了B树。由于B树每个节点存储的数据和孩子节点数都大于2,所以和二叉搜索树相比,它的树深要明显小得多。因此读写磁盘的次数也更少,带来的IO开销也就越小。这也是它适合用在文件引擎以及数据库引擎上的原因。

B树

B树的定义

虽然B树是一棵多叉搜索树,但是并不意味着只要是多叉搜索树就是B树。B树对节点同样存在着一些限制,每个节点能够存储的元素以及孩子节点数量并不是随意的。

我们来看具体的定义:

  • 每个节点最多有m-1个关键字(可以存有的键值对)。
  • 根节点最少可以只有1个关键字
  • 非根节点至少有m/2-1个关键字
  • 每个节点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。
  • 所有叶子节点都位于同一层,或者说根节点到每个叶子节点的长度都相同。
  • 每个节点都存有索引和数据,也就是对应的key和value。

所以,根节点的关键字数量范围: 1 ≤ k ≤ m − 1 1\le k\le m-1 1km1,非根节点的关键字数量范围:。 [ ( m / 2 ) − 1 ] ≤ k ≤ ( m − 1 ) \left[ \left( m/2 \right) -1 \right] \le k\le \left( m-1 \right) [(m/2)1]k(m1)。k为元素数量。m/2向上取整。

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

B树的查找

B树当中一个节点对应的K个子树和它K-1个元素是对应的。我们只需要判断查找的key和当前节点所有元素的大小关系就可以判断数据的范围。

class Node:
    def __init__(self):
        self.keys = []
        self.childs = []

B树的一个节点当中有K-1个元素以及K个子树,我们用keys和childs来存储。并且我们知道,keys当中的元素是有序的。childs中的子树对应keys中元素分隔得到的区间。

我们假设我们要查找的元素是key,当前的节点是node。

首先我们查找node.keys当中大于等于key的位置,我们命名为pos。如果pos等于len(node.keys)或者node.keys[pos] != key,说明当前节点不是我们要找的,我们要继续搜索子树。

这个子树是什么呢?其实就是node.childs[pos],因为我前面说了我们在node.keys当中查找第一个大于等于key的,而node.keys[pos] != key,那么显然node.keys[pos] > key或者是key比node.keys当中的所有元素都要大,这样pos会是len(node.keys),也是node.childs[-1],所以不论是哪种情况,我们访问node.childs[pos]都是正确的。所以我们递归调用,否则的话说明node就是目标,我们直接返回。

举个例子:

image-20201216214322038

比如我们要搜索7,首先我们在根节点当中找到第一个大于等于7的位置,这个位置的元素是9不等于7,说明当前节点当中没有7,我们需要继续往子树递归查找。由于子树对应元素分割出来的区间,所以我们可以确定如果7存在子树当中,只会出现在9前面的子树中,所以我们往9的下标的子树,也就是node.childs[1]的子树方向递归。

B树的插入

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

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

博客

这篇博客我觉得讲的很好,大家可以访问学习。

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

  • 插入18,70,50,40

image-20201216224548267

  • 插入22

image-20201216224637792

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

image-20201216224710755

  • 接着插入23,25,39

image-20201216224859407

分裂,得到下面的。

image-20201216224733146

B树的删除

所有删除的都是叶子节点

首先,我们来理清楚第一个要点。无论我们当前删除的元素是什么,最终都会落实在叶子节点上,也就是说所有的情况都可以转化成删除叶子节点的问题。

我们举个例子,在下面这张图中,假如我们要删除元素11,而11在根节点上,显然我们要删除的位置并不在叶子节点。

image-20201216225652386

但是为了避免删除非叶子节点的元素,我们可以先找到11的后继节点。这里的后继节点指的是在这棵树上,比当前元素大的最小的节点。在这个图当中,11的后继节点是12,我们将12赋值给11,递归往下调用,转变成删除12,如图2:

image-20201216225730065

当然,我们选出来的后继节点仍然可能并不是叶子节点,这没有关系,我们只需要重复执行以上操作即可。因为我们可以保证后继节点出现的位置在树上的深度只会比当前元素更大,不会更小。而树深是有限的,也就是说最多经过有限次转化,我们就可以把删除操作转嫁到叶子节点身上。这一点是后续所有推导的前提,我们必须要搞清楚。

删除之后的两种情况

image-20201216230024478

在上图当中,M=4,也就是说我们最多允许一个节点出现4个分支,一个节点最少拥有4/2 - 1,也就是一个元素。

假如我们要删除的元素是19,由于节点3当中元素众多,即使删除掉一个元素,依然符合节点的要求,那么就不做任何操作。但如果我们删除的是10,由于节点10只有一个元素,如果删除了,那么就会破坏节点的最小元素数量的限制。

在这种情况下,只有一个办法,就是先删除,再和其他节点借。

和兄弟节点借

我们首先考虑和节点的兄弟节点借一个元素,这个思路很朴素。因为兄弟节点和当前节点的父亲节点相同,可以很方便地转移节点并保证树的性质。

假设我们要删除节点10,节点10只有一个元素,删除了必然破坏合法性。这个时候我们先看下哥哥的情况,哥哥节点当中有3个元素,即使借走一个仍然可以满足要求,那么我们就和哥哥借。

借的方法很简单,由于哥哥节点当中所有的元素都大于当前节点,所以显然为了保证元素顺序,我们会借第一个元素,也就是13。但是13是大于父节点中的12的,所以我们不能直接把13塞到原来10的位置,而需要先将父节点的12挪下来,放到10的位置,再将13填到12的位置上去。如果你熟悉平衡树的话,你会发现这其实是一个左旋操作,如果你不熟悉的话,也不用纠结。

最后达到平衡态的样子如下:

image-20201216230357076

兄弟节点也是穷光蛋

既然存在兄弟节点可以借那么显然也会存在兄弟节点借不了的情况,如果兄弟节点自身也是勉强达到条件,显然是借不了的。这种时候没办法,只能还是和父亲节点要。如果父亲节点稍稍富裕,给出了一个元素之后还是能满足条件,那么就从父亲节点出。但是这里需要注意,父亲节点给出去了一个元素,那么它的子树数量也应该随之减少,不然也会不满足B树的特性。为了达成这一点,可以通过合并两个子树来实现。

比如在下图当中,我们继续删除10:

image-20201216231115058

删除之后,会得到:

image-20201216231135229

你会发现这个图很奇怪,除了比较丑之外,最大的问题就是它的根节点有两个元素,但是却有四颗子树,这显然违反了B树的性质。

违反性质的原因是因为原本属于根节点的元素9被子树借走了,所以为了解决这个问题,我们需要将邻近的两棵子树合并。也就是将[6, 7]和[9]子树合并,得到:

image-20201216231154646

到这里,我们还有最后一个问题没解决,当我们跟父节点借合并子树时同样会导致父节点中的元素数量减少,万一父节点减少了一个元素之后也不满足条件应该怎么办?你可能会觉得是不是跟子节点借?其实想一下就会知道这不可行。原因也很简单,因为即使我们能找到富裕的子节点,但是也没办法让子树的数量随着也增加1。

解决这个问题的方法也不难,我们只需要递归借节点的操作,让父节点去和它的兄弟以及父节点借元素就好了。在极端情况下,这很有可能导致树的高度发生变化,比如:

image-20201216231328210

上图是一个阶数为5的B树,如果我们删除7,根据刚才的惯例,我们会跟父亲节点借元素6,并且和[1, 3]子树合并,得到:

image-20201216231352760

但是这一借会导致父节点破坏了B树的最低元素要求,所以我们需要递归维护父节点。也就是让父节点去重复借元素的步骤,我们可以发现对于节点[10]来说,它没有富裕的兄弟节点,只能继续和父节点借,这一借会再一次导致合并的发生,最终我们得到的结果如下:

image-20201216231420270

B+树

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

插入元素

当节点元素数量大于m-1的时候,按中间元素分裂成左右两部分,中间元素分裂到父节点当做索引存储,但是,本身中间元素还是分裂右边这一部分的

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

  • 插入5,10,15,20

image-20201216233248843

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

image-20201216233302956

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

image-20201216233321299

image-20201216233330438

删除操作

  • 初始状态

image-20201216233429891

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

image-20201216233520732

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

image-20201216233649292

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

image-20201216233717499

为什么使用B+树?

hash表的缺点

1.利用hash存储的话需要将所有的数据文件添加到内存,比较耗费内存空间。
2.如果所有的查询都是等值查询,那么hash确实很快,但是在企业或者实际工作中,范围查找的数据更多,而不是等值查询,因此hash就不太合适了。

二叉树红黑树的缺点

无论是二叉树还是红黑树,都会因为树的深度过深而造成io次数变多,影响数据读取的效率

B树的索引格式和缺点


B树的特点:

1.所有键值(key)分布在整棵树中
2.搜索有可能在非叶子结点结束,在关键字全集内做一次查找,性能逼近二分查找
3.每个结点最多拥有m个子树
4.根节点至少有2个子树
5.分支节点至少拥有m/2颗子树(除根节点和叶子节点外都是分支节点)
6.所有叶子节点都在同一层,每个节点最多有m-1个key,并且升序排列

B树的缺点:

1.每个节点都有key,同时也包括data,而每个页存储空间是有限的,如果data比较大的话,就会导致每个节点存储的key数量变小
2.当存储的数据量很大的时候,会导致深度较大,增加查询时磁盘io次数,进而影响查询性能。

B+树的索引格式:


B+树是在B树的基础上做的一种优化,变化如下:

1.B+树每个节点(16kb)可以包含更多的节点,这个做的原因主要有两个:第一是为了降低树的高度,第二个原因是将数据范围变为多个区间,区间越多,数据检索越快
2.非叶子节点存储key,叶子节点存储key和数据
16kb / (8B+6B) = 1170个
3.叶子节点两两指针相互连接,符合磁盘的预读性能,顺序查询性能更高。

注意
1.InnoDB是通过B+树结构对主键创建索引,然后叶子节点中存储记录,如果没有主键,那么会选择唯一键,如果没有唯一键,那么会生成一个6字节的row_id来作为主键

2.如果创建索引的键是其他字段,那么在叶子节点中存储的是该记录的主键,然后在通过主键索引找到对应的记录,叫做回表。

总结

索引其实是一种数据结构,MySql中的索引有B+树和Hash。其中常用的引擎索引用的都是B+树。为什么用B+不用其他数据结构呢。数组、二叉树、hash不都挺吃香的嘛。有序数组的确查询挺好的,但是当表中有记录增加或删除时,整个数组都要移动,效率堪忧。二叉树、红黑树则随着表中数据量增加,树越来越高,查询效率也就越来越差。hash作为java中常用数据结构,数组+链表,一个是存在hash冲突,第二点也就是最重要一点是元素是根据hash规则存储的,区间查询就麻烦,得全表查询了。而B+树,一个节点可以存多个key,树的高度就降下来,IO性能好很多,最底层的节点还有链表相连,区间查询又方便。综上,B+被作为是Innodb和MyIsam索引。

MyISAM和Innodb的比较

myisam

myISAM索引文件和数据文件是分离的(非聚集)

对应三个文件:

  1. frm: 表的定义
  2. myd:数据
  3. myi:索引文件

select如何执行: 一看where字段有索引,就去索引文件myi,根据B+树的结构来查找,找到key,拿到data(磁盘地址指针),再去myd里面拿到对应的一行数据。

innodb

对应两个文件

  1. frm:表的定义
  2. ibd:数据和索引

索引实现(聚集)

  • 表数据文件本身就是按B+树组织的一个索引结构文件

  • 聚集索引 叶子节点包含了完整的数据记录

  • 为什么InnoDB表必须有主键,并且推荐使用整型的自增主键?

    InnoDB是通过B+树结构对主键创建索引,然后叶子节点中存储记录,如果没有主键,那么会选择唯一键,如果没有唯一键,那么会生成一个6字节的row_id来作为主键。另外如果使用uuid的话,1<2总是比字符串比较快,字符串比较大小先要转换成ACSII码。并且整型占用的空间小。 最后自增的话,不容易造成插入到中介造成叶子节点的分裂形成大的开销。

  • 为什么非主键索引蟒袍叶子节点存储的是主键值。一致性和节省存储空间

万字详解B树
我觉得这篇文章将的很好,等我对B 树再熟悉了解后再来整理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值