公告

👇微信扫码添加好友👇

图片
Skip to content

B+ 树原理与数据库索引

B+ 树是数据库索引的核心数据结构,理解它的原理对于数据库性能优化至关重要。

一、为什么数据库需要 B+ 树

1.1 二叉树的问题

如果使用二叉搜索树:
         10
        /  \
       5    15
      / \   / \
     3   7 12  20
    / \
   1   4

假设数据量 = 100万
树高度 ≈ log₂(1000000) ≈ 20

问题:
- 每个节点只有 2 个子节点
- 树的高度较高
- 每次查询需要 20 次磁盘 I/O
- 磁盘 I/O 是性能瓶颈!

1.2 磁盘 I/O 的特点

操作耗时
内存访问~100 纳秒
SSD 随机读~100 微秒
HDD 随机读~10 毫秒

内存 vs HDD:10万倍的差距!

减少磁盘 I/O 次数是数据库索引设计的核心目标。

1.3 B 树的思路

让每个节点存储多个键值,增加分支数:

B 树 (阶数=3):
         [10 | 20]
        /    |    \
   [1,5] [12,15] [25,30]

- 每个节点最多 3 个子节点
- 树的高度大幅降低
- 同样 100万 数据,高度可能只有 3-4 层

二、B 树的基本概念

2.1 B 树的定义

B 树(Balance Tree)是一种自平衡的多路搜索树,一个 m 阶 B 树满足:

1. 每个节点最多有 m 个子节点
2. 每个非根节点至少有 ⌈m/2⌉ 个子节点
3. 根节点至少有 2 个子节点(除非是叶子节点)
4. 所有叶子节点在同一层
5. 节点内的键值有序排列

2.2 B 树结构示例(3阶)

              [10 | 20]
             /    |    \
    [1 | 5]   [12 | 15]  [25 | 30]
    / | \      / | \       / | \
   ↓  ↓  ↓    ↓  ↓  ↓     ↓  ↓  ↓
  数据 数据 数据 数据 数据 数据 数据 数据 数据

2.3 B 树的特点

特点说明
多路平衡每个节点可以有多个子节点
节点有序节点内键值有序排列
叶子同层所有叶子节点在同一层
数据分布每个节点都存储数据

三、B+ 树的结构特点

3.1 B+ 树 vs B 树

B 树:
              [10 | 20]
             /    |    \
    [1 | 5]   [12 | 15]  [25 | 30]
    ↓   ↓      ↓    ↓      ↓    ↓
   数据 数据  数据  数据   数据  数据

B+ 树:
         [10 | 20]              ← 非叶子节点只存索引
        /    |    \
   [1|5|10]→[12|15|20]→[25|30] ← 叶子节点存数据,且链表连接

3.2 B+ 树的核心特点

特点1:非叶子节点只存索引

B 树节点:  [键值 | 数据 | 指针 | 指针 | 指针]
B+ 树节点: [键值 | 指针 | 指针 | 指针]

好处:
- 同样的节点大小可以存更多键值
- 树更矮,I/O 次数更少

特点2:叶子节点存储所有数据

所有数据都在叶子节点
非叶子节点只是索引,用于路由

好处:
- 查询性能稳定,所有查询都走到叶子
- 范围查询高效

特点3:叶子节点用链表连接

[1|5|10] ⇄ [12|15|20] ⇄ [25|30]
   ↑              ↑            ↑
 双向链表      双向链表      双向链表

好处:
- 范围查询只需遍历链表
- 排序查询高效
- 支持反向遍历

3.3 B+ 树结构图示

                    [30 | 60]                     ← 根节点 (索引)
                   /    |    \
          [10|20|30] [40|50|60] [70|80|90]        ← 内部节点 (索引)
          / | | \    / | | \    / | | \
         ↓  ↓ ↓ ↓   ↓  ↓ ↓ ↓   ↓  ↓ ↓ ↓
        [1|5]→[10|15]→[20|25]→[30|35]→[40|45]→... ← 叶子节点 (数据)
          ↕     ↕      ↕      ↕      ↕
        双向链表连接所有叶子节点

