3. 索引基础:B+树与索引结构
聊查询优化,绕不开索引。但索引到底是个什么东西?
很多人背过“索引是一种数据结构”,可真问起来,脑子里还是模模糊糊的。我个人的习惯是——**把索引理解成书的目录**。你想想,没有目录的书,要找“B+树”这个知识点,得从第一页翻到最后一页,这叫全表扫描。有了目录,直接在目录里定位到第 300 页,翻过去就是。效率差异就在这里。
3.1 为什么是 B+树
数据库底层的数据结构不止一种,哈希表、二叉树、B 树、B+树,选择很多。但主流关系型数据库的索引,几乎都选了 B+树。为什么?
我来简单对比下。哈希表快,O(1) 的查找速度,但只适合等值查询。你做个范围查询,比如 WHERE age BETWEEN 20 AND 30,哈希表就傻了,得全扫。二叉树呢?数据量一大,树的高度就失控了,磁盘 I/O 次数直线上升。B 树虽然做了改良,每个节点存多个值,但它的非叶子节点也存数据,同样一个节点能存的索引项就少了。
B+树不一样。它做了两件事:
- 非叶子节点只存索引键,不存数据,所以一个节点能容纳的索引项会更多,树的高度更低。
- 叶子节点存完整数据(或者说指向数据的指针),并且用双向链表串起来。这样范围查询就变成了链表遍历,效率极高。
我在项目中遇到过类似情况。一张表几千万数据,用 SELECT * FROM orders WHERE create_time >= '2024-01-01' AND create_time < '2024-02-01' 查出来上万条记录。如果是 B 树,你要在多个节点间来回跳。换成 B+树,定位到第一个满足条件的叶子节点之后,顺着链表往后读就行了。这个差异,在真实的大表查询里非常明显。
3.2 B+树的结构拆解
我从两个角度来拆解 B+树的结构:从上到下,从左到右。
3.2.1 根节点、内部节点、叶子节点
一棵 B+树通常有三层结构。我画个简单的示意图帮你理解。
3.2.2 一个关键细节:Page(页)
数据库的最小存储单位不是行,而是 Page(页)。MySQL InnoDB 默认的页大小是 16KB。一个 Page 里能装多少行数据,决定了你的索引效率。
来个简单的计算。假设你的索引键是 bigint(8 字节),加上指向儿子的指针(6 字节),一共 14 字节。那么一个 16KB 的 Page 能放多少个索引项?
16 * 1024 / 14 ≈ 1170 个
三层 B+树,第二层和第三层还是同样的结构。
1170 * 1170 * 16 ≈ 2190 万行
看懂了吗?只需要三次磁盘 I/O,就能从 2000 万行数据中找到你要的那条记录。这就是 B+树的魔力。
3.3 聚簇索引与非聚簇索引
聊 B+树,不能不提聚簇索引和非聚簇索引。很多新手在这里栽过跟头。
3.3.1 聚簇索引
InnoDB 中,聚簇索引就是主键索引。它的叶子节点直接存了一整行数据。换句话说,数据和索引是“捆在一起”的。所以当你通过主键去查数据时,一次索引就能拿到所有字段,不需要回表。
我个人建议:
- 建表一定要显式指定主键,而且要自增。否则 InnoDB 会帮你偷偷建一个隐藏的 ROW_ID。
- 主键不要用 UUID。UUID 是随机的,插入时会导致页分裂和索引重排,性能会断崖式下降。
3.3.2 非聚簇索引(二级索引)
非聚簇索引的叶子节点存的不再是完整数据,而是 主键值。所以当你通过二级索引查询时,实际上有两步:
- 先扫描二级索引的 B+树,找到对应的主键。
- 再拿着主键去聚簇索引里回表查完整数据。
这一步如果走多了,就叫“回表开销”。
3.4 索引的优缺点一览
| 维度 | 优点 | 缺点/代价 |
|---|---|---|
| 查询速度 | 大幅减少磁盘 I/O,范围查询极快 | 索引本身占用磁盘空间 |
| 写操作 | 合适的索引能加速 UPDATE 的 WHERE 条件 | 每次 INSERT/UPDATE/DELETE 都要维护索引树,降低写入性能 |
| 排序 | 索引本身就是有序的,可以避免文件排序(Using filesort) | —— |
| 内存 | 常被访问的索引会缓存在 Buffer Pool 中 | 索引过多会挤占数据缓存空间 |
3.5 讲点实在的:怎么用好 B+树
理论讲完了,落地才是关键。我个人总结了三条原则:
- 最左前缀原则。联合索引 (a, b, c) 只有在查询条件里带上 a 的时候才会生效。跳过了 a,只查 b 和 c,索引就废了。
- 尽量用覆盖索引。SELECT 的字段如果能全部命中索引,就不用回表,IO 少一半。
- 注意索引列上的函数操作。比如
WHERE DATE(create_time) = '2024-01-01',这种情况下索引会失效,因为函数把原始值改了。
EXPLAIN 看一眼,确认 type 列是不是 ref 或 range,Extra 列有没有 Using index。看到 Using filesort 或 Using temporary 就要警惕了,十有八九是索引没用好。
B+树这个东西,初看觉得抽象,用多了就发现它其实很“直觉”。你把一棵树的高度压到 3 层,把查询从全表扫描降成三次 IO,剩下的优化空间就出来了。