高级 #database#index#btree

B+ 树索引

B+ 树是关系数据库最常用的索引结构——它是多路平衡查找树,所有数据都在叶子节点,内部节点只存"路标",适合磁盘块存储和大规模范围查询

🤔 为什么数据库不用二叉搜索树做索引?

你已经在数据结构部分学过二叉搜索树(BST)——查找效率 O(log n),在内存中表现很好。

但数据库的数据存在磁盘上。磁盘和内存有个根本性区别:磁盘读取一次数据的最小单位是一个”块”(Block,通常是 4KB~16KB),而不是一个字节。

如果你用二叉树做索引:

         [50]
        /    \
      [25]   [75]
     /   \   /   \
   [10] [37][60] [90]
  • 每个节点只存一个键(比如 50)
  • 每个节点占用一个磁盘块(4KB)
  • 读取一个块只拿到一个键,浪费了 99.9% 的空间
  • 100 万行数据的二叉树大约有 20 层高——查一次可能要读 20 次磁盘,每次几毫秒,加起来上百毫秒

B+ 树的核心思路就是:让每个磁盘块”塞满”尽可能多的键,大幅降低树的高度。

📚 类比:图书馆的索引系统

假设你要在一座巨大的图书馆里找一本《数据库系统概论》。

二叉搜索树 = 每层书架只贴一个字:“A-Z”的书的入口处只有一个”中”字标签。你要一个一个书架地看——从”中”到”数据”到”数据”到”数据库”……走很多步。

B+ 树 = 入口处有个大指示牌:“计算机类 → 3 楼 东南区”。到 3 楼又有更细的指示牌:“数据库 → 3-B-04 书架”。顺着指示走三步就到了。

关键区别:二叉树的每个”路标”只指向两个方向(左/右),B+ 树的每个”路标”指向几十甚至几百个方向。


🌳 B+ 树的结构

B+ 树由两种节点组成:

内部节点(Internal Node)——只存”路标”

内部节点存的是键的”分界线”——告诉你要去哪个子节点找数据:

内部节点:[50, 100, 150]

      键 < 50 → 左边
    50 ≤ 键 < 100 → 中间
   100 ≤ 键 < 150 → 右边
   150 ≤ 键 → 最右边

每个内部节点通常可以存几百个键——这就是为什么 B+ 树可以很”矮”。

叶子节点(Leaf Node)——存实际数据

叶子节点存的是真正的数据行指向数据行的指针。所有叶子节点在同一层,通过指针连成一个链表:

叶子层:[10, 20, 30] → [40, 50, 60] → [70, 80, 90]
           ↑ 链表指针                      ↑

这个链表结构使得范围查询非常高效——找到范围的起点后,顺着链表往后读就行。

完整的 B+ 树

内部节点层:
        [50, 100]
       /    |     \
   [20,35] [70,85] [120,150]
    /   |   /   |    /    \
叶子节点层:
 [10,15,20] [25,30,35] [50,60,70] [75,80,85] [100,110,120] [130,140,150]
  ──→ 链表连接 ────────────────────────────────────────────────────→

三步查找过程(以找 75 为例):

  1. 从根节点 [50, 100] 开始 → 75 在 50 和 100 之间 → 走中间子节点
  2. 到达 [70, 85] → 75 在 70 和 85 之间 → 走左数第二个子节点
  3. 到达叶子节点 → 扫描找到 75

💡 整个查找只需 3 次 I/O——无论表里有 100 万行还是 1000 万行。这就是 B+ 树的威力。


📊 B+ 树的关键特性

特性含义为什么重要
多路(Multi-way)每个节点存几百个键一个磁盘块塞满信息
平衡(Balanced)所有叶子在同一深度查询时间稳定,不会退化
有序(Ordered)叶子节点按链表连接范围查询(BETWEEN、>、<)效率高
扇出高(High Fanout)每个节点有几百个子节点树高度低(~3-4 层)

数值对比

数据量二叉树高度B+ 树高度(扇出 500)
1,000~10~2
1,000,000~20~3
1,000,000,000~30~4

1 亿条数据,B+ 树查一次只需要 4 次磁盘 I/O。 相比二叉树的 30 次,差距是数量级的。


🚀 B+ 树的插入和删除

插入——节点满了就”分裂”

INSERT INTO students VALUES (45, '赵六', 22, '计科1班');
  1. 找到 45 应该插入的叶子节点
  2. 如果节点还有空位 → 直接插入(保持有序)
  3. 如果节点满了 → 分裂(Split):把节点一分为二,把中间键提升到父节点
  4. 如果父节点也满了 → 父节点继续分裂
  5. 直到根节点——如果根也满了,树的高度 +1

删除——节点太空了就”合并”

删除的反向过程:

  1. 找到并删除目标键
  2. 如果节点太”空”(低于填充阈值) → 尝试和兄弟节点合并(Merge)
  3. 合并后父节点少了一个子节点 → 调整父节点
  4. 如果父节点也太空了 → 继续向上合并

🧩 类比:图书馆书架的扩容

插入就像往书架上放新书。

  • 书架有空位 → 直接插进去(保持按字母序)
  • 书架满了 → 从中间分成两个书架,贴上新的范围标签(写到内部节点)
  • 书架太多的时候 → 增加一个指示牌层(树增高)

B+ 树的自平衡

B+ 树的分裂和合并机制确保树始终平衡——无论你怎么插入、删除,所有叶子节点永远在同一深度。

这是 B+ 树和二叉搜索树的关键区别:二叉搜索树在最坏情况下会退化成链表(比如插入有序数据),但 B+ 树永远不会。


🏢 为什么 MySQL 的 InnoDB 用 B+ 树?

MySQL 是最流行的关系数据库之一,它的默认存储引擎 InnoDB 用 B+ 树作为索引结构。

-- 在 MySQL 中创建表时,主键自动用 B+ 树索引
CREATE TABLE students (
    id INT PRIMARY KEY,   -- 自动创建 B+ 树索引
    name VARCHAR(50),
    age INT
);

-- 手动创建的普通索引也是 B+ 树
CREATE INDEX idx_name ON students(name);

InnoDB 的 B+ 树有个特殊之处——它用的是聚簇索引(Clustered Index):叶子节点直接存储整行数据,而不是指向数据的指针。

这意味着:

  • 按主键查:一次 B+ 树查找就能拿到所有列的数据
  • 按普通索引查:先在索引的 B+ 树里找到主键值,再回主键的 B+ 树里找完整数据(这叫”回表”)
-- 按主键查 → 直接拿数据(一次 B+ 树查找)
SELECT * FROM students WHERE id = 100;

-- 按普通索引查 → 先找主键,再回主键树查(两次 B+ 树查找)
SELECT * FROM students WHERE name = '张三';

📝 小结

概念一句话
B+ 树多路平衡查找树——每个节点存几百个键,高度只有 3-4 层
内部节点只存”路标”(键的分界线),不存数据
叶子节点存实际数据,用链表连接支持范围查询
扇出每个节点的子节点数——扇出越大,树越矮
分裂(Split)节点满时一分为二,保证平衡
聚簇索引MySQL InnoDB 的 B+ 树——叶子直接存整行数据

🎯 思考题:B+ 树为什么可以实现为”每个节点的大小 = 一个磁盘块的大小”?这给读取带来了什么好处?(提示:想想 I/O 次数和块读取的关系)

为什么先学这个? B+ 树是数据库最通用的索引结构。但有些场景——比如等值查询——还有更快的选择:哈希索引