好久没翻译文章了,感觉自己的英文功底有点下降,前几篇文章都着眼于网络I/O了,这篇文章之后我们开始看点数据库、数据结构之类的内容。
原文链接: https://blog.allegro.tech/2023/11/how-does-btree-make-your-qu...
已经征得作者的同意可以进行翻译。
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-树需要不断更新新值。
树的形状取决于添加进入树的值的顺序,这在二叉树中很容易看到,如果数值添加顺序不正确,我们可能会得到不同深度的树。
当树在不同节点上具有不同的深度时,它被称为不平衡树,有两种方式可以将这样的树恢复到平衡状态。
- 重新构建这棵树,按照正确的顺序添加值。
- 在添加新值的时候同时保持平衡。
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-树也不能算错,这一点可以通过:
// actor 是我在MySQL里面建的一张表,会输出这张表的索引信息,里面展示出了索引的数据结构是B树
show index from actor;
有些人的理解可能就是两个名字对应两个概念就是两个不同的事物,如果A概念是从B概念衍生而来,只是固化了B概念的一个特点,那么说A是B似乎也没什么问题,当我们讨论的再精确一些,讨论到具体的实现的时候,我们就可以创造一个概念专门为了讨论问题方便,这也就是B-树和B+树之间的关系。
翻译参考资料
[1] LSM树详解 https://zhuanlan.zhihu.com/p/181498475
[2] 理解Mysql索引原理及特性 | 京东物流技术团队 https://juejin.cn/post/7311623433817620517#heading-5
[3] DT课堂-颜群的JAVA课 https://space.bilibili.com/326782142?spm_id_from=333.337.sear...
[4] 《SQL优化(MySQL版;不适合初学者,需有数据库基础)》 https://www.bilibili.com/video/BV1es411u7we/?spm_id_from=333....
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。