数据库基础-B+树

查询类型

  1. 全表扫描,不提供索引,扫描所有集合中的数据。
  2. 根据指定key值查询指定点
  3. 范围查询,在指定区间内查询

有很多方法能够进行快速扫面数据,但是再快复杂度也是O(N),我们的目标是想办法将查询复杂度降低到O(logN)。

A range query consists of 2 phases:

  1. Seek: find the starting key.
  2. Iterate: find the previous/next key in sorted order.

哈希表

在讨论如何构建哈希表(hashtable),尤其是内存中的哈希表时,确实存在一些挑战。尽管如此,相较于之后会编写的B树,编写哈希表还是相对简单的。以下是一些需要考虑的问题和解决方案:

  1. 如何扩展哈希表? 当负载因子过高时,意味着当前哈希表的容量不足以有效地处理更多的键值对,这时就需要将键迁移到一个更大的哈希表中。如果一次性移动所有数据,其时间复杂度为Ο(Ν),这在实际应用中是非常不理想的。因此,重新散列(rehashing)必须渐进式地进行,即使是像Redis这样的内存应用程序也应如此。一种常见的做法是在每次插入新键值对时,随机选择一定比例的旧键值对进行重散列,直到所有键都被迁移至新的哈希表。

  2. 其他提及的事项:

    • 就地更新(In-place updates): 这指的是直接在现有存储位置上更新键值对,而不是删除再插入。这对于保持哈希表的性能非常重要。
    • 空间复用(Space reuse): 有效管理哈希表的空间使用情况,包括回收被删除键值对占用的空间,以减少内存碎片并提高内存使用效率。

排序数组

讨论完哈希表后,让我们转向最简单的排序数据结构:排序数组(sorted array)。对于如字符串这样的可变长度数据(键值对),可以使用指针(或偏移量)数组来进行二分查找,其时间复杂度为Ο(log Ν)。

然而,更新排序数组的成本是Ο(Ν),无论是否就地更新。这意味着直接在排序数组中进行插入和删除操作并不是非常实用,尤其是在需要频繁更新的场景下。不过,这种结构可以通过一些方式扩展成更复杂的、支持高效更新的数据结构。

一种减少更新成本的方法是将数组分割成几个较小的、互不重叠的数组------即嵌套排序数组。这种扩展最终可以发展为B+树(多级n叉树),但同时也带来了维护这些小数组(即树节点)的额外挑战。

另一种形式的"可更新数组"是日志结构合并树(Log-Structured Merge Tree, LSM树)。在这种结构中,更新首先被缓冲在一个较小的数组(或其他排序数据结构)中,当这个数组变得过大时,再与主数组合并。通过将较小数组逐步合并到较大数组中的方式,摊销了更新成本。

LSM树的设计旨在优化写入密集型应用场景,它通过对数据进行批量处理来提高效率,尽管这可能会以增加读取操作的复杂性为代价。这种方法非常适合于需要处理大量写入操作的应用程序,例如数据库系统。通过这种方式,不仅解决了原始排序数组更新成本高的问题,还提供了一种有效管理大规模数据集的方法。

MVCC也是为了应对写密集型

B树

B树是一种平衡的n叉树,类似于平衡二叉树。每个节点可以存储多达𝑛个键(和分支),其中𝑛大于2。B树的设计主要是为了减少磁盘随机访问次数,从而提高查询效率。

减少随机访问次数

由于磁盘每秒只能执行有限数量的IO操作(IOPS),这是查找操作的主要限制因素。在进行查找时,树的每一层都对应一次磁盘读取。对于相同数量的键,n叉树比二叉树更短(log𝑛 𝑁对比log₂ 𝑁),因此使用n叉树可以减少每次查找所需的磁盘读取次数。

选择𝑛值时存在权衡:

  • 较大的𝑛值意味着每次查找需要的磁盘读取次数较少,这能改善延迟和吞吐量。
  • 但是,较大的𝑛值也意味着节点更大,更新速度较慢(这将在后面讨论)。

磁盘IO的基本单位是页面

虽然你可以从文件的任意偏移量读取任意数量的字节,但磁盘并不是这样工作的。磁盘IO的基本单位不是字节,而是扇区,在旧的HDD上,扇区是512字节的连续块。

然而,应用程序并不直接关心磁盘扇区,因为常规文件IO不直接与磁盘交互。操作系统会在页缓存中缓存/缓冲磁盘读写,页缓存由称为"页"的4KB内存块组成。

无论如何,存在一个最小的IO单位。数据库也可以定义自己的IO单位(也称为"页"),其大小可能超过操作系统的页大小。

最小IO单位意味着树节点应该以该单位的倍数分配;如果一个单位只使用了一半,则另一半就浪费了IO资源。这也是反对使用较小的𝑛值的另一个原因!

通过确保树节点的大小为IO基本单位的整数倍,可以最大化利用每一次IO操作,减少因未充分利用IO单位而造成的浪费。这对于优化数据库性能至关重要,尤其是在处理大量数据和高并发场景时。此外,考虑到节点大小对更新性能的影响,合理选择𝑛值可以在查询性能和更新成本之间取得平衡。

B+树的优势

在数据库的上下文中,提到的B树通常指的是其一种变体------B+树。B+树的特点是内部节点不存储实际值,只存储键和指向子节点的指针;所有的值仅存在于叶节点中。这种设计使得B+树的内部节点可以容纳更多的分支,从而导致树的高度更矮、结构更扁平。

  • 缩短树的高度:由于内部节点无需存储数据值,它们可以使用更多的空间来存储键和指针,这允许每个节点包含更多的子节点,从而降低了树的高度。
  • 内存中的B+树:即使作为内存中的数据结构,B+树依然有意义。例如,在RAM与CPU缓存之间,最小的IO单位是64字节(缓存行)。尽管在这种情况下性能提升不如磁盘上的那么显著,因为64字节能容纳的数据量有限,但优化仍然有效。

数据结构的空间开销

二叉树在实际应用中往往不太实用的一个原因是其大量的指针开销。对于每个键,至少需要一个来自父节点的指针。而在B+树中,多个键共享一个来自父节点的指针,这样就大大减少了指针的数量和因此带来的空间开销。

此外,B+树的叶节点中的键还可以以紧凑格式存储或进行压缩,以进一步减少空间占用。这种方式不仅节省了存储空间,也提高了缓存的命中率,进而提升了访问效率。通过这些优化措施,B+树能够在保证高效查询的同时,也能够有效地管理空间使用,使其成为许多数据库系统底层存储结构的首选。

日志结构存储(Log-structured Storage)

Update by merge: amortize cost

日志结构存储的核心思想是通过"合并"操作来摊销更新成本,其中最典型的例子是日志结构合并树(LSM-tree) 。尽管名字中包含"日志"和"树",但其核心机制实际上是合并

基本原理:双文件系统

我们从两个文件开始:

  1. 小文件:用于存储最近的更新。
  2. 大文件:用于存储其余的数据。

更新首先写入小文件,但小文件不能无限增长。当小文件达到某个阈值时,它会被合并到大文件中。

复制代码
writes => | 新更新 | => | 积累的数据 |
            file 1         file 2

合并操作会生成一个新的、更大的文件,用以替换旧的大文件,同时清空小文件。合并的时间复杂度为𝑂(𝑁),但由于合并可以与读写操作并发执行,因此不会显著影响性能。


多级合并:减少写放大效应

缓冲更新比每次重写整个数据集更高效,但如果我们将这种机制扩展到多个层级,效果会更好:

复制代码
              | 第1层 |
                 ||
                 \/
          |------第2层------|
                  ||
                  \/
|-----------------第3层-----------------|

在两级方案中,每当小文件达到阈值时,大文件都会被重写。这种额外的磁盘写入称为写放大效应(write amplification)。随着大文件的增长,写放大效应会变得更加严重。

通过引入更多层级,我们可以将第2层保持较小规模,将其合并到第3层,类似于如何保持第1层的小规模。直观上,每一层的大小呈指数增长(例如,第2层是第1层的两倍,第3层是第2层的两倍),并且将规模相似的层级进行合并可以最小化写放大效应。


写放大效应与层级数量的权衡

虽然增加层级数量可以有效减少写放大效应,但这也会对查询性能产生负面影响:

  • 更多的层级意味着查询时可能需要访问更多的文件,从而增加了查询的复杂性。
  • 更少的层级则可能导致更高的写放大效应。

因此,在设计LSM-tree时,需要在写放大效应查询性能之间找到平衡点。


总结

日志结构存储(如LSM-tree)通过合并操作摊销了更新成本,并通过多级结构进一步优化了写放大效应。其关键特点包括:

  1. 高效的写入性能:更新被缓冲在小文件中,避免频繁重写整个数据集。
  2. 可控的写放大效应:通过多级合并,减少不必要的磁盘写入。
  3. 灵活性:可以根据具体应用场景调整层级数量,权衡写放大效应和查询性能。

LSM-tree 索引

