2026-10-25 索引为什么是 B+ 树:自己实现一棵量了一下

数据库课讲索引必提 B+ 树,理由通常是"矮"和"适合范围查询"。我自己写了一棵 B+ 树,再写一棵普通二叉搜索树,用同一个指标比:一次查询要读几个页。

一、先把参数定死

比之前得先说清楚假设,不然后面的数没意义。

InnoDB 一个页是 16KB。主键是 bigint,占 8 字节;页内指向子节点的指针 6 字节。所以一个索引项 14 字节,一页能放 16384 / 14 = 1170 项。

这个 1170 就是扇出:一个内部节点最多有 1170 个孩子。

二、自己实现一棵

B+ 树和普通树的区别不在查找,在分裂。叶子节点满了,就劈成两半,把中间那个 key 提上去当分隔符;内部节点满了,同样劈开,再把中间 key 往上提。提到根还满,就长出新根。

// 叶子分裂:取中间位置劈开,右半部分的第一个 key 上提
int mid = leaf.n / 2;
int m = leaf.n - mid;
System.arraycopy(leaf.k, mid, right.k, 0, m);
right.n = m;
leaf.n = mid;
right.next = leaf.next;   // 叶子之间用 next 串起来,这一步是范围查询的关键
leaf.next = right;
return new Object[]{right.k[0], right};

一共不到两百行。插 100 万条主键 1 到 100 万,模拟自增主键。

三、量出来的数

参数:页 16384 字节,索引项 14 字节,扇出 1170
数据量:1000000 条

=== B+ 树 ===
树高(层)       = 3
占用页数         = 1712
平均每叶节点存   = 585 条
建树耗时         = 29 ms

点查 1000 次,平均读页 = 3.0
范围查 100 行,平均读页 = 3.16

=== 二叉搜索树(顺序插入 50000 条)===
树高(层)     = 50000
建树耗时       = 1766 ms

=== 二叉搜索树(随机插入)===
树高(层)     = 47
建树耗时       = 183 ms

四、这三个数各自说明什么

第一,100 万行只占 3 层。

树高 3,意味着任何一次点查最多读 3 个页。1712 页乘以 16KB 是 26.7 MB——这就是 100 万行的索引体积。

按扇出 1170 往上推,4 层能存 1170³ ≈ 16 亿 行。所以哪怕数据涨到十几亿,点查还是 4 次 I/O。这是"矮"的真正含义:它不是一个形容词,是一个上限。

第二,同一个问题,BST 要读 47 个页。

随机插入 100 万条,BST 树高 47。按 SSD 随机读 0.1ms 估,B+ 树 0.3ms,BST 4.7ms,差 16 倍。

而这还只是 SSD。要是机械盘,一次随机读 5-10ms,47 次就是半秒——这就是"树高等于 I/O 次数"这件事的分量。比较次数反而不是瓶颈,CPU 一秒能比几千万次。

第三,顺序插入的 BST 直接退化成链表。

我插了 5 万条自增主键,树高正好 50000——和条数一模一样,每个节点只有一个右孩子。

建树还花了 1766ms,因为顺序插入是 O(n²),每插一条都要从头走到尾。这也顺便解释了我为什么只敢测 5 万条:100 万条要跑几个小时。

真实场景里自增主键恰恰就是顺序插入。所以"用二叉搜索树做索引"在自增主键上不只是慢,是直接坏掉。

五、范围查询凭什么快

B+ 树的叶子节点之间用 next 指针串成一条链。查 WHERE id BETWEEN 1000 AND 1100,先走 3 层定位到 1000 所在的叶子,然后顺着链表读 100 条就完了——实测平均 3.16 个页。

普通 BST 没有这条链。找到下界之后,要回到父节点往上走、再往右下去,每取一条都要上下折腾。要用好几次 I/O 才能凑够 100 条连续记录。

这就是"叶子节点连成链表"这个设计的意义:它把范围查询从"多次点查"变成"一次顺序读"。

六、结论

索引选 B+ 树,不是因为它查询算法更聪明——点查的比较次数它并不比 BST 少。原因是三件事叠在一起:

扇出 1170 让树高被压到 3 层,点查的 I/O 次数有确定上限;叶子链表让范围查询变成顺序扫描;插入分裂是自平衡的,不会因为主键有序而退化。

反过来看,这三个优点分别对应着三种如果不用 B+ 树就会踩的坑:树太高、范围查询要回头、有序插入退化。自己写完一遍之后,这三个坑我都踩过了。

最后说清楚边界:这是我按 InnoDB 参数(16KB 页、14 字节索引项)自己实现的简化 B+ 树,不是 InnoDB 本身。真实实现还有页头页尾开销、填充因子、碎片整理,扇出会比 1170 略小。但量级和结论是一致的。

附:核心代码

public class BPlusTreeVsBst {

    static final int PAGE_BYTES  = 16384;
    static final int ENTRY_BYTES = 8 + 6;
    static final int ORDER       = PAGE_BYTES / ENTRY_BYTES;   // 1170

    static final class Leaf extends Node {
        final long[] k = new long[ORDER + 2];
        int n;
        Leaf next;
    }

    static final class Inner extends Node {
        final long[] k = new long[ORDER + 2];
        final Node[] c = new Node[ORDER + 3];
        int n;
    }

    /** 返回 null 表示没分裂;否则返回 {上提的 key, 新右节点} */
    static Object[] insert(Node node, long key) {
        if (node instanceof Leaf leaf) {
            int i = lowerBound(leaf.k, leaf.n, key);
            System.arraycopy(leaf.k, i, leaf.k, i + 1, leaf.n - i);
            leaf.k[i] = key;
            leaf.n++;
            if (leaf.n <= ORDER) return null;

            Leaf right = new Leaf();
            int mid = leaf.n / 2, m = leaf.n - mid;
            System.arraycopy(leaf.k, mid, right.k, 0, m);
            right.n = m;
            leaf.n = mid;
            right.next = leaf.next;
            leaf.next = right;
            return new Object[]{right.k[0], right};
        }

        Inner in = (Inner) node;
        int i = upperBound(in.k, in.n, key);
        Object[] split = insert(in.c[i], key);
        if (split == null) return null;

        long upKey = (Long) split[0];
        Node rightChild = (Node) split[1];
        System.arraycopy(in.k, i, in.k, i + 1, in.n - i);
        in.k[i] = upKey;
        System.arraycopy(in.c, i + 1, in.c, i + 2, in.n - i);
        in.c[i + 1] = rightChild;
        in.n++;
        if (in.n <= ORDER) return null;

        Inner rightNode = new Inner();
        int mid = in.n / 2, m = in.n - mid - 1;
        long up = in.k[mid];
        System.arraycopy(in.k, mid + 1, rightNode.k, 0, m);
        System.arraycopy(in.c, mid + 1, rightNode.c, 0, m + 1);
        rightNode.n = m;
        in.n = mid;
        return new Object[]{up, rightNode};
    }
}
posted @ 2026-10-25 14:28  呱哒  阅读(3)  评论(0)    收藏  举报