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

好久没翻译文章了,感觉自己的英文功底有点下降,前几篇文章都着眼于网络I/O了,这篇文章之后我们开始看点数据库、数据结构之类的内容。
原文链接: blog.allegro.tech/2023/11/how...
已经征得作者的同意可以进行翻译。

B-树是一种搜索大量数据的结构,发明于四十年前,现在仍然被用于现代的数据库。尽管已经有了一些新的索引结构,像LSM(Log-Structured-Merge-Tree)树, 在处理大多数数据库查询的时候,B-树仍然是无与伦比的。

阅读本篇文章之后,你将会了解B-树如何组织数据以及如何执行搜索查询。

译者注, LSM树并不像是B树、红黑树一样是严格的数据结构,其实是一种存储结构,目前HBase,LevelDB,RocksDB这些NoSQL存储都是采用的LSM树)

源起

为了让我们理解B-tree,让我们先来将目光放在二叉搜索树

等等,这难道不是一样的吗?

那B-树的B代表什么?

根据维基百科, B-树的发明者 Edward M. McCreight曾经说过:

你越是思考B-树的B代表些什么,就能越能理解B-树。

把B-树和二叉搜索树混为一谈是一个非常普遍的误解,不管如何,在我看来,二叉搜索树是重塑B-树的一个很好的起点,让我们先从一个非常简单的二叉搜索树开始:

右边节点的数字总是比双亲节点要大,左边节点的数据总是比双亲节点的小。我们再添加一些数字会变得再清晰一些。

现在这颗二叉搜索树包含七个节点,但是我们最多需要访问三个节点才能找到我们想要找到的数字,下面示例搜索14这个数字的过程,这里我使用SQL去定义查询,以便将这棵树视为实际数据库索引。

硬件

理论上来说,使用二叉搜索树来运行我们的查询看起来没有问题,它的搜索花费的时间复杂度是O(logn),和B-树一样。然后,在实践中,数据结构需要工作在实际的硬件上。索引必须存储在你机器上的某个位置上。

计算机有三个位置存储数据:

  • CPU缓存
  • RAM(memory) 内存
  • Disj(存储) 磁盘

缓存完全被CPU管理。此外,它相对较小,通常只有几兆字节。索引可能包含几千兆的数据,所以这里不适合。