四、B+ 树的插入操作

4.1 插入规则

1. 找到合适的叶子节点
2. 如果叶子节点未满,直接插入
3. 如果叶子节点已满,分裂:
   a. 叶子节点分裂成两个
   b. 中间键值提升到父节点
   c. 如果父节点也满,继续分裂

4.2 插入示例(3阶B+树)

初始状态:
    [10 | 20]
   /    |    \
 [1|5] [12|15] [25|30]

插入 8:
找到叶子节点 [1|5],未满,直接插入
    [10 | 20]
   /    |    \
 [1|5|8] [12|15] [25|30]

插入 3:
叶子节点 [1|5|8] 已满,需要分裂
分裂: [1|3] 和 [5|8],中间值 5 提升
      [5 | 10 | 20]
     /    |    |    \
  [1|3] [5|8] [12|15] [25|30]

如果父节点也满,继续分裂...

4.3 插入操作代码思路

python
def insert(node, key, value):
    if node.is_leaf:
        if node.is_full:
            # 分裂叶子节点
            new_node = node.split()
            # 将中间键提升到父节点
            parent.insert_key(new_node.min_key, new_node)
        else:
            node.insert(key, value)
    else:
        # 找到合适的子节点
        child = node.find_child(key)
        insert(child, key, value)

五、B+ 树的删除操作

5.1 删除规则

1. 找到包含该键的叶子节点
2. 删除该键
3. 如果叶子节点太小(少于 ⌈m/2⌉-1 个键):
   a. 尝试从兄弟节点借一个键
   b. 如果兄弟也小,合并节点
4. 更新父节点的索引

5.2 删除示例

初始状态:
    [5 | 10 | 20]
   /   |    |    \
 [1|3] [5|8] [12|15] [25|30]

删除 8:
叶子节点 [5|8] 变成 [5],键数 = 1 < ⌈3/2⌉-1 = 1
从兄弟借: 兄弟 [12|15] 有多余的键
借 12: [5] → [5|12],父节点索引更新

    [5 | 12 | 20]
   /   |    |    \
 [1|3] [5|12] [15] [25|30]

5.3 合并节点

如果兄弟也没有多余的键:

删除前:
    [10]
   /    \
 [1|5]  [12|15]

删除 5:
[1|5] → [1],兄弟 [12|15] 也小
合并: [1] + [12|15] = [1|12|15]
父节点删除键 10

结果:
    [1|12|15]

六、B+ 树的优势分析

6.1 查询性能稳定

B 树:
- 最好情况: 查到根节点 = 1 次 I/O
- 最坏情况: 查到叶子节点 = h 次 I/O
- 性能不稳定

B+ 树:
- 所有查询都走到叶子节点 = h 次 I/O
- 性能稳定,可预测

6.2 范围查询高效

sql
-- 查询 id 在 10-100 之间的记录
SELECT * FROM users WHERE id BETWEEN 10 AND 100;
B 树:
- 需要中序遍历整棵树
- 可能需要多次回溯父节点

B+ 树:
- 找到 id=10 的叶子节点
- 沿着链表遍历到 id=100
- 非常高效!

6.3 磁盘 I/O 更少

对比项B 树B+ 树
节点存储键值+数据只存键值
单节点容量较小较大
树高度较高较矮
I/O 次数较多较少

6.4 完整对比

特性B 树B+ 树
数据位置所有节点只在叶子
叶子链表
范围查询效率低效率高
查询稳定性不稳定稳定
空间利用较低较高
适用场景文件系统数据库索引

七、MySQL InnoDB 中的应用

7.1 聚簇索引(Clustered Index)

InnoDB 的主键索引就是 B+ 树结构:

叶子节点存储: 完整的行数据
非叶子节点: 主键值 + 指针

        [10 | 20]                    ← 主键索引
       /    |    \
   [1|5]  [12|15]  [25|30]           ← 叶子节点存储完整行
   ↓      ↓        ↓
  行数据  行数据   行数据

特点:

  • 每个表只能有一个聚簇索引(通常是主键)
  • 数据按主键顺序物理存储
  • 主键查询非常快

