Skip to content

02.1 - B+ 树索引结构

定位:MySQL InnoDB 的默认索引结构,理解 B+ 树的设计是理解一切索引优化的基础 面试高频度:⭐⭐⭐⭐⭐ 考查方式:为什么用 B+树不用 B 树/红黑树/哈希?B+树层高和数据量的关系?范围查询如何工作?


一、这是什么?为什么需要它?

是什么

B+树是一种自平衡的多路搜索树,是 B 树的变体。它的核心特征:

  • 内节点只存 key(不存数据)→ 扇出(fan-out)极高,树极矮
  • 叶子节点存所有 key 对应数据(或指向数据的指针)
  • 叶子节点之间用链表串联 → 范围查询极其高效

为什么数据库需要 B+树?

想象没有 B+ 树的数据库:

  1. 用哈希表? 等值查询 O(1),但无法范围查询WHERE age > 20 → 全表扫描),也无法排序。数据库的日常工作大量是范围查询和排序。

  2. 用二叉树/红黑树? 树高 log₂N。1 亿行数据 → 树高约 27 层 → 每次查询需要 27 次磁盘 IO → 大约 270ms(磁盘每次 IO 约 10ms)。

  3. 用 B 树? 好很多,但内节点也存数据,扇出较低(每个节点只能存 2-3 个 key),且范围查询需要"中序遍历回溯"(从子节点回溯到父节点再找兄弟节点)。

B+树的解决方案:

  • 内节点只存 key → 每个节点可存 几百个 key → 1 亿行数据 → 树高仅 3-4 层 → 3-4 次磁盘 IO
  • 叶子节点链表 → 找到起始 key 后直接沿着链表遍历 → 不需要回溯

核心 insight:B+树的所有设计都针对磁盘 IO 昂贵这一前提。用"冗余 key"(数据只存一次,key 在内节点中复制)换取"极矮的树"和"高效的范围遍历"。


二、原理拆解

2.1 B+树结构详解

                     B+树(三层示例)
                       
         ┌────────────────────────┐
         │         [50, 100]       │   ← 内节点:存 key,不存数据
         │        /    |    \      │      扇出 = 3(实际 Innodb ~470+)
         └───────┼────┼────┼──────┘
               ┌──┘    │    └──┐
               ▼       ▼       ▼
         ┌────────┐ ┌────────┐ ┌────────┐
         │[10,30, │ │[60,80, │ │[120,   │
         │ 40,50] │ │ 90,100]│ │ 150]   │   ← 第二层内节点
         └───┬────┘ └───┬────┘ └───┬────┘
        ┌────┼───┐   ┌──┼───┐   ┌──┼───┐
        ▼    ▼   ▼   ▼  ▼   ▼   ▼  ▼   ▼
     ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐
     │ 1 │ │ 11│ │ 31│ │ 51│ │ 61│ │ 81│ │ 101│ │121│ │151│
     │行数据│ │行数据│ │行数据│ │行数据│ │行数据│ │行数据│ │行数据│ │行数据│ │行数据│
     └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘
     └───────────────────────────────────────────────────────┘
     叶子节点层:存完整数据(或主键+行指针),链表相连

     ←─────────────────────────────────────────────────────→
     叶子节点链表:范围查询从这里遍历

2.2 扇出(Fan-out)为什么重要?

扇出 = 一个内节点能存储的子节点指针数量。

InnoDB 每个节点 = 1 个数据页(默认 16KB)。

一个数据页大小:    16KB = 16,384 字节
一个 key 占用:    约 8-20 字节
一个指针占用:     约 6 字节
一个(key+指针)对: 约 14-26 字节

一个节点的 key 数 ≈ 16KB / 20字节 ≈ 800 个
扇出 ≈ 800+ (实际考虑页内元数据,约 470+)

因此:
树高 1 层:    ~500 行
树高 2 层:    ~500 × 500 = 250,000 行
树高 3 层:    ~500 × 500 × 500 = 125,000,000 行
树高 4 层:    ~625 亿行

结论:对于绝大多数 OLTP 数据库(百万 ~ 亿级),B+树高度为 3-4 层
      每次查询最多 3-4 次磁盘 IO

2.3 B+树 vs B 树 vs 红黑树对比

                    B+树                     B 树                 红黑树/AVL
  ┌──────────  ──────────      ──────────       ───────────────
  │ 内节点    只存 key             存 key + data      存 key + data
  │ 叶子节点  存所有 key + data    同内节点           同内节点
  │ 叶子链表  ✅ 有(范围查询快)   ❌ 无              ❌ 无
  │ 扇出      极大(~500)         小(~2-3)          最小(2)
  │ 树高(1亿)  3-4 层              约 8-10 层          约 27 层
  │ 磁盘 IO   3-4 次               8-10 次             27 次
  │ 范围查询  O(N) 极快            O(N*logN) 慢        O(N*logN) 慢
  │ 插入性能  良好(自动分裂)      良好                 可能需要旋转

2.4 插入与分裂

插入 key = 25(假设叶子节点已满):

分裂前:
  [20, 30, 40, 50]   ← 叶子节点已满(假设阶=4)
        ↑ 插入 25