数据库大量使用内存,内存有一些非常棒的优点:

  • 快速随机存取(将在下一章节介绍)
  • 容量可以非常大((例如,AWS RDS 云服务提供可用内存为几 TB 的实例)

缺点是断点的时候会丢失数据,而且相对于磁盘,它相当昂贵。

最后内存的缺点就是存储器的优点,它很便宜,即使断电数据也会保留在那里。然而,天下没有免费的午餐,问题在于我们需要谨慎对待顺序访问和随机访问。只有在指定的条件下,磁盘读取数据才会很快。我会试着简单解释一下。

随机访问和顺序访问

内存可以形象的理解为一排排存放数值的容器,每个容器有对应的编号。

现在让我们假设我们需要从编号1,4,6这三个容器上读取数据,这需要随机访问:

然后和读编号为3,4,5的容器进行比较,它就可以按顺序完成。 随机跳转和顺序读取的不同可以用磁盘驱动器来解释,磁盘由磁头和磁盘组成。

"随机跳转"要求磁头移动到磁盘上的指定位置。"顺序读取"只需要旋转磁盘,让磁头读取连续的值, 在读取兆字节的数据时,这两种访问方式之间的差距是巨大的。使用"顺序读取"可以大大的降低获取数据所需的事件。

Adam Jacobs发表在Acm Queue上发表的文章"The Pathologies of Big Data" ,研究了随机访问和顺序访问在速度上的差异。文章揭示了一些令人震惊的事实。

  • 顺序访问在机械硬盘上的访问速度比随机访问快几十万倍。

  • 从磁盘顺序读取有可能比从内存读取更快

但是现在谁还用机械硬盘? 那固态硬盘呢? 这项研究显示机械硬盘上的顺序完全读取数据可能比固态硬盘更快。不过请注意,这篇文章是2009年的,而固态硬盘在过去十年得到了长久的发展。这些结果可能已经过时了。

总之,关键就在于就是尽可能选择顺序访问。下一个章节,我们将解释如何将其应用到我们的索引结构上。

优化对树的顺序访问

  • 二叉搜索树在内存中的表示方法和堆相同

    • 父节点的位置是i

    • 左节点的位置是2i

    • 右节点的位置是2i + 1

这是根据示例计算出来的位置(父节点从1开始)

根据被计算出来的位置,节点被对齐到内存中

你还记得我们前面讨论的可视化查询吗?

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

执行查询的时候,内存地址1,3,6会被访问,访问三个节点不是问题,然而,如果我们存储了更多数据,这棵树就可能变得更高。存储超过100万个值需要一颗高度至少为20的树。这意味着必须从内存不同位置读取20个值,这会导致完全的随机访问。

页面

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

​ 它有以下好处:

  • 树更浅,两层而不是三层。

  • 仍然有大量的空间可以容纳新的值,而无需进一步增长。

在这种索引上执行查询如下图所示:

请注意每次我们访问一个节点,我们都需要加载这个节点所有的值,在这个例子中,我们需要加载4个值(如果树是满的,就需要6个值)才能找到我们需要的值。下面是这棵树在内存中的展示

与上一个示例相比(树的高度不断增加),搜索应当更快,我们仅需要随机访问两次(跳转到0和9单元),然后顺序读取剩余的值。

  • 二叉搜索树的有20层

  • 只有10层的3值节点树。

单个节点的值构成一个页面,在上面的例子中,每个页面由三个值组成。页面是磁盘上一组相邻的值,因此数据库进需要一次顺序访问读取,就能同时读取整个页面,

它与现实又是如何联系的呢? Postgres的页面大小只有8kb,假设20%是元数据,那么还剩下6kb。页面的一半需要存储指向子节点的指针,所以给我们存储值剩余的空间就只剩下3kb,BIGINT的大小是8 bytes,因此我们能存储375个值再单个页面里面。

假设数据库有一些超级大的表有10亿条记录,那么我们在postgres树种需要多层才能存储? 根据上面的计算,单个节点可以存储375个值,它可以只有四层的树来存储10亿个值。对于如此大量的数据,二叉搜索树将需要30层来存储。

总之,在单个节点里面存储放置多个值有助于我们可以减少树的高度。我们因此就能从顺序访问中受益。然后B-树不仅可以增加高度,也可以通过增加页面大小来增加宽度。

平衡

在数据库中有两种基本的操作: 写和读。在上一节中我们讨论了从B-树中读取数据的问题。然而写入数据也是一个非常关键的点,向数据库中写入数据的时候,B-树需要不断更新新值。

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

当树在不同节点上具有不同的深度时,它被称为不平衡树,有两种方式可以将这样的树恢复到平衡状态。

  1. 重新构建这棵树,按照正确的顺序添加值。

  2. 在添加新值的时候同时保持平衡。

B- 树选择了第二种方案,使树始终保持平衡的特性称之为自平衡。

自平衡算法示例

构建B-树可以简单的从创建一个单独的节点开始,并不断的添加新值,直到节点里面没有空闲空间为止。

如果相应的页面没有空间,就需要进行页分裂,为了执行分裂,需要选择一个"分裂点",在这种情况下选择的分裂点将会是12,因为12处于3和15的中间,分裂点将会是一个移动到上个页面的值。

现在,我们遇到了一个有趣的问题,即没有上层页面,在这种情况下需要生产一个新的页面,分裂点将成为新的根页面。

最终,3所在的页面有一些剩余空间,因此可以将14添加进去。

按照这种算法,我们可以不断向B-树里面添加新值,而B树会一直保持平衡。

在这一点上,你可能会有一些合理的担忧,会有很多空闲空间没有机会被填满,例如,14、15、16位于不同的页面上,所以这些页面将永远只有一个值和两个空闲空间。

这是由于分裂位置的选择引起的,我们总是将页面从中间分裂。但是,每次进行分裂时。我们可以选择我们想要的任何分裂位置。

Postgres在执行页分裂的时候会执行一个算法,对应的实现可以在Postgre源代码中的bt_findsplitloc()找到实现(见参考链接)

总结一下

在这篇文章里面,你学习到B-树是如何工作的,总的来说,它可以被简化为一颗具有两个变化的二叉搜索树。

  • 每个节点包含超过一个值

  • 插入新的值的时候,会有一个自平衡算法。

尽管现代数据库用的是B-树的某个变体(像是B+树),它们仍然基于原始概念。我的观点是,B-树的一个巨大优势是它是为在实际硬件上存储大量数据而设计的。这可能是B-树在这么长的时间里面仍然陪伴着我们的原因。

译者对B-树和B+树的理解

我认为B-树和B+树最主要的区别在于非叶子节点是否存储数据:

  • B树: 非叶子节点和叶子节点都会存储数据
  • B+树: 只有叶子结点才会存储数据,非叶子节点存储键值。叶子节点之间使用双向指针连接,最底层的叶子节点之间形成了一个有序的双向链表。

想起之前看B站UP主颜群老师讲课的视频《SQL优化(MySQL版;不适合初学者,需有数据库基础)》, 在讲MySQL索引的数据结构的时候,说MySQL用的是B数,弹幕有人说,老师讲错了吧,应该是B+树,这种认知建立在没有准确理解B-树和B+树之间的联系上,B+树是B-树的变体,也就是说B+树在B-树之上固化了只有叶子节点才会存储数据,非叶子节点存储键值这个特定。在这个意义上,B+树是B-树的一个特例,倒是没那么多的差距,比如安卓与miui,miui也对安卓进行了深度定制,但是我们说miui是安卓系统也不错。MySQL的开发人员似乎也是这么认为的,认为MySQL普通索引的数据结构还是B-树的一个变体,可以取B+树,但是说是B-树也不能算错,这一点可以通过:

mysql 复制代码
// actor 是我在MySQL里面建的一张表,会输出这张表的索引信息,里面展示出了索引的数据结构是B树
show index  from  actor;

有些人的理解可能就是两个名字对应两个概念就是两个不同的事物,如果A概念是从B概念衍生而来,只是固化了B概念的一个特点,那么说A是B似乎也没什么问题,当我们讨论的再精确一些,讨论到具体的实现的时候,我们就可以创造一个概念专门为了讨论问题方便,这也就是B-树和B+树之间的关系。

翻译参考资料

1 LSM树详解 zhuanlan.zhihu.com/p/181498475

2 理解Mysql索引原理及特性 | 京东物流技术团队 juejin.cn/post/731162...

3 DT课堂-颜群的JAVA课 space.bilibili.com/326782142?s...

4 《SQL优化(MySQL版;不适合初学者,需有数据库基础)》 www.bilibili.com/video/BV1es...

相关推荐
爱勇宝6 小时前
第 1 章:别把“需求文档”当成真正的需求
前端·后端·程序员
IT_陈寒10 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码12 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo12 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志12 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..12 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects13 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰13 小时前
一些基础和问题解决
后端
陈随易13 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN13 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp