2. 数据库索引基础:B+树索引原理、聚簇索引与非聚簇索引、联合索引的最左前缀原则
索引是数据库性能调优的核心武器。在 Go 项目慢查询调优中,理解索引的底层原理能帮助你精准定位“为什么这条 SQL 走索引却依然慢”或“为什么优化器没选索引”。本节从 B+ 树结构出发,逐步拆解聚簇/非聚簇索引的差异,以及联合索引最左前缀原则的实战含义。
2.1 B+ 树索引原理
B+ 树是 MySQL InnoDB 存储引擎默认的索引结构。它是一棵平衡多路查找树,所有数据都存储在叶子节点,非叶子节点只存储键值和指针。
核心特征:
- 非叶子节点:仅存放索引键(key)和指向子节点的指针(pointer),不存数据行。
- 叶子节点:存放完整的索引键 + 指向数据行的指针(或直接存放数据行,取决于聚簇/非聚簇)。
- 叶子节点之间:通过双向链表连接,形成有序链表,支持高效的范围查询(
BETWEEN、>、<)。 - 树的高度:通常为 2~4 层,即使千万级数据,也只需 3~4 次磁盘 I/O 即可定位到目标行。
下图展示了 B+ 树索引的典型结构:
调优启示:B+ 树的高度决定了索引查找的磁盘 I/O 次数。在 Go 项目慢查询日志中,如果看到
rows_examined 远大于 rows_sent,且表数据量很大,通常意味着索引选择度不够或发生了全表扫描。此时应检查索引的区分度(cardinality)是否足够高。
2.2 聚簇索引与非聚簇索引
InnoDB 中,聚簇索引(Clustered Index)和非聚簇索引(Secondary Index)在数据存储方式上有本质区别:
| 对比维度 | 聚簇索引 | 非聚簇索引(二级索引) |
|---|---|---|
| 数据存储位置 | 叶子节点直接存放整行数据 | 叶子节点存放索引键 + 主键值(或行指针) |
| 每张表数量 | 只能有 1 个(主键索引) | 可以有多个 |
| 查询方式 | 直接通过索引找到数据行,无需回表 | 先找到主键值,再通过聚簇索引回表查询完整行 |
| 范围查询性能 | 优秀(数据按主键顺序物理存储) | 一般(需要多次回表) |
| 插入性能 | 顺序插入快,随机插入可能触发页分裂 | 插入时需维护索引树,索引越多开销越大 |
常见误区:很多 Go 开发者认为“只要建了索引,查询就一定快”。实际上,如果查询的列不在索引中,非聚簇索引需要回表(回主键索引查完整行)。当回表次数过多(如扫描大量二级索引记录后回表),性能可能反而不如全表扫描。此时应使用覆盖索引(索引包含查询所需的所有列)来避免回表。
下图对比了聚簇索引与非聚簇索引的查询路径:
2.3 联合索引的最左前缀原则
联合索引(Composite Index)是指基于多个列创建的索引。最左前缀原则决定了联合索引的可用性:查询条件必须从索引的最左列开始匹配,才能使用该索引。
最左前缀原则核心规则:
- 索引
(a, b, c)可以匹配的查询模式:a、a,b、a,b,c。 - 无法匹配的查询模式:
b、c、b,c(跳过了最左列a)。 - 可以部分匹配:
a,c只能用到a列索引,c列无法使用索引。 - 范围查询(
>、<、BETWEEN)后的列无法使用索引进行等值匹配。
假设有一张用户表 users,联合索引为 (city, age, status),以下 SQL 的索引使用情况如下:
| SQL 查询 | 索引使用情况 | 说明 |
|---|---|---|
WHERE city='北京' AND age=25 AND status=1 |
✅ 完全使用索引 | 三列全部匹配 |
WHERE city='北京' AND age=25 |
✅ 使用索引前两列 | 最左前缀匹配 |
WHERE city='北京' |
✅ 使用索引第一列 | 最左前缀匹配 |
WHERE age=25 AND status=1 |
❌ 无法使用索引 | 跳过了最左列 city |
WHERE city='北京' AND status=1 |
⚠️ 仅使用 city 列 |
status 无法使用索引(跳过了 age) |
WHERE city='北京' AND age > 20 AND status=1 |
⚠️ 仅使用 city 和 age 列 |
age 范围查询后 status 无法使用索引 |
Go 项目实战建议:
- 字段顺序决定索引效率:将区分度高的列放在联合索引最左侧。例如
(city, age)优于(age, city),因为city的区分度通常更高。 - 避免冗余索引:如果已有
(a, b)索引,则(a)索引是冗余的,可以删除。 - 利用覆盖索引减少回表:如果查询只需要
a, b两列,而索引(a, b)已经包含这两列,则无需回表。 - 慢查询日志分析:在 Go 项目中开启慢查询日志后,重点关注
rows_examined远大于rows_sent的查询,检查是否因违反最左前缀原则导致索引失效。
避坑提醒:不要为了“覆盖所有查询”而创建包含大量列的联合索引。索引列越多,B+ 树节点能容纳的键值越少,树高增加,写入性能下降。通常联合索引控制在 3~5 列以内,且优先覆盖高频查询的 WHERE 和 ORDER BY 列。