都2026年了,SQL优化居然还有是改写SQL文本的条件前后顺序,前面有案例<MySQL 多表关连时优化器缺陷出现的错误Join order>记录过多表join时优化器的评估缺陷,最近又看到了MySQL在两表关加的on的多个等值条件时,条件的书写顺序同样会导致性能问题。简单记录。
案例1 on 的条件位置不同
T1表大约3万行,T2表大约100万行(T2.f2字段90%是null),T1.f1,T2.f1,T2.f2都有索引,下面两条sql的结果集是一模一样的,差异点只有一个:T2.f2 IS NULL 放在了哪里。为什么第一条sql要3秒,第二条sql只有1毫秒?
SELECT * FROM T1 LEFT JOIN T2 ON T1.f1 = T2.f1 AND T2.f2 IS NULL WHERE T1.f1 = '2875205';

和
SELECT * FROM T1 LEFT JOIN T2 ON T1.f1 = T2.f1 WHERE T1.f1 = '2875205' AND T2.f2 IS NULL;

这个现象非常典型,本质是 LEFT JOIN + 条件位置不同,导致 MySQL 优化器选择了完全不同的执行路径。你这两条 SQL 逻辑等价(在这个场景下),但执行方式不等价,所以性能差很多。
sql1:
执行计划第一行完全没问题,T1.f1='2875205' 直接 const/ref 定位,第二行优化器选择了 idx_f2(单列索引),问题在这。
因为join条件里有T2.f2 is null,所以选择了T2.f2的索引idx_f2,idx_f2 选择性极差,扫描行数是1+1*90w,就是说T1每匹配一行,都需要扫描T2的90%的行,扫描的是大量 NULL 记录,虽然被驱动表走了索引,但又不完全走,索引没完全覆盖,约等于BNL算法被驱动表全表扫描,所以流程实际上是:
idx_f2 扫一堆 NULL
↓
回表
↓
判断 f1 是否等于
ON 里的条件是 JOIN 条件,MySQL 必须保证 LEFT JOIN 语义,不能随便把条件下推、重排,优化器认为“f2 IS NULL 是 JOIN 条件的一部分,那我用它的索引吧。即使它过滤能力很差,也只能这么选。
sql2:
T2.f2 IS NULL 写在 WHERE 里,T1.f1 = T2.f1选择了T2.f1的索引idx_f1,扫描行数是1+n,LEFT JOIN 被“降级”为 INNER JOIN,不再保留 T2 不匹配的行,LEFT JOIN 的“补 NULL 语义”消失,优化器 可以大胆重排条件。优化器改用 idx_f1(高选择性),这是 “先少后筛”,成本低得多。
两条 SQL 的“真实执行差异”
慢 SQL(ON 里)
T2.idx_f2 ↓大量 NULL 行 ↓回表 ↓再比对 f1
快 SQL(WHERE 里)
T1.idx_f1 → const ↓T2.idx_f1 精确查 ↓再判断 f2 IS NULL
差距不在 rows 显示值,而在“扫描路径”
rows 是 估算值,idx_f2 的 真实扫描成本 被严重低估,NULL 值分布对优化器并不“友好”。
优化方法
语义允许 → 改 INNER JOIN(最优),如快的SQL
必须 LEFT JOIN → 联合索引是唯一正解
CREATE INDEX idx_t2_f1_f2 ON T2(f1, f2);
案例2 on 的条件前后顺序不同
这个案例发生在TDSQL for MySQL中,当on 后多个关连条件,仅调整了条件的顺序,SQL响应时间从300s到27s.
当存在联合索引 (cod_acct_no, cod_dep_no) 时:
a.cod_acct_no = b.cod_acct_noAND a.cod_dep_no = b.cod_dep_no
比
a.cod_dep_no = b.cod_dep_noAND a.cod_acct_no = b.cod_acct_no
更容易命中联合索引的最优使用路径
👉 因此更快
没有拿到完整的执行计划,仅做猜测。
SQL 的逻辑结果当然一样,但 MySQL 在 JOIN 时并不是完全无序解析条件的。MySQL 在 JOIN 时是“边解析边评估”的,MySQL 并不会总是把 ON 里的等值条件“完美重排”,优化器评估是有顺序的。所以“把高选择性的列写前面”,或分布式的分片键写在前面,能“提示”优化器做出正确选择。
尤其在:
- 多条件 JOIN
- 多索引可选
- 成本差距不大的情况下
在 JOIN 的 ON 条件中,MySQL 内部是这样干活的(简化版):
- 先挑一个 驱动表
- 逐行取驱动表数据
- 按 ON 条件顺序,尝试构造索引访问条件
- 一旦能用上索引,就“定型”访问方式
— over —