CategoryResourceRepost/极客时间专栏/SQL必知必会/第二章:SQL性能优化篇/24丨索引的原理:我们为什么用B+树来做索引?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

12 KiB
Raw Blame History

上节课我讲到了索引的作用,是否需要建立索引,以及建立什么样的索引,需要我们根据实际情况进行选择。我之前说过,索引其实就是一种数据结构,那么今天我们就来看下,索引的数据结构究竟是怎样的?对索引底层的数据结构有了更深入的了解后,就会更了解索引的使用原则。

今天的文章内容主要包括下面几个部分:

  1. 为什么索引要存放到硬盘上?如何评价索引的数据结构设计的好坏?
  2. 使用平衡二叉树作为索引的数据结构有哪些不足?
  3. B树和B+树的结构是怎样的为什么我们常用B+树作为索引的数据结构?

如何评价索引的数据结构设计好坏

数据库服务器有两种存储介质,分别为硬盘和内存。内存属于临时存储,容量有限,而且当发生意外时(比如断电或者发生故障重启)会造成数据丢失;硬盘相当于永久存储介质,这也是为什么我们需要把数据保存到硬盘上。

虽然内存的读取速度很快但我们还是需要将索引存放到硬盘上这样的话当我们在硬盘上进行查询时也就产生了硬盘的I/O操作。相比于内存的存取来说硬盘的I/O存取消耗的时间要高很多。我们通过索引来查找某行数据的时候需要计算产生的磁盘I/O次数当磁盘I/O次数越多所消耗的时间也就越大。如果我们能让索引的数据结构尽量减少硬盘的I/O操作所消耗的时间也就越小。

二叉树的局限性

二分查找法是一种高效的数据检索方式时间复杂度为O(log2n),是不是采用二叉树就适合作为索引的数据结构呢?

我们先来看下最基础的二叉搜索树Binary Search Tree搜索某个节点和插入节点的规则一样我们假设搜索插入的数值为key

  1. 如果key大于根节点则在右子树中进行查找
  2. 如果key小于根节点则在左子树中进行查找
  3. 如果key等于根节点也就是找到了这个节点返回根节点即可。

举个例子我们对数列3422895237791创造出来的二分查找树如下图所示


但是存在特殊的情况,就是有时候二叉树的深度非常大。比如我们给出的数据顺序是(5, 22, 23, 34, 77, 89, 91),创造出来的二分搜索树如下图所示:


你能看出来第一个树的深度是3也就是说最多只需3次比较就可以找到节点而第二个树的深度是7最多需要7次比较才能找到节点。

第二棵树也属于二分查找树但是性能上已经退化成了一条链表查找数据的时间复杂度变成了O(n)。为了解决这个问题人们提出了平衡二叉搜索树AVL树它在二分搜索树的基础上增加了约束每个节点的左子树和右子树的高度差不能超过1也就是说节点的左子树和右子树仍然为平衡二叉树。

这里说一下常见的平衡二叉树有很多种包括了平衡二叉搜索树、红黑树、数堆、伸展树。平衡二叉搜索树是最早提出来的自平衡二叉搜索树当我们提到平衡二叉树时一般指的就是平衡二叉搜索树。事实上第一棵树就属于平衡二叉搜索树搜索时间复杂度就是O(log2n)。

我刚才提到过数据查询的时间主要依赖于磁盘I/O的次数如果我们采用二叉树的形式即使通过平衡二叉搜索树进行了改进树的深度也是O(log2n)当n比较大时深度也是比较高的比如下图的情况


每访问一次节点就需要进行一次磁盘I/O操作对于上面的树来说我们需要进行5次I/O操作。虽然平衡二叉树比较的效率高但是树的深度也同样高这就意味着磁盘I/O操作次数多会影响整体数据查询的效率。

针对同样的数据如果我们把二叉树改成M叉树M>2当M=3时同样的31个节点可以由下面的三叉树来进行存储


你能看到此时树的高度降低了当数据量N大的时候以及树的分叉数M大的时候M叉树的高度会远小于二叉树的高度。

什么是B树

如果用二叉树作为索引的实现结构会让树变得很高增加硬盘的I/O次数影响数据查询的时间。因此一个节点就不能只有2个子节点而应该允许有M个子节点(M>2)。

B树的出现就是为了解决这个问题B树的英文是Balance Tree也就是平衡的多路搜索树它的高度远小于平衡二叉树的高度。在文件系统和数据库系统中的索引结构经常采用B树来实现。

B树的结构如下图所示


B树作为平衡的多路搜索树它的每一个节点最多可以包括M个子节点M称为B树的阶。同时你能看到每个磁盘块中包括了关键字和子节点的指针。如果一个磁盘块中包括了x个关键字那么指针数就是x+1。对于一个100阶的B树来说如果有3层的话最多可以存储约100万的索引数据。对于大量的索引数据来说采用B树的结构是非常适合的因为树的高度要远小于二叉树的高度。

