B+ 树:
在十亿行里,一步命中
如果 SQL 是“我要什么”,B+ 树就是数据库回答“我怎么少翻页”的第一把刀。它不是一棵普通树,而是一台为磁盘页、范围扫描和更新稳定性定制的查找机器。
5.1问题不是“会不会找”,
而是“翻几页”
数据库系统里,很多看似高级的概念,最后都会落到一个很朴素的问题:为了回答这个查询,我要读多少个 page?如果没有索引,查 WHERE id = 42 只能扫表。表有十亿行,就像在一栋楼里逐间敲门问“你是不是 42”。
B+ 树索引做的事情,是给这栋楼装一套目录:先看楼层,再看门牌区间,最后只敲很少几扇门。它的关键不在“树”这个形状,而在 每个节点正好是一页,节点里塞很多 key,树因此非常矮。
B+ 树把一次查找变成:读 root page → 读几个 internal page → 读目标 leaf page。如果 fan-out 是 250,三四层就能覆盖上亿到十亿量级的记录。它赢的不是算法课里的 O(log n),而是系统课里的 3 次 I/O,对比 100 万次 I/O。
5.2先记住这 5 条 invariant
你不需要把所有 case 背下来。真正要抓住的是 invariant:只要它们成立,查找、插入、删除都只是为了维护这些约束。
Internal node 只做导航,不存 RID payload。
范围查询不用回到 root,可以顺着 sibling pointer 扫。
任何 key 的查找路径长度一样,延迟稳定。
除了 root,节点不能太空,否则空间和 fan-out 都会崩。
遇到相等 key,向右走,才能命中正确叶子。
5.3沙盒:插入、分裂、搜索、
范围扫描、删除
下面这个模型采用 CS186 常用的 order d 记法:非 root 节点最少 d 个 key,最多 2d 个 key。试着连续插入 10, 20, 5, 6, 12, 30, 7, 17,观察第一次 leaf split 如何把 separator 复制到 parent。
深潜 为什么 separator 是复制,不是搬走?›
叶子里的 key 是真实索引项的一部分,不能因为提升到 parent 就从 leaf 里删掉。Leaf split 后,右叶子的第一个 key 会被复制到 parent,作为导航边界。Internal split 不一样:中间 separator 本来就是导航 key,可以被推上去。
这就是 B+ tree 和很多教科书里的 B-tree 最容易混的地方:B+ tree 的数据全在 leaf,internal node 是路牌。
5.4查找路径,就是页 I/O
算法课会说 B+ 树查找是 O(log_f N)。数据库系统里,我们更关心这句话的物理版本:一次 point lookup 要读树高这么多页。如果 root 常驻 buffer pool,成本还可以再少一页。
范围查询则多了一个尾巴:先沿树高找到第一个 leaf,再顺着 leaf sibling chain 扫到范围结束。B+ 树非常适合 BETWEEN、排序输出和索引嵌套循环连接,原因就在这条 leaf chain。
Point lookup: height 次 page read,常见情况下 root 被缓存,实际更接近 height - 1。
Range scan: height + leaf_pages_touched,如果需要回表取记录,还要加 data page I/O。
Insert/delete: 先 lookup,再修改 leaf。只有节点满/太空时,才触发 split/borrow/merge 向上传播。
5.5Fan-out:为什么十亿行也只有几层
B+ 树真正的系统魔法是 fan-out。一个 4KB page 里,如果每个 separator key 加 child pointer 只占几十字节,一个 internal page 就能指向几百个孩子。高度不是按二叉树的 2 取 log,而是按几百取 log。
5.6现代视角:B+ 树不是唯一答案
CS186 先讲 B+ 树,是因为它是传统 OLTP 数据库的核心索引结构。但现代存储系统里,你会经常遇到另一个选择:LSM tree。B+ 树擅长读和范围扫描,更新时原地修改页;LSM 把写入先追加到内存和日志,再批量刷成有序文件,用 compaction 慢慢整理。
5.7下一圈螺旋:把索引接回查询执行
学到这里,B+ 树不再是一道数据结构题。它会重新出现在后面的几章里:
- Join: Index Nested Loop Join 的代价,取决于内表索引 lookup 有多便宜。
- Optimizer: 选择 seq scan 还是 index scan,本质是估算 selectivity 和 I/O。
- Transactions: 并发更新 B+ 树,需要 latch coupling 和 predicate/next-key locks。
- Recovery: split 修改多个 page,必须能被 WAL redo/undo 正确恢复。
- Modern systems: RocksDB、Bigtable、DynamoDB 背后很多设计都在重分配 B+Tree 和 LSM 的 trade-off。
本章压缩包
- B+ 树是为了减少 page I/O,不是为了炫耀树结构。
- Internal node 是导航,leaf 才有真实索引项;leaf chain 让 range scan 便宜。
- Order d 的 split/borrow/merge 都是在维护“半满、平衡、排序”的 invariant。
- Fan-out 很大,所以树很矮;树高直接变成 lookup 的 I/O cost。
- B+ 树和 LSM 是读写 trade-off 的两端,不是新旧优劣关系。