SQL 多表 JOIN 时 ON 条件写法与 WHERE 过滤的语义与性能差异

1次阅读

on只决定连接匹配,不负责最终过滤;left join中业务条件应放where而非on,否则会误删左表行;where过滤右表字段会使left join退化为inner join;优化应关注explain的filtered值变化。

SQL 多表 JOIN 时 ON 条件写法与 WHERE 过滤的语义与性能差异

ON 条件里不能随便塞 WHERE 逻辑

JOIN 的 ON 子句只决定「哪些行能被连上」,不是过滤最终结果的工具。比如 LEFT JOIN 后在 ON 里写 t2.status = 'active',会把 t2 中不满足该条件的行当成 NULL 补上,而不是直接剔除——这和写在 WHERE 里效果完全不同。

常见错误现象:LEFT JOIN 结果里本该有左表全部记录,但实际只剩部分;或者右表字段全为 NULL 却没意识到是 ON 条件太严。

  • 想保留左表所有行?ON 里只放关联条件(如 t1.id = t2.t1_id),状态、时间等业务过滤一律挪到 WHERE
  • 想只连上「活跃用户」的订单?用 INNER JOIN + ON ... AND t2.status = 'active' 可以,但等价写法是 INNER JOIN (...) ON ... WHERE t2.status = 'active' —— 前者更清晰表达意图
  • ON 中混用非关联字段,可能让优化器放弃使用索引,尤其当字段来自右表且无索引时

WHERE 过滤 NULL 会让 LEFT JOIN 变成 INNER JOIN

这是最常踩的坑:在 LEFT JOIN 后加 WHERE t2.id IS NOT NULLWHERE t2.name != 'test',表面看只是筛数据,实际等效于强制要求右表必须有匹配行——语义上已退化为 INNER JOIN

使用场景:你真需要「左表有、右表无」的记录(比如查没下过单的用户),就不能在 WHERE 里碰右表任何字段,哪怕只是判 IS NULL 也要确认是否真想排除掉右表有值的行。

  • WHERE t2.id IS NULL 是合法且常用的需求(找未匹配项)
  • WHERE t2.id IS NOT NULL ≡ 把 LEFT JOININNER JOIN 用,不如直接改写,避免误导后续维护者
  • 如果必须混合过滤(比如「左表用户 + 右表活跃订单 + 订单金额 > 100」),优先把右表业务条件放在 ON(对 INNER JOIN)或子查询中,别依赖 WHERE 拦截

多表 JOIN 时 ON 的顺序影响可读性,不影响执行计划

sql 标准不规定 ON 必须紧贴对应 JOIN,但人读的时候容易混乱。比如 FROM t1 JOIN t2 ON ... JOIN t3 ON ...,第二个 ON 是修饰 t2 JOIN t3 还是 t1 JOIN t2?答案是前者,但缩进或换行不对就极易误读。

性能上,现代优化器基本能重排 JOIN 顺序,ON 写在哪一行不改变实际执行路径;但可读性和后期修改成本差别很大。

  • 每个 JOIN 后立刻跟它的 ON,不要到最后统一写
  • 涉及复合条件(如 t2.a = t1.a AND t2.b = t3.b),说明设计已有缺陷——跨三个表的关联条件应拆到子查询或视图里,否则难以维护
  • 用括号明确嵌套关系:比如 (t1 JOIN t2 ON ...) JOIN t3 ON ...,尤其在混合 LEFT/INNER 时能避免歧义

EXPLAIN 看不出 ON 和 WHERE 的语义区别,得看 rows 和 filtered

EXPLAIN 输出里的 typekeyrows 能反映实际扫描量,但不会告诉你哪条过滤是在 JOIN 阶段应用、哪条是最后筛。真正关键的是 filtered 列——它表示该表在应用完所有条件后,剩余行数占扫描行数的百分比。

举个例子:如果 t2filtered 是 10%,而你在 ON 里写了 t2.status = 'active',说明这个条件确实参与了驱动连接,但如果 filtered 是 100%,大概率是条件被移到了最后一步过滤,没起到提前剪枝作用。

  • 对比两次 EXPLAIN:一次把条件放 ON,一次放 WHERE,重点看右表的 rowsfiltered 是否变化
  • 如果 rows 不变但 filtered 从 100% 降到 5%,说明条件成功下推到了 JOIN 阶段,值得保留
  • mysql 8.0+ 的 format=json 更直观,关注 attached_condition 字段出现的位置

事情说清了就结束。真正难的不是语法对错,而是每次写 JOIN 时,得想清楚:我到底要保留哪些空?哪些行不该出现?这些意图能不能从 SQL 里一眼看出来。

text=ZqhQzanResources