人人都会AI编程

5.5 联合索引结构与最左前缀原则底层逻辑

更新时间:2026-07-11

联合索引(也叫复合索引)就是把多个列组合在一起建成一个索引。很多开发者知道“联合索引要遵循最左前缀原则”,但容易停留在死记规则的层面,一旦遇到“为什么这个查询用不到索引”时就卡住了。只有理解了联合索引在 B+ 树里的真实存储结构,才能彻底吃透最左前缀原则。

联合索引在 B+ 树中是怎么存的

假设有一张用户表 users,我们建了一个联合索引:

CREATE INDEX idx_name_age ON users(name, age);

在单列索引的 B+ 树里,非叶子节点的键就是索引列的值,叶子节点存放键和对应的主键值。联合索引的特别之处在于:索引键不是一个单一的值,而是一个由多个列值拼接而成的“组合键”

对于 idx_name_age,InnoDB 会按先 nameage 的顺序对数据进行排序存储。也就是说:

  • 首先按 name 列排序,name 相同再按 age 排序。
  • 在 B+ 树的每个节点中,相邻的索引记录都是先比较 namename 相等再比较 age,以此决定先后顺序。

例如表中有这几行数据:

| id | name | age |
|----|------|-----|
| 1 | Alice| 25 |
| 2 | Alice| 30 |
| 3 | Bob | 22 |
| 4 | Bob | 28 |

那么在联合索引的叶子节点中,索引记录的排列顺序会是:

(Alice, 25) -> (Alice, 30) -> (Bob, 22) -> (Bob, 28)

这个顺序至关重要,因为它决定了索引能够支持哪些查询条件。

最左前缀原则的本质

所谓最左前缀原则,是指只有当查询条件中包含了联合索引最左边开始的连续列时,索引才能被有效利用。这不是数据库故意设下的规矩,而是由 B+ 树的排序方式天然决定的。

还是用 (name, age) 这个联合索引来举例:

  • 可以用到索引
  • WHERE name = 'Alice' —— 等值匹配最左列,可以快速在 B+ 树中定位到 (Alice, *) 的起始范围。
  • WHERE name = 'Alice' AND age = 25 —— 两个列都等值匹配,索引定位更精确,直接锁定叶子节点中的具体记录。
  • WHERE name = 'Alice' AND age > 20 —— name 等值确定一个块,然后按 age 排序在块内进行范围扫描。
  • 无法用到索引或只能用一部分
  • WHERE age = 25 —— 缺少最左列 name。因为索引整体是按 name 排序的,整个 B+ 树中 age=25 的记录分散在不同 name 的区间里,就像一本按姓氏排序的电话本,你想找所有年龄为 25 岁的人,索引帮不了你,只能全表扫描。
  • WHERE name > 'Alice' AND age = 25 —— 索引中 name 是范围查询,age 列虽然在索引中,但因为 name 不是等值,导致 age 在跨 name 时并不可控。实际上优化器只会使用索引对 name 进行范围过滤,然后再在结果集上逐行判断 age

这就是为什么联合索引的列顺序设计如此重要。建立 (name, age) 这个索引后,就相当于你同时免费获得了 (name) 的索引,因为数据已经按照 name 排序。但你并没有获得 (age) 的单独索引,除非你另外再建一个。

不止等值:范围条件的截断效应

最左前缀原则在实际使用中有一个非常容易踩坑的地方:一旦最左匹配链条中出现了范围条件,后面就更进一步的条件通常无法继续使用索引进行定位

举例来说,假设有一个联合索引 (a, b, c),查询条件是 WHERE a = 1 AND b > 10 AND c = 5

  • 对于条件 a = 1b > 10,索引可以发挥作用:在 B+ 树中先定位到 a=1 的区间,然后在这个区间内按 b 的排序继续排除 b <= 10 的部分。
  • 但对于 c = 5,由于 b 已经是范围条件,不同 b 值对应的叶子节点里 c 没有全局排序,索引无法继续利用 c 来减少扫描范围。c = 5 只能作为过滤器在回表后或者索引条件下推时做判断。

所以这个索引实际只用了两列:ab。如果把列顺序调整为 (a, c, b),而查询是 a = 1 AND c = 5 AND b > 10ac 都是等值,索引对 ac 都能精确定位,剩下的 b 虽然还是范围,但这时 b 前面都是等值,它依然能部分利用索引做范围扫描。这说明联合索引中把区分度高、常用等值查询的列放在前面,把范围查询的列往后放,通常更优

同样地,LIKE 'abc%' 这种前缀模糊查询可以视为等值前缀,但 LIKE '%abc' 就不能利用了,因为它在 B+ 树中没有顺序。

能跳过最左列吗?索引跳跃扫描

从 MySQL 8.0.13 开始,优化器引入了一项叫索引跳跃扫描(Index Skip Scan)的新能力。在某些情况下,即使查询跳过了最左列,依然可以使用联合索引。

比如有索引 (gender, age),查询条件只有 WHERE age = 25gender 的取值比较少(如只有男、女两个值),优化器内部可以把它拆成两个范围扫描——先扫 gender='男'age=25 的,再扫 gender='女'age=25 的——然后将结果合并。这相当于把一个大范围扫描拆成了多个小范围扫描,当最左列基数很低时,性能远比全表扫描好。

但这不是万能的,只有当最左列不同值比较少、SELECT 中未涉及太多额外列时,优化器才可能选择这种策略。它并没有推翻最左前缀原则,而是利用前缀值集有限的特点做了变通。在绝大多数情况下,你仍然应该遵守最左前缀来设计索引。

如何用好最左前缀原则

  1. 分析你的查询模式:不要拍脑袋建 (a, b, c),先统计系统中哪些查询最常见、哪些条件组合最高频。
  2. 把等值条件列往前放:高频等值查询列放在联合索引最左,能最大化利用索引精确定位的能力。
  3. 范围条件往后排><BETWEEN 这类条件可以放在等值列之后,这样它们前面等值部分依然可以全面利用索引。
  4. 避免最左列缺失:如果查询经常跨过最左列,说明这个索引设计可能不适合该查询,需要调整列顺序或建新索引。
  5. 善用覆盖索引:如果联合索引已经包含了查询需要的全部字段,即使是用到部分列,也可以避免回表,提升性能。比如 SELECT name, age FROM users WHERE name = 'Alice'(name, age) 索引上就是覆盖查询。

理解最左前缀的底层逻辑,会让你在设计索引和诊断慢查询时胸有成竹。记住,不是 MySQL 故意用规则为难你,而是 B+ 树这种顺序数据结构本身就决定了这种使用方式。