SQL 的联合索引(Composite Index,也叫复合索引、组合索引)是在多个列上建立的一个索引。它不是一个索引叠加另一个,而是把多列按定义顺序压进同一棵 B+ 树,使一个索引就能服务多列组合查询。据 MySQL 官方文档(How MySQL Uses Indexes),若建有 (col1, col2, col3) 的三列索引,优化器可利用其任意最左前缀 (col1)、(col1, col2)、(col1, col2, col3) 来查找行。由此派生出最左前缀原则:查询条件必须从联合索引定义的最左列开始、连续向右匹配,跳过左列则右侧列无法利用该索引。这一原则不是 MySQL 的语法规则,而是 InnoDB 的 B+ 树物理排序结构决定的。
一、联合索引是什么
单列索引每列一棵树,多个单列索引应对多条件查询时,优化器往往只能选其一或做索引合并(效率一般更低)。联合索引把高频组合条件”预排序”进一棵树,一次定位即可。据 aicancode 的联合索引教程,设计良好的联合索引还能顺带成为覆盖索引——若查询所需列都在这棵树里,EXPLAIN 会显示 Using index,无需回表。
二、最左前缀的底层原理
联合索引在 B+ 树中按列定义顺序分层排序:先按第一列全局排序,第一列相同再按第二列排,第二列相同再按第三列排。这与”先按国家、再按省份、最后按城市”的电话簿完全一致。
以官方示例索引 (col1, col2, col3) 为例,B+ 树隐式提供三种前缀查询能力:
| 可用前缀 | 不可用组合 |
|---|---|
(col1) |
(col2) |
(col1, col2) |
(col3) |
(col1, col2, col3) |
(col2, col3) |
据 CSDN 的 B+ 树结构解析,必须先确定”大分类”(最左列)才能利用这本目录;直接查 col2 或 col3 而没有 col1,等于不按国家直接找城市,B+ 树无法定位起点,索引整体失效。
三、生效与失效全场景
用索引 idx(targetType, targetId) 举例(综合 CSDN 实战):
-- 完全生效:最左列 + 右侧精准列
SELECT * FROM report WHERE targetType = 0 AND targetId = 100;
-- 完全生效:仅最左列
SELECT * FROM report WHERE targetType = 0;
-- 完全失效:跳过最左列
SELECT * FROM report WHERE targetId = 100;
注意两点:一是查询条件书写顺序不影响匹配——优化器会重排 WHERE targetId=100 AND targetType=0 与前者等价,关键是”是否包含最左列”;二是范围查询会截断后续列,下文展开。
四、列顺序设计的核心规则
据 aicancode 与多个官方/技术资料,顺序遵循”等值列在前、范围列在后、高基数列靠前”:
-- 查询:WHERE user_id = ? AND created_at BETWEEN ? AND ?
-- 正确:(user_id, created_at) —— 等值先钉住一个 B+ 树分片,再在片内范围扫
CREATE INDEX idx_good ON orders(user_id, created_at);
-- 错误:(created_at, user_id) —— 日期范围会扫过所有用户,user_id 无法收窄
CREATE INDEX idx_bad ON orders(created_at, user_id);
理由:user_id 等值能把搜索范围钉在 B+ 树的一个小子树,created_at 再在子树内顺序扫,读取行数最少;颠倒后日期范围横跨全部用户,索引几乎退化为大范围扫描。
五、范围查询截断最左前缀
最左前缀是”连续的”。一旦中间出现范围条件(>、<、BETWEEN、LIKE 'x%'),其后的列在索引中不再有序,无法用于精确定位。
-- 索引 (status, created_at, user_id)
-- status 等值 + created_at 范围:前两列可用
EXPLAIN SELECT * FROM orders
WHERE status = 'PAID' AND created_at > '2025-01-01';
-- 再加 user_id = 42:user_id 通常不再用于范围扫描,
-- 至多靠索引条件下推做过滤,不能继续收窄 B+ 树扫描
EXPLAIN SELECT * FROM orders
WHERE status = 'PAID' AND created_at > '2025-01-01' AND user_id = 42;
据 aicancode 说明,优化器在第一个无等值过滤的列处停止利用索引收窄——第二列若是范围,第三列便不再参与 B+ 树定位。因此把可能被范围查询的列放在索引末尾。
六、低基数列与索引合并的坑
据 aicancode 提醒,不要把低基数列(如布尔 active、性别)放联合索引最左——只有两个取值意味着第一次键查找后还有约一半行要扫。另:优化器可对两个单列索引做 index_merge(EXPLAIN 显示 type: index_merge),但通常慢于一个设计得当的联合索引,不应依赖它替代联合索引设计。
七、设计联合索引的执行步骤
- 列出高频多条件查询,提取常共现的列组合。
- 把等值过滤列放最左,范围/排序列放最右。
- 高选择性列靠前,低基数列避免打头。
- 尽量让索引覆盖查询列,减少回表。
- 建好后跑
EXPLAIN,看key是否命中、key_len是否用到预期列数。
常见问题(FAQ)
查询条件写反顺序(先写右列后写左列)会失效吗?
不会。优化器会重排条件,只要包含最左列即可走索引;失效取决于”是否含最左列”而非书写顺序。
第二列用了范围,第三列还能用索引吗?
通常不能继续收窄 B+ 树扫描,最多靠索引条件下推过滤;所以范围列应放索引末位。
MySQL 8.0 的 Skip Scan 能替代最左前缀吗?
它是优化器对”跳过最左列”的例外手段,依赖最左列低基数,不可作为设计依据,仍应按最左前缀建索引。