B树如何让你的查询更快?

B树是一种搜索大量数据的结构。它是在40多年前发明的,但它仍然被大多数现代数据库所使用。虽然有较新的索引结构,如LSM树,但在处理大多数数据库查询时,B树是无敌的。

在阅读这篇文章后,你会知道B树是如何组织数据的,以及它是如何执行搜索查询的。

起源

为了理解B树,让我们首先了解二叉搜索树(BST)。

等等,不是一样的吗?

那么"B"代表什么呢?

爱德华M. B-tree的发明者McCreight曾经说过:

"the more you think about what the B in B-trees means, the better you understand B-trees."

将B-tree与BST混淆是一个非常常见的误解。无论如何,在我看来,BST是了解B树的一个很好的起点。让我们从一个简单的BST例子开始:

大的数字总是在右边,小的在左边。当我们增加更多的数字时,它可能会变得更清晰。

这棵树包含七个数字,但我们最多需要访问三个节点才能找到任何数字。下面的示例可视化搜索14。使用SQL定义查询,以便将此树视为实际的数据库索引。

硬件

从理论上讲,使用二叉搜索树来运行我们的查询看起来很好。它的时间复杂度(搜索时)是(O(log n)),与B树相同。然而,在实践中,这种数据结构需要在实际硬件上工作。

计算机有三个地方可以存储数据:

  • CPU高速缓存
  • RAM(内存)
  • 磁盘(存储)

高速缓存完全由CPU管理。而且,它相对较小,通常只有几兆字节。索引可能包含千兆字节的数据,所以它不适合那里。

数据库大量使用内存(RAM)。它有一些很大的优势:

  • 确保快速随机访问
  • 它的大小可能非常大(例如,AWS RDS云服务提供具有几TB可用内存的实例)

缺点?当电源关闭时,数据就会丢失。此外,与磁盘相比,它非常昂贵。

最后,内存的缺点是磁盘存储的优点。它很便宜,即使我们失去了电力,数据也会留在那里。然而,天下没有免费的午餐!

随机和顺序存取

内存可以被可视化为一行值的容器,其中每个容器都被编号。

现在让我们假设我们想要从容器1、4和6中读取数据。它需要随机访问:

然后让我们将其与阅读容器3、4和5进行比较。它可以按顺序进行:

"随机跳转"和"顺序读取"之间的区别可以根据硬盘驱动器来解释。它由磁头和磁盘组成。

"随机跳转"需要将磁头移动到磁盘上的给定位置。"顺序读取"只是简单地旋转磁盘,允许磁头读取连续的值。当阅读兆字节的数据时,这两种访问类型之间的差异是巨大的。使用"顺序读取"可以显著降低获取数据所需的时间。

随机访问和顺序访问之间的速度差异在Adam Jacobs发表在Acm Queue上的文章"[The Pathologies of Big Data](The Pathologies of Big Data - ACM Queue)"中进行了研究。它揭示了一些令人兴奋的事实:

  • HDD上的顺序访问可能比随机访问快数十万倍。🤯
  • 从磁盘中顺序读取可能比从内存中随机读取快。

现在谁还在用HDD?SSD怎么样?这项研究表明,从HDD完全顺序地阅读可能比SSD更快。然而,请注意,这篇文章是从2009年开始的,SSD在过去十年中发展迅速,因此这些结果可能已经过时。

总而言之,关键的要点是尽可能地选择顺序访问。在下一段中,我将解释如何将其应用于我们的索引结构。

为顺序访问优化树

二叉搜索树可以在内存中以与堆相同的方式表示:

  • 父节点位置为 i
  • 左节点位置为 2i
  • 右节点位置为 2i+1

示例计算这些位置(父节点从1开始):

根据计算出的位置,将节点放置到存储器中:

还记得前面的可视化的查询吗?

这就是它在内存级别上的样子:

执行查询时,需要访问内存地址1、3和6。访问三个节点不是问题;然而,随着我们存储更多的数据,树变得更高。存储超过一百万个值需要树的高度至少为20。这意味着必须读取内存中不同位置的20个值。它会导致完全随机访问!

当树的高度增长时,随机访问会导致越来越多的延迟。解决这个问题的方法很简单:在宽度上而不是在高度上生长树。它可以通过将多个值打包到单个节点中来实现。

它为我们带来了以下好处:

  • 树更浅(两层而不是三层)
  • 它仍然有很大的空间来容纳新的值,而不需要进一步发展。

在这样的索引上执行的查询如下所示:

请注意,每次访问一个节点时,我们都需要加载它的所有值。在这个例子中,我们需要加载4个值(如果树已满,则为6个),以达到我们正在寻找的值。下面,你会在内存中找到这棵树的可视化:

与上一个示例(树的高度增加)相比,这个搜索应该更快。我们只需要随机访问两次(跳转到单元格0和9),然后依次读取其余的值。

随着数据库的增长,这个解决方案的效果越来越好。如果你想存储100万个值,那么你需要:

  • 二叉搜索树,有20层

OR

  • 3-值节点树,具有10个级别

来自单个节点的值构成一个页面。在上面的示例中,每个页面都包含三个值。页是一组相邻放置在磁盘上的值,因此数据库可以通过一次顺序读取一次访问整个页。

它又是如何与现实联系起来的呢?Postgres页面大小为8KB。我们假设20%用于元数据,因此还剩下6kB。页面的一半需要存储指向节点子节点的指针,因此它为我们提供了3KB的值。BIGINT大小为8字节,因此我们可以在一个页面中存储约375个值。

假设一个数据库中的一些相当大的表有10亿行,我们需要在Postgres树中存储多少层?根据上面的计算,如果我们创建一个可以在单个节点中处理375个值的树,那么它可能会存储10亿个值,而树只有四个级别。二叉搜索树将需要30级这样的数据量。

总而言之,将多个值放在树的单个节点中有助于我们降低其高度,从而利用顺序访问的好处。此外,B树不仅可以在高度上增长,还可以在宽度上增长(通过使用更大的页面)。

平衡

数据库中有两种类型的操作:写和读。在上一节中,我们讨论了从B树阅读数据的问题。然而,写也是一个重要的部分。当将数据写入数据库时,B树需要不断更新新值。

树的形状取决于添加到树中的值的顺序。在二叉树中很容易看到。如果这些值以不正确的顺序相加,我们可能得到不同深度的树。

当树在不同的节点上有不同的深度时,它被称为不平衡树。有两种方法可以使这样的树返回到平衡状态:

  1. 开始重建它,只需按正确的顺序添加值。
  2. 保持平衡的所有时间,因为新的值被添加。

B-tree实现了第二种选择。使树始终保持平衡的特性称为自平衡。

自平衡算法举例

构建B树可以简单地通过创建一个节点并添加新值直到其中没有可用空间。

如果相应的页面上没有空间,则需要将其拆分。要执行拆分,选择"拆分点"。在这种情况下,它将是12,因为它在中间。"分割点"是一个将被移动到上一页的值。

现在,它把我们带到了一个有趣的地方,那里没有上层页面。在这种情况下,需要生成一个新的根页面(它将成为新的根页面!)。

最后,在这三个中有一些空闲空间,因此可以添加值14。

按照这个算法,我们可以不断地向B树添加新的值,它将始终保持平衡!

总结

在本文中,您了解了B树的工作原理。总而言之,它可以简单地描述为一个二叉搜索树,有两个变化:

  • 每个节点可以包含多个值
  • 插入新值之后是自平衡算法。

虽然现代数据库使用的结构通常是B树的一些变体(如B+树),但它们仍然基于原始概念。在我看来,B树的一个强大之处在于,它被设计成直接在硬件上处理大量数据。这可能是B树与我们在一起这么长时间的原因。

原文: blog.allegro.tech/2023/11/how...

相关推荐
凭君语未可几秒前
豆包MarsCode:小C点菜问题
算法
xiao-xiang3 分钟前
jenkins-通过api获取所有job及最新build信息
前端·servlet·jenkins
C语言魔术师20 分钟前
【小游戏篇】三子棋游戏
前端·算法·游戏
自由自在的小Bird20 分钟前
简单排序算法
数据结构·算法·排序算法
Channing Lewis34 分钟前
flask常见问答题
后端·python·flask
Channing Lewis35 分钟前
如何保护 Flask API 的安全性?
后端·python·flask
匹马夕阳2 小时前
Vue 3中导航守卫(Navigation Guard)结合Axios实现token认证机制
前端·javascript·vue.js
你熬夜了吗?2 小时前
日历热力图,月度数据可视化图表(日活跃图、格子图)vue组件
前端·vue.js·信息可视化
王老师青少年编程7 小时前
gesp(C++五级)(14)洛谷:B4071:[GESP202412 五级] 武器强化
开发语言·c++·算法·gesp·csp·信奥赛
DogDaoDao7 小时前
leetcode 面试经典 150 题:有效的括号
c++·算法·leetcode·面试··stack·有效的括号