【java-core-collections】B+ 树深度解析

B+ 树深度解析

一、什么是 B+ 树

B+ 树是一种多路平衡查找树,是 B 树的变体。所有数据都存储在叶子节点,内部节点只存索引(键值),叶子节点通过链表串联。

核心思想 :降低树高,减少磁盘 I/O 次数。一棵 3 层 B+ 树(每页 1000 个键)可索引 10 亿 条记录。

二、B+ 树 vs B 树

特性 B 树 B+ 树
数据存储位置 所有节点都存数据 只有叶子节点存数据
叶子节点链表 有(双向链表)
查找效率 不稳定(可能在非叶节点命中) 稳定(必须到叶子节点)
范围查询 需要中序遍历 顺着链表直接扫描
内部节点空间 存数据,索引少 只存索引,能容纳更多键
复制代码
B+ 树结构(3 阶示例):

            [30 | 70]              ← 内部节点(只存索引)
           /    |     \
      [10,20] [30,50] [70,80,90]   ← 叶子节点(存数据)
        ↔       ↔        ↔        ← 双向链表连接

三、B+ 树核心参数

参数 MySQL InnoDB 默认值 说明
页大小 16 KB 磁盘读写的基本单位
阶数(m) ~1200 非叶节点最多 m 个子节点
键数量 非叶节点 m-1 个键 叶子节点存实际数据
最小度数(t) t ≥ 2 每个节点至少 t-1 个键

树高与容量的关系(假设每个键+指针占 12 字节):

复制代码
一页 16KB = 16384 字节
非叶节点可存: 16384 / 12 ≈ 1365 个键
叶子节点(存数据,假设每条 1KB): 16 条

层数   可索引记录数
1层:   16
2层:   1365 × 16 ≈ 2 万
3层:   1365 × 1365 × 16 ≈ 3000 万
4层:   1365³ × 16 ≈ 400 亿

MySQL 为什么用 B+ 树? 3~4 层即可索引海量数据,每次查找只需 3~4 次磁盘 I/O。

四、核心操作

4.1 查找

复制代码
查找 key = 50:

1. 根节点 [30 | 70]: 50 ∈ [30, 70) → 走中间指针
2. 中间节点 [30 | 50]: 50 = 50 → 走右边
3. 叶子节点 [40, 50, 60]: 找到 50 ✓

等值查找: O(log_m N),m 为阶数

4.2 范围查询

复制代码
查询范围 [30, 70):

1. 先查找 30 → 定位到叶子节点
2. 沿链表向右扫描: 30 → 40 → 50 → 60 → 停(70 不含)
3. 只需一次树查找 + 顺序扫描

这就是 B+ 树的优势:范围查询极快

4.3 插入

规则:节点满了(> m-1 个键)就分裂

复制代码
插入 25(假设 3 阶 B+ 树,每个节点最多 2 个键):

1. 找到叶子节点 [20, 30],插入 25 → [20, 25, 30] 溢出!
2. 分裂: [20] | [25, 30],把 25 上提到父节点
3. 父节点加入 25 → 可能继续分裂(级联)

分裂过程:
  Before:     [20 | 30]
  Insert 25:  [20 | 25 | 30]  (溢出)
  Split:      [20]  ←25→  [25 | 30]

4.4 删除

规则:节点太空(< ⌈m/2⌉ - 1 个键)就合并或借位

复制代码
删除 25:

1. 找到叶子节点 [25, 30],删除 25 → [30]
2. 检查是否下溢: 如果只有一个键,满足最小度数则结束
3. 不满足: 尝试从兄弟借位,或与兄弟合并

借位:
  兄弟 [15, 20] 借 20 → [20] | [30]

合并:
  兄弟也无力借位 → 合并为 [20, 30],删除父节点中的分隔键

五、MySQL InnoDB 中的 B+ 树

5.1 聚簇索引(主键索引)

复制代码
叶子节点存储完整行数据

       [10 | 20 | 30]           ← 非叶节点(主键索引)
      /    |     |    \
  [1,行数据] [10,行数据] [20,行数据] [30,行数据]  ← 叶子节点

聚簇索引就是数据本身,一张表只有一个。

5.2 二级索引(非主键索引)

复制代码
叶子节点存储主键值

       ['a' | 'm']                    ← 非叶节点(name 索引)
      /         \
  ['a'→5, 'b'→10]  ['m'→3, 'z'→20]   ← 叶子节点存主键

回表查询: 通过二级索引找到主键 → 再用主键查聚簇索引获取完整行。

5.3 覆盖索引

sql 复制代码
-- 覆盖索引:查询的列全在索引中,无需回表
SELECT name, id FROM user WHERE name = 'Alice';
-- name 索引的叶子已经有 id,不需要回表

-- 非覆盖:需要回表
SELECT name, age FROM user WHERE name = 'Alice';
-- age 不在 name 索引中,需要回表查聚簇索引

5.4 页分裂与页合并

复制代码
页分裂(插入导致):
  Page1: [1,2,3,4,5]  → 插入 3.5 溢出
  分裂为:
  Page1: [1,2,3]    Page2: [3.5,4,5]
  性能影响: 涉及数据拷贝,产生碎片

页合并(删除导致):
  Page1: [1,2]  Page2: [5]  → Page2 太空
  合并为: Page1: [1,2,5]

建议: 使用自增主键避免频繁页分裂

六、B+ 树 vs 其他索引结构

结构 查找 范围查询 插入 磁盘友好 适用场景
B+ 树 O(log N) 优秀 O(log N) 极好 关系型数据库
Hash 索引 O(1) 不支持 O(1) 等值查询(Memory 引擎)
红黑树 O(log N) 中序遍历 O(log N) 内存数据结构
LSM 树 O(log N) 需合并 O(1) 写密集(RocksDB)
跳表 O(log N) 支持 O(log N) 内存索引(Redis ZSet)

七、Java 中 B+ 树的应用

虽然 Java 标准库没有直接暴露 B+ 树实现,但以下场景涉及 B+ 树思想:

7.1 数据库索引

java 复制代码
// JDBC 操作,底层走 B+ 树索引
// 建立索引 → MySQL 创建 B+ 树
String sql = "CREATE INDEX idx_user_name ON user(name)";

// 范围查询走 B+ 树叶子链表扫描
String query = "SELECT * FROM user WHERE age BETWEEN 20 AND 30";

// EXPLAIN 查看是否命中索引
String explain = "EXPLAIN SELECT * FROM user WHERE name = 'Alice'";
// type = ref 表示走了索引

7.2 文件系统

复制代码
NTFS、Ext4 等文件系统使用 B+ 树变体管理目录和文件映射。

文件系统 B+ 树:
  内部节点: 目录名 → 子目录/文件的磁盘位置
  叶子节点: 文件数据块的磁盘位置列表

八、面试高频题

Q1: 为什么 MySQL 用 B+ 树而不是 B 树?

难度:⭐⭐⭐⭐

答案:三个原因:(1) B+ 树内部节点不存数据,同样页大小能存更多索引键,树更矮,I/O 更少;(2) 叶子节点链表连接,范围查询只需顺序扫描;(3) 查询性能稳定,每次都要到叶子节点,路径长度一致。

Q2: 为什么不用红黑树做数据库索引?

难度:⭐⭐⭐⭐

答案:红黑树是二叉树,树高 = O(log₂ N)。百万数据约 20 层,即 20 次磁盘 I/O。B+ 树是多路树,百万数据只需 3 层(3 次 I/O)。磁盘 I/O 是瓶颈,减少 I/O 次数比减少内存比较次数重要得多。

Q3: 为什么推荐自增主键?

难度:⭐⭐⭐

答案:自增主键保证插入顺序递增,新数据总是追加在 B+ 树最右侧,避免页分裂。随机主键(如 UUID)会导致频繁页分裂,产生大量随机 I/O 和碎片。

Q4: B+ 树的页大小为什么是 16KB?

难度:⭐⭐⭐⭐

答案:操作系统的页大小通常为 4KB,InnoDB 选择 16KB(4 个 OS 页)是为了减少磁盘 I/O 次数。16KB 在顺序读写和随机读写之间取得平衡,既能容纳足够多的索引键降低树高,又不会太大导致浪费。

Q5: 什么是回表?如何避免?

难度:⭐⭐⭐

答案:二级索引的叶子节点只存主键值,查询非索引列需要用主键再查一次聚簇索引,称为回表(两次 B+ 树查找)。避免方法:使用覆盖索引(查询列都在索引中)、联合索引、或直接查主键。

相关推荐
gihigo19982 小时前
MATLAB中实现混沌序列的相空间重构
开发语言·matlab·重构
xzl042 小时前
RT-Thread 5.2.2内核模块
开发语言·rt-thread
Wenzar_2 小时前
**发散创新:基于算子融合的深度学习推理优化实战**在现代AI推理场景中,模型性能瓶颈往往不是由单一算子决定的,而是多个连续算子之间数
java·人工智能·深度学习
我命由我123452 小时前
Android 开发问题:无法从存储库 “D:\keys\MyNotifications.jks“ 中读取密钥 MyNotifications.
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
Evand J2 小时前
【MATLAB代码介绍】三维环境下的IMM(交互式多模型),使用CV和CT模型,EKF作为滤波,目标高精度、自适应跟踪定位
开发语言·算法·matlab·imm·代码介绍
AI玫瑰助手2 小时前
Python基础:字符串的切片操作(含正向反向索引)
android·开发语言·python
ROLL.72 小时前
同步与异步
android·java
青槿吖2 小时前
告别RestTemplate!Feign让微服务调用像点外卖一样简单
java·开发语言·分布式·spring cloud·微服务·云原生·架构
chxii2 小时前
lua 下载和配置环境变量
开发语言·lua