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...

相关推荐
Myli_ing4 分钟前
HTML的自动定义倒计时,这个配色存一下
前端·javascript·html
EterNity_TiMe_14 分钟前
【论文复现】(CLIP)文本也能和图像配对
python·学习·算法·性能优化·数据分析·clip
dr李四维21 分钟前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
机器学习之心25 分钟前
一区北方苍鹰算法优化+创新改进Transformer!NGO-Transformer-LSTM多变量回归预测
算法·lstm·transformer·北方苍鹰算法优化·多变量回归预测·ngo-transformer
yyt_cdeyyds35 分钟前
FIFO和LRU算法实现操作系统中主存管理
算法
Iced_Sheep38 分钟前
干掉 if else 之策略模式
后端·设计模式
雯0609~42 分钟前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
℘团子এ1 小时前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z1 小时前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
XINGTECODE1 小时前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang