B树与B+树:从磁盘到内存的索引艺术

说实话,我第一次接触B树的时候,心里想的是:这不就是二叉搜索树的变种吗?有什么了不起的。直到我在一个嵌入式数据库项目中,发现用二叉搜索树做索引,查询速度慢得让人抓狂——磁盘I/O成了瓶颈。嗯,那时候我才真正理解B树的价值。

今天咱们就来聊聊B树和B+树。这两个结构,说白了就是为磁盘而生的大块头索引。你想想看,内存里跑红黑树很快,但数据一多,放磁盘上,每次读一个节点就要一次I/O,那二叉树的深度就成灾难了。B树的核心思想就是:一个节点多存几个键,把树压扁,减少I/O次数。

B树的定义与阶

B树是一棵多路平衡搜索树。它的核心参数是阶(order),记作m。我习惯把阶理解为“一个节点最多能有多少个孩子”。

具体规则是这样的:

  • 每个节点最多有m个孩子
  • 每个节点(除根外)至少有⌈m/2⌉个孩子
  • 根节点至少有两个孩子(除非它是叶子)
  • 所有叶子节点在同一层
  • 节点内的键值有序排列

举个例子,一棵5阶B树:

  • 每个节点最多4个键,5个孩子
  • 非根节点至少2个键(⌈5/2⌉ - 1 = 2)
  • 树的高度比二叉搜索树矮得多

关键点:B树的阶决定了树的宽度和高度。阶越大,树越矮,但每个节点占用的空间也越大。我在项目中一般选m=100到500之间,具体看磁盘块大小。

B树的查找与插入

查找其实很简单。从根开始,在节点内做二分查找,找到就返回;没找到就沿着对应的孩子指针往下走。直到叶子节点。

插入就有点意思了。我刚开始写B树插入时,踩过一个坑——忘记处理节点分裂。你想想看,一个节点满了(有m-1个键),再插入一个键怎么办?

答案是:分裂

  • 把节点从中间劈开,中间键上提到父节点
  • 左右两部分变成两个新节点
  • 如果父节点也满了,继续往上分裂
  • 最坏情况一直裂到根,树就长高一层
// 伪代码:B树插入核心逻辑
void btree_insert(Node* root, int key) {
    if (root->key_count == MAX_KEYS) {
        // 根节点满了,先分裂根
        Node* new_root = create_node();
        new_root->is_leaf = 0;
        new_root->children[0] = root;
        split_child(new_root, 0, root);
        insert_nonfull(new_root, key);
        return new_root;
    } else {
        insert_nonfull(root, key);
    }
}

避坑指南:我曾经在实现分裂时,忘记更新父节点的孩子指针数组,导致查找时直接越界。调试了整整一个下午。记住:分裂后,父节点的孩子数+1,键数+1,所有后续的孩子指针都要后移一位。

B+树的结构特点

B+树是B树的变种。说实话,实际工程中用B+树比B树多得多。为什么?因为B+树把数据全放在叶子节点,内部节点只存索引键。

结构特点:

  • 内部节点只存键值,不存数据指针
  • 所有数据记录都在叶子节点
  • 叶子节点之间用链表连接
  • 内部节点的键是“路由”作用,告诉你去哪个子树找

你想想看,这意味着什么?内部节点可以存更多的键,树更矮。而且叶子节点连成链表,范围查询就变得极其高效——找到起点后,顺着链表往后扫就行。

注意:B+树的插入和删除比B树稍微复杂一点,因为要维护叶子节点的链表。我建议你先实现B树,再改造成B+树,这样思路更清晰。

B树与B+树在数据库索引中的应用

数据库索引,说白了就是加速查询的数据结构。MySQL的InnoDB引擎用的就是B+树。为什么选B+树而不是B树?

特性 B树 B+树
内部节点存储 键+数据指针 仅键
叶子节点 分散存储 链表连接
范围查询 需要多次回溯 一次定位+顺序扫描
磁盘I/O次数 较多(节点含数据) 较少(节点更紧凑)
适用场景 点查询为主 范围查询+点查询

我在做嵌入式数据库时,遇到过一个问题:用B树做索引,每次查询都要从磁盘读一个完整的节点。如果节点里存了数据,那节点就大,一次I/O读的数据就少。换成B+树后,内部节点只存键,一个磁盘块能装几百个键,树的高度从4层降到了2层。查询速度提升了将近一倍。

嗯,这里有个细节:数据库的页大小通常是16KB。B+树的节点大小就按16KB来设计,保证一次I/O刚好读一个节点。这就是为什么B+树在数据库领域这么流行——它和磁盘的物理特性配合得天衣无缝。

总结一下我的经验:

  • 如果应用场景是点查询(比如根据ID查一条记录),B树和B+树差别不大
  • 如果涉及范围查询(比如查某段时间的所有记录),B+树完胜
  • 如果内存足够大,B树的缓存命中率可能更高,但磁盘场景下B+树更优
  • 实际工程中,90%的数据库索引都选B+树
B树与B+树核心知识结构 B树 • 多路平衡搜索树,阶m决定宽度 • 节点内键有序,所有叶子同层 • 插入:节点满则分裂,中间键上提 • 查找:从根到叶子,节点内二分 • 内部节点存键+数据指针 B+树 • B树变种,数据全在叶子节点 • 内部节点只存键,不存数据 • 叶子节点用链表连接 • 范围查询:定位起点后顺序扫描 • 节点更紧凑,I/O效率更高 数据库索引应用 • MySQL InnoDB 使用 B+树 作为聚簇索引 • 点查询:B树/B+树均可,B+树略优 • 范围查询:B+树完胜(链表顺序扫描) 核心:用空间换时间,用多路换深度,用I/O次数换查询速度

最后说一句:B树和B+树的实现代码量不小,我建议你从B树开始写,理解分裂和合并的逻辑后,再改造成B+树。别一上来就搞B+树,容易把自己绕晕。


公众号:蓝海资料掘金营,微信deep3321