在 LSM-tree 中,每一层都包含索引数据结构。由于除第 1 层外的所有层都不会被直接更新,因此一个简单的选择是使用排序数组作为索引。然而,二分查找在随机访问上的性能优势并不比二叉树显著,因此更合理的做法是在每一层中使用 B 树,这就是 LSM-tree 名字中的"树"部分。

无论如何,由于缺乏直接的更新操作,数据结构的设计变得更加简单。


合并的思想与哈希表的类比

为了更好地理解"合并"的思想,可以尝试将其应用于哈希表。这种设计被称为日志结构哈希表(log-structured hashtable)。尽管实现方式不同,但其核心思想仍然是通过缓冲和合并来优化写入性能。


LSM-tree 查询

在 LSM-tree 中,键可能存在于任何层级中,因此查询时需要将所有层级的结果结合起来:

  • 范围查询:采用多路归并(n-way merge)。
  • 点查询:可以通过布隆过滤器(Bloom filters)优化,减少需要搜索的层级数量。

由于旧层级不会被更新,因此可能存在旧版本的键,而删除的键会在新层级中标记为特殊标志(称为墓碑,tombstone)。因此,在查询时,新层级具有优先级。

合并过程会自然地回收旧键或已删除键占用的空间,这个过程也被称为压缩(compaction)


实际应用中的 LSM-tree:SSTable、MemTable 和日志

这些术语描述了 LSM-tree 的实现细节。虽然从原理上构建 LSM-tree 不需要了解它们,但它们确实解决了一些实际问题。

  1. SSTable(Sorted String Table)

    每一层被分成多个互不重叠的文件,而不是一个大文件。这样做的好处包括:

    • 合并操作可以逐步进行,避免一次性合并大文件所需的大量空闲空间。
    • 合并过程可以分散到时间上,降低峰值资源需求。
  2. 日志(Log)

    第 1 层可以直接更新,但由于其大小有限,使用日志是一个合理的选择。这是 LSM-tree 名字中的"日志"部分,展示了如何将日志与其他索引数据结构结合使用。

  3. MemTable

    即使日志很小,仍然需要一个适当的索引数据结构来加速查询。日志数据会被复制到内存中的索引结构,称为 MemTable。MemTable 可以是 B 树、跳表(skiplist)或其他结构。它是一个小而有界的内存数据结构,能够加速对最近更新的读取场景。


索引数据结构的总结

目前有两种主要的索引数据结构选择:

  1. B+树

    B+树适用于需要频繁更新的场景,但在磁盘上的更新效率较低,并且空间复用的问题需要额外处理。

  2. LSM-tree

    LSM-tree 解决了许多上一章提到的挑战,例如如何高效更新基于磁盘的数据结构以及如何复用空间。虽然这些问题对于 B+树依然存在


总结对比

特性 B+树 LSM-tree
更新效率 较低(每次更新需维护树结构) 高效(通过合并摊销成本)
查询效率 快速(单次查询即可找到目标) 较慢(需多层查询与合并)
空间复用 需要额外机制 自然支持(通过压缩回收空间)
适用场景 频繁读取与少量更新 写密集型工作负载

通过理解这两种数据结构的特点,可以根据具体应用场景选择最适合的解决方案。

相关推荐
rchmin8 分钟前
向量数据库Milvus安装及使用实战经验分享
数据库·milvus
ego.iblacat14 分钟前
Python 连接 MySQL 数据库
数据库·python·mysql
祖传F8725 分钟前
quickbi数据集数据查询时间字段显示正确,仪表板不显示
数据库·sql·阿里云
Leon-Ning Liu1 小时前
Oracle 26ai新特性:时区、表空间、审计方面的新特性
数据库·oracle
humors2211 小时前
各厂商工具包网址
java·数据库·python·华为·sdk·苹果·工具包
Yushan Bai2 小时前
ORACLE数据库在进行DROP TABLE时失败报错ORA-00604问题的分析处理
数据库·oracle
77美式2 小时前
Node + Express + MongoDB 后端部署全解析:新手零踩坑
数据库·mongodb·express
城数派2 小时前
2000-2025年我国省市县三级逐8天日间地表温度数据(Shp/Excel格式)
数据库·arcgis·信息可视化·数据分析·excel
AC赳赳老秦2 小时前
OpenClaw text-translate技能:多语言批量翻译,解决跨境工作沟通难题
大数据·运维·数据库·人工智能·python·deepseek·openclaw
AI应用实战 | RE2 小时前
014、索引高级实战:当单一向量库不够用的时候
数据库·人工智能·langchain