分裂后:
      [30]           ← 新的内节点 key(提升)
     ↙    ↘
  [20, 25]  [40, 50]  ← 两个半满的叶子节点

为什么分裂从叶子向上传播? B+树以叶子节点为数据存储层,分裂产生的"key 提升"保证内节点始终是路由信息。与 B 树不同,数据永远不会放在内节点上——这让树的形状更稳定。


三、图解全景

                    B+树 vs B 树 vs 红黑树对比图

       B+树                           B 树
┌────────────────┐            ┌────────────────┐
│    [50, 100]   │            │  [50, data]    │ ← 内节点就有数据
│   ↙   ↓    ↘  │            │ ↙   ↓    ↘    │    扇出更小
│ [10,30]  [60,  │            │ [10,30][60]  │
│  80]   [120]  │            │ [80][120,150]│
│ ↓ ↓ ↓ ↓ ↓ ↓ ↓│            │ ↓ ↓ ↓ ↓ ↓  │
│ 1~9│11~29│...││            │ 1~9│11~29│...││
│ └─────────┘  │            │ └─────────┘  │
│ 叶子节点链表→→→│            │ (无叶子链表)   │
│ (范围查询快)  │            │ (范围查询慢)   │
└────────────────┘            └────────────────┘

      红黑树                         哈希表
┌────────────────┐            ┌────────────────┐
│     A          │            │ bucket[]       │
│   ↙   ↘       │            │ [0]: 1, 11,... │
│  B     C      │            │ [1]: 2, 22,... │
│ ↙ ↘   ↙ ↘    │            │ ...            │
│ D  E  F  G    │            │ 范围查询: ❌   │
│ 树高: log₂N   │            │ 等值查询: O(1) │
│ 范围查询: ❌  │            └────────────────┘
└────────────────┘

四、实战验证

查看 B+树层高

sql
-- 查看表的索引树高度(近似)
SELECT 
  b.name AS table_name,
  a.name AS index_name,
  INDEX_HEIGHT(a.space, a.page_no) AS height
FROM information_schema.INNODB_TABLES b
JOIN information_schema.INNODB_INDEXES a ON b.table_id = a.table_id
WHERE b.name = 'your_db/your_table';

⚠️ 注意:INDEX_HEIGHT() 是调试函数,MySQL 8.0 可能需要打开 innodb_debug 参数。

估算 B+树层高

sql
-- 根据表行数估算 B+树层数
-- 假设:
--   页大小 = 16KB
--   主键 = BIGINT (8字节)
--   指针 = 6字节
--   每行数据 ≈ 200字节
--   
-- 内节点每页存: (16KB - 元数据) / (8 + 6) ≈ 1170 个 key
-- 叶子节点每页存: 16KB / 200 ≈ 80 行

SELECT 
  ROW_COUNT,
  CEIL(LOG(1170, ROW_COUNT / 80)) + 1 AS estimated_height
FROM (SELECT 1000000 AS ROW_COUNT) AS t;
-- 100万行 → 树高约 2 层
-- 1亿行  → 树高约 3 层

验证 B+树范围查询效率

sql
-- 创建测试表
CREATE TABLE test_btree (
  id INT PRIMARY KEY AUTO_INCREMENT,
  val INT,
  INDEX idx_val (val)
);

-- 插入 100 万行数据
-- ... (省略插入过程)

-- 等值查询
EXPLAIN SELECT * FROM test_btree WHERE id = 500000;
-- 预期: type=const, 1 row

-- 范围查询(主键)
EXPLAIN SELECT * FROM test_btree WHERE id BETWEEN 500000 AND 500100;
-- 预期: type=range, 101 rows, Extra: Using index condition

-- 排序
EXPLAIN SELECT * FROM test_btree ORDER BY id LIMIT 10;
-- 预期: type=index, Extra: Using index(B+树直接遍历叶子链表)

五、面试视角

追问答案要点
为什么不用红黑树做数据库索引?树高 log₂N,1 亿行 ≈ 27 层 IO;且无法利用磁盘预读(B+树一页读几百 key)
B+树和 B 树的核心区别?B+树内节点只存 key(扇出大),B 树内节点存数据(扇出小);B+树叶子节点有链表(范围查询快)
B+树层高怎么算?树高 = log_{扇出}(行数/叶子节点每页行数) + 1。通常 3-4 层够存亿级数据
为什么不用哈希做索引?哈希不支持范围查询(>、<、BETWEEN),不支持排序,不支持最左前缀
InnoDB 索引页大小可以改吗?可配置 innodb_page_size(4KB/8KB/16KB/32KB/64KB),但数据文件不可移植
为什么 B+树插入也高效?节点分裂是局部操作(最多影响相邻页),且页内有序可二分查找

📚 相关链接

  • 索引实践:**聚簇索引与二级索引**
  • 优化进阶:**索引优化策略**
  • 分析工具:**EXPLAIN与查询优化**
  • 上游存储:**InnoDB 架构**
  • ← 返回 **索引与查询优化索引**

Knowledge4J — Java 知识库