根本原因是MySQL无法用多个单列索引同时匹配多列ON条件,必须依赖顺序严格的复合索引;单列索引不满足最左前缀原则,优化器通常弃用而退化为全表扫描或块嵌套循环。

多列组合关联慢,根本原因不是JOIN写得不对,而是右表的ON字段没落在同一复合索引的最左前缀上——哪怕每个字段单独都有索引,也大概率失效。
为什么单列索引对多列ON条件基本无效
MySQL(及多数主流引擎)在匹配 ON t1.a = t2.x AND t1.b = t2.y 这类条件时,不会自动合并多个单列索引。它只能用上一个索引,且必须满足“最左前缀原则”:如果 t2 表只建了 INDEX(x) 和 INDEX(y),优化器无法同时利用两者,通常退化为全表扫描或 Using join buffer (Block Nested Loop)。
- 复合索引
(x, y)能覆盖ON t2.x = ? AND t2.y = ?,但不能用于ON t2.y = ? AND t2.x = ?(顺序错) -
(y, x)索引对ON t2.x = ? AND t2.y = ?仍可能走不到,因为优化器不保证重排条件顺序去匹配索引 - 若还带范围查询,比如
AND t2.z > '2025-01-01',复合索引应设计为(x, y, z),把等值字段放前面,范围字段放后面
复合索引字段顺序必须严格对齐ON子句中的等值顺序
不要凭感觉排序。执行计划里 Extra 出现 Using where; Using index 才算真正用上了覆盖索引;如果只有 Using index 或压根没出现,说明索引没被完整识别。
- ON 条件是
t2.code = t1.code AND t2.status = t1.status→ 索引必须是(code, status),而非(status, code) - 如果实际查询中
status过滤性更强(比如只有 2% 行满足),也**不能**因此把status放前面——顺序必须服从 ON 中出现的先后 - 有
WHERE同时存在时,优先保证 ON 字段在索引最左,再把 WHERE 中的等值字段追加进去,最后才是范围或 SELECT 字段(用于覆盖)
函数、隐式转换、类型不一致会让整个复合索引失效
哪怕索引建得完全正确,只要 ON 或 WHERE 里动了字段,就前功尽弃。这类问题在 EXPLAIN 里往往表现为 type: ALL 或 key: NULL,但你容易误以为是索引没建。
-
ON UPPER(t2.code) = UPPER(t1.code)→ 索引失效,改用生成列 + 函数索引(MySQL 8.0+)或提前标准化存储 -
t2.code是VARCHAR(50),t1.code是VARCHAR(100)→ 可能触发隐式转换,导致索引不走 -
t2.id是BIGINT,但 JOIN 时和t1.id(INT)比较 → 类型不一致,强制转换后索引失效 - 时间字段用
DATE(created_at) = '2026-05-19'→ 放弃索引,改用created_at >= '2026-05-19' AND created_at < '2026-05-20'
EXPLAIN 里这几个字段比“是否用了索引”更重要
别只盯着 key 列是否非空。真正暴露问题的是 rows 和 Extra —— 它们告诉你数据库实际打算怎么干,而不是你以为它会怎么干。
-
rows显示 100 万,但你知道右表只有 1 万行匹配?说明统计信息过期,立刻执行ANALYZE TABLE t2 -
Extra: Using join buffer (Block Nested Loop)→ 不是数据量大,是右表根本没走索引,正在全表扫描匹配 -
Extra: Using temporary; Using filesort→ 关联后还要聚合或排序,考虑把排序字段加入复合索引末尾,或提前在子查询里完成 -
type: ref是合格线;type: eq_ref是理想状态(通常是主键/唯一键 JOIN);type: ALL就是警报
最容易被忽略的一点:复合索引生效的前提,是左表(驱动表)已经通过 WHERE 或其他方式大幅缩小了结果集。如果左表本身没过滤,几百万行去 JOIN,哪怕右表索引完美,NLJ 的外层循环次数也足以拖垮性能。先看驱动表能不能砍掉 90%,再谈右表索引。

















