Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SortedList 与 B+树 #670

Open
981377660LMT opened this issue Dec 21, 2024 · 0 comments
Open

SortedList 与 B+树 #670

981377660LMT opened this issue Dec 21, 2024 · 0 comments

Comments

@981377660LMT
Copy link
Owner

SortedList的本质是一个高度为2的B+树

整体结构设计思路来看,sortedcontainers.SortedList 确实有些类似于「高度只有 2 级的 B+ 树」,因为它也采用了块(chunk)的概念,把大量元素分散到多个小的有序块中,并在顶层维护一个对所有块的索引,从而在查找插入/删除时都可以借助“先找到合适的块,再在块内做操作”的思路。这跟 B+ 树“先在内部节点查关键字区间,再去叶子节点找具体元素”的做法看起来非常相似。

不过,要说“SortedList 的本质就是一个高度为 2 的 B+ 树”也需要做几点区分和补充:


1. SortedList 的核心实现思路

根据 sortedcontainers 项目的文档及其源码,SortedList 内部主要是一个 top-level 列表 + 多个小的已排序列表(runs/chunks)

  1. 顶层: 维护了一个数组(或列表)来管理所有块,对这些块的“范围”进行元信息存储,比如每个块的最小/最大值、块的大小等。
  2. 底层: 每个块本身也是一个 Python 列表,里面的元素是有序的。

在执行搜索时,先用“块”的范围做一次近似查找——类似在“内部节点”快速判断应该去哪个块;然后进入具体的块,用二分或其它方法找到目标元素。如果块过大或过小,会进行分裂或合并,保证每个块的大小在一个相对平衡的范围内。

从这个角度看,SortedList 与传统 B+ 树中“内部节点 + 叶子节点”的结构很像:

  • 顶层“块索引”可以类比成 B+ 树的内部节点
  • “具体块”可以类比成 B+ 树的叶子节点

它之所以可以被称为“高度为 2 的 B+ 树”,是因为绝大多数情况下只有这两层——不会再往下分出更多层级。元素都实实在在地存放在第二层的多个块当中,而不是进一步的子节点里。


2. 与传统 B+ 树仍存在差异

  1. 高度的可扩展性

    • 传统 B+ 树如果数据量继续增大,每个节点再分裂,就会动态增加树的高度(当然通常也就 3~5 层左右,对数据库来说已经够用)。
    • SortedList 里的块虽然也可能进一步分裂,但它依旧不会在顶层之下再增加一个“块的块”的层次;除非手动去实现多层结构,否则基本就是两层。
  2. 具体的分裂/合并策略

    • B+ 树会严格保证每个节点的键数量介于 [t-1, 2t-1](以最小度数 t 为例),并在插入/删除时进行“节点拆分/合并”。
    • SortedList 则是根据一个“目标块大小”或“平衡参数”来管理块的拆分合并,并没有像 B+ 树那样固定的阶(branching factor)。
  3. 节点(块)中的数据结构

    • B+ 树叶子节点通常也能用数组来存储有序键,并可通过指针连接各叶子节点。
    • SortedList 则直接用 Python list 存储,再配合一些优化技巧(如在插入时延迟拷贝、批量操作等),以适应 Python 语言特性。
  4. 应用场景

    • 传统 B+ 树更多是磁盘/SSD 场景,一层节点就是一次页面或块的 I/O,设计核心在于减少随机 I/O。
    • SortedList 更偏内存场景,它没有专门去做磁盘页对齐、块级缓存等。

3. 怎么看 SortedList “高度=2 的 B+ 树” 这种说法

  • 类比是可以的:从分块、索引、叶子的结构确实非常像 B+ 树的一种特例(只有“根节点 + 叶子节点”两层)。
  • 并不严格等价:SortedList 的拆分/合并策略、底层存储方式、适用场景与传统数据库的 B+ 树仍有不少差异;理论上如果块再继续分块,就可以变成“多层 B+ 树”,但 SortedList 默认只实现了两层。

一句话总结

SortedList 可以类比成一个只有两层的 B+ 树(顶层索引 + 底层块),它的目标是充分利用 Python list 的局部性和内置操作,以在内存中高效地维护有序序列。和真正的 B+ 树概念相比,SortedList 更加轻量化,并没有扩展到多层、高并发或磁盘优化等场景。

4. 为什么B+Tree不用两层高度

理论上讲,如果数据量足够小,B+Tree 的高度确实可以是 2,即“根节点 + 叶子节点”两层结构(与 Python SortedList 的块索引有些类似);然而在实际数据库大规模存储中,往往需要存储海量数据,两层高度并不足以应对数据量的增长和复杂的操作需求。下面主要从几个方面解释为什么 B+Tree 通常需要多层,而不仅仅是两层。


1. 容量限制

假设我们有一颗“2 层”的 B+Tree,其中:

  • 根节点是唯一的“内部节点”;
  • 其所有子节点都是叶子节点,存放实际记录(数据)。

在 B+Tree 中,叶子节点能存放的记录数是有上限的。若用 m 表示节点的最大子节点数,则:

  1. 根节点最多能容纳 m - 1 个索引键,并指向 m 个叶子节点。
  2. 每个叶子节点若能容纳最多 L 条记录,则整棵树最多能容纳 m * L 条记录(忽略边界上的 (\pm 1) 做近似计算)。

当数据库规模上升到几亿、几十亿甚至更高量级时:

  • 两层高度就需要极其庞大的 m 或非常巨大的叶子节点大小才能容纳所有记录。
  • 节点过大或过宽会导致单次更新、分裂等操作代价过高,并且在实际磁盘页/块的设计下,不可能无限制增大一个节点对应的物理块大小

因此,为了让结构能弹性扩展、在磁盘页大小固定的情况下也能容纳更多数据,B+Tree 必须允许自己增加“中间层”,从而在合理的节点扇出 (branching factor) 下保存海量数据。


2. 减少磁盘 I/O

数据库使用 B+Tree 的一个重要目的,就是减少磁盘随机访问带来的高成本。

  • 多层 B+Tree

    • 高度通常只有 3~5 层,就能表示几百万到上亿数量级的数据。
    • 在查找时,从根节点到叶子节点,最多只需要访问 3~5 个节点(即 3~5 次磁盘读取)。
  • 2 层 B+Tree(若想容纳海量数据)

    • 只能通过极高的扇出 m 或极大的叶子节点大小来覆盖所有记录。
    • 一旦扇出极大,一个节点占据的磁盘页/块数就会增多,还可能带来“热点”问题(所有操作都在同一个根节点或叶子节点上集中读写)。
    • 同时,插入、删除、分裂一个如此庞大的节点,也变得低效且复杂。

因此,允许 B+Tree 在适当大小的节点扇出(比如几百到几千不等)下多增一两层,反而能保证在大数据量下查询/插入都只需很少的磁盘访问。


3. 分裂、合并和动态增长

B+Tree 的插入和删除都可能触发节点的分裂或合并,而这些操作一般只会影响局部节点,不需要全树大范围移动数据。

  • 节点满了要分裂时,如果只有 2 层高度,那么根节点一旦也满了,就会很容易遭遇瓶颈;数据库需要不断地扩大根节点才能应对新数据。
  • 多层结构能将分裂的影响在局部范围内消化掉,且仅在必要时才增加树高。

4. 并发与锁粒度

数据库一般都有高并发需求,需要在多线程或分布式场景下对同一棵 B+Tree 进行读写操作。

  • 若只有 2 层,高度过低且节点过宽,并发冲突(如锁定整页)会极其频繁,影响吞吐量。
  • 多层 B+Tree 可以对不同分支的叶子节点及中间节点分别加锁,从而减少冲突、提高并行度。

5. 小结

  • “2 层 B+Tree” 只能在数据量相对较小单机内存应用中才可能简单可行。但真正的数据库往往面向海量数据,需要节点分裂合并、并发控制、磁盘读取优化等多方面的考虑。
  • 多层 B+Tree(常见高度 3~5 层)能够在不牺牲节点扇出的前提下,弹性容纳海量数据,保持较低的树高,减少磁盘 I/O,并为并发提供更好的锁粒度。

因此,对于数据库这类大规模持久化系统而言,B+Tree 通常会有多层结构,而不仅仅是两层。这样才能在节点大小和磁盘页大小间取得平衡,又能保持高效的插入、删除和查询性能。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant