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树需要不断更新新值。
树的形状取决于添加到树中的值的顺序。在二叉树中很容易看到。如果这些值以不正确的顺序相加,我们可能得到不同深度的树。
当树在不同的节点上有不同的深度时,它被称为不平衡树。有两种方法可以使这样的树返回到平衡状态:
- 开始重建它,只需按正确的顺序添加值。
- 保持平衡的所有时间,因为新的值被添加。
B-tree实现了第二种选择。使树始终保持平衡的特性称为自平衡。
自平衡算法举例
构建B树可以简单地通过创建一个节点并添加新值直到其中没有可用空间。
如果相应的页面上没有空间,则需要将其拆分。要执行拆分,选择"拆分点"。在这种情况下,它将是12,因为它在中间。"分割点"是一个将被移动到上一页的值。
现在,它把我们带到了一个有趣的地方,那里没有上层页面。在这种情况下,需要生成一个新的根页面(它将成为新的根页面!)。
最后,在这三个中有一些空闲空间,因此可以添加值14。
按照这个算法,我们可以不断地向B树添加新的值,它将始终保持平衡!
总结
在本文中,您了解了B树的工作原理。总而言之,它可以简单地描述为一个二叉搜索树,有两个变化:
- 每个节点可以包含多个值
- 插入新值之后是自平衡算法。
虽然现代数据库使用的结构通常是B树的一些变体(如B+树),但它们仍然基于原始概念。在我看来,B树的一个强大之处在于,它被设计成直接在硬件上处理大量数据。这可能是B树与我们在一起这么长时间的原因。