7.2 二级索引(Secondary Index)

非主键索引(如 name 索引):

叶子节点存储: 主键值(不是完整行)
非叶子节点: 索引列值 + 指针

        ["Alice" | "Bob"]            ← name 索引
       /        |         \
   ["Amy"]  ["Alice"]  ["Bob"]       ← 叶子节点存主键值
   ↓         ↓          ↓
   10        20         30           ← 回表查主键索引获取完整数据

回表查询:

sql
-- 使用二级索引查询
SELECT * FROM users WHERE name = 'Alice';

-- 步骤:
-- 1. 在 name 索引中找到 Alice,得到主键 id=20
-- 2. 用 id=20 去主键索引查完整行数据(回表)

7.3 覆盖索引

sql
-- 如果查询的列都在索引中,不需要回表
SELECT id, name FROM users WHERE name = 'Alice';

-- name 索引的叶子节点已经存储了 id 和 name
-- 直接返回,无需回表

7.4 联合索引与最左前缀

sql
-- 联合索引 (name, age)
CREATE INDEX idx_name_age ON users(name, age);
索引结构:
        [("Alice",20) | ("Bob",25)]
       /        |              \
  [("Alice",20)] [("Amy",30)]  [("Bob",25)]

可以使用索引的查询:
- WHERE name = 'Alice'              ✓
- WHERE name = 'Alice' AND age = 20 ✓
- WHERE age = 20                    ✗ (不满足最左前缀)

八、索引设计最佳实践

8.1 选择合适的索引列

sql
-- 适合建索引的列:
-- 1. WHERE 条件频繁使用的列
-- 2. JOIN 连接的列
-- 3. ORDER BY 排序的列
-- 4. 高选择性的列(不同值多)

-- 不适合建索引的列:
-- 1. 频繁更新的列
-- 2. 选择性低的列(如性别)
-- 3. 数据量小的表

8.2 索引列顺序

sql
-- 联合索引的列顺序很重要
-- 规则: 选择性高的列放前面

-- 例如: users 表
-- name 选择性: 0.9 (几乎不重复)
-- age  选择性: 0.01 (很多重复)
-- gender 选择性: 0.001 (只有几个值)

-- 推荐: (name, age, gender)

8.3 避免索引失效

sql
-- ❌ 在索引列上使用函数
WHERE YEAR(create_time) = 2025

-- ✅ 改为范围查询
WHERE create_time >= '2025-01-01' AND create_time < '2026-01-01'

-- ❌ 隐式类型转换
WHERE phone = 13800138000  -- phone 是 VARCHAR

-- ✅ 使用正确类型
WHERE phone = '13800138000'

-- ❌ LIKE 左模糊
WHERE name LIKE '%风'

-- ✅ 右模糊可以使用索引
WHERE name LIKE '沐%'

8.4 覆盖索引优化

sql
-- 创建覆盖索引
CREATE INDEX idx_covering ON orders(user_id, status, create_time);

-- 查询只访问索引,无需回表
SELECT user_id, status, create_time 
FROM orders 
WHERE user_id = 1001 AND status = 1;

九、总结

9.1 B+ 树的核心优势

优势说明
树高度低多路平衡,减少磁盘 I/O
查询稳定所有查询都走到叶子节点
范围查询高效叶子节点链表连接
空间利用率高非叶子节点只存索引

9.2 B+ 树在数据库中的应用

应用说明
聚簇索引叶子存完整行数据
二级索引叶子存主键值
联合索引多列组合索引
覆盖索引避免回表查询

9.3 索引设计原则

1. 选择高选择性的列建索引
2. 联合索引注意最左前缀
3. 避免索引失效的写法
4. 使用覆盖索引减少回表
5. 控制索引数量,避免过度索引

学习建议

  1. 理解 B 树到 B+ 树的演进原因
  2. 掌握 B+ 树的插入删除过程
  3. 理解聚簇索引和二级索引的区别
  4. 结合 MySQL 实际使用理解索引优化
  5. 通过 EXPLAIN 验证索引使用情况