一个M阶的B树M>2有以下的特性

  1. 根节点的儿子数的范围是[2,M]。
  2. 每个中间节点包含k-1个关键字和k个孩子孩子的数量=关键字的数量+1k的取值范围为[ceil(M/2), M]。
  3. 叶子节点包括k-1个关键字叶子节点没有孩子k的取值范围为[ceil(M/2), M]。
  4. 假设中间节点节点的关键字为Key[1], Key[2], …, Key[k-1]且关键字按照升序排序即Key[i]<Key[i+1]。此时k-1个关键字相当于划分了k个范围也就是对应着k个指针即为P[1], P[2], …, P[k]其中P[1]指向关键字小于Key[1]的子树P[i]指向关键字属于(Key[i-1], Key[i])的子树P[k]指向关键字大于Key[k-1]的子树。
  5. 所有叶子节点位于同一层。

上面那张图所表示的B树就是一棵3阶的B树。我们可以看下磁盘块2里面的关键字为812它有3个孩子(35)(910) 和 (1315),你能看到(35)小于8(910)在8和12之间而(1315)大于12刚好符合刚才我们给出的特征。

然后我们来看下如何用B树进行查找。假设我们想要查找的关键字是9那么步骤可以分为以下几步

  1. 我们与根节点的关键字(1735进行比较9小于17那么得到指针P1
  2. 按照指针P1找到磁盘块2关键字为812因为9在8和12之间所以我们得到指针P2
  3. 按照指针P2找到磁盘块6关键字为910然后我们找到了关键字9。

你能看出来在B树的搜索过程中我们比较的次数并不少但如果把数据读取出来然后在内存中进行比较这个时间就是可以忽略不计的。而读取磁盘块本身需要进行I/O操作消耗的时间比在内存中进行比较所需要的时间要多是数据查找用时的重要因素B树相比于平衡二叉树来说磁盘I/O操作要少在数据查询中比平衡二叉树效率要高。

什么是B+树

B+树基于B树做出了改进主流的DBMS都支持B+树的索引方式比如MySQL。B+树和B树的差异在于以下几点

  1. 有 k 个孩子的节点就有k个关键字。也就是孩子数量=关键字数而B树中孩子数量=关键字数+1。
  2. 非叶子节点的关键字也会同时存在在子节点中,并且是在子节点中所有关键字的最大(或最小)。
  3. 非叶子节点仅用于索引不保存数据记录跟记录有关的信息都放在叶子节点中。而B树中非叶子节点既保存索引也保存数据记录。
  4. 所有关键字都在叶子节点出现,叶子节点构成一个有序链表,而且叶子节点本身按照关键字的大小从小到大顺序链接。

下图就是一棵B+树阶数为3根节点中的关键字1、18、35分别是子节点1814182431354153中的最小值。每一层父节点的关键字都会出现在下一层的子节点的关键字中因此在叶子节点中包括了所有的关键字信息并且每一个叶子节点都有一个指向下一个节点的指针这样就形成了一个链表。


比如我们想要查找关键字16B+树会自顶向下逐层进行查找:

  1. 与根节点的关键字(11835)进行比较16在1和18之间得到指针P1指向磁盘块2
  2. 找到磁盘块2关键字为1814因为16大于14所以得到指针P3指向磁盘块7
  3. 找到磁盘块7关键字为141617然后我们找到了关键字16所以可以找到关键字16所对应的数据。

整个过程一共进行了3次I/O操作看起来B+树和B树的查询过程差不多但是B+树和B树有个根本的差异在于B+树的中间节点并不直接存储数据。这样的好处都有什么呢?

首先B+树查询效率更稳定。因为B+树每次只有访问到叶子节点才能找到对应的数据而在B树中非叶子节点也会存储数据这样就会造成查询效率不稳定的情况有时候访问到了非叶子节点就可以找到关键字而有时需要访问到叶子节点才能找到关键字。

其次B+树的查询效率更高这是因为通常B+树比B树更矮胖阶数更大深度更低查询所需要的磁盘I/O也会更少。同样的磁盘页大小B+树可以存储更多的节点关键字。

不仅是对单个关键字的查询上在查询范围上B+树的效率也比B树高。这是因为所有关键字都出现在B+树的叶子节点中并通过有序链表进行了链接。而在B树中则需要通过中序遍历才能完成查询范围的查找效率要低很多。

总结

磁盘的I/O操作次数对索引的使用效率至关重要。虽然传统的二叉树数据结构查找数据的效率高但很容易增加磁盘I/O操作的次数影响索引使用的效率。因此在构造索引的时候我们更倾向于采用“矮胖”的数据结构。

B树和B+树都可以作为索引的数据结构在MySQL中采用的是B+树B+树在查询性能上更稳定在磁盘页大小相同的情况下树的构造更加矮胖所需要进行的磁盘I/O次数更少更适合进行关键字的范围查询。


今天我们对索引的底层数据结构进行了学习你能说下为什么数据库索引采用B+树而不是平衡二叉搜索树吗另外B+树和B树在构造和查询性能上有什么差异呢

欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。