left join中on过滤右表关联行,where全局筛行;on中放关联逻辑(如外键),where放业务筛选(如状态、时间);混用易致左表空匹配行丢失,且跨数据库或版本可能行为不一致。

LEFT JOIN 时 WHERE 和 ON 的过滤效果完全不同
LEFT JOIN 后加过滤条件,放在 WHERE 还是 ON 里,直接决定是否“变相转成 INNER JOIN”。ON 是连接时判断关联逻辑,WHERE 是连接完再筛行——这一步错,就可能把本该保留的左表空匹配行给干掉了。
常见错误现象:LEFT JOIN users u ON u.id = o.user_id WHERE u.status = 'active',结果发现订单没用户信息的行全没了——因为 WHERE 把 u.status 为 NULL 的行全踢了。
- 想保留左表所有行?过滤右表字段必须写进
ON,比如ON u.id = o.user_id AND u.status = 'active' - 想筛最终结果里的完整记录(含左右字段)?才用
WHERE,但要确认是否允许右表字段为NULL -
ON中的条件只影响右表参与连接的行,不影响左表行数;WHERE是全局筛,会删掉整行
INNER JOIN 下 WHERE 和 ON 效果一样,但语义不该混用
对 INNER JOIN 来说,ON a.id = b.id AND b.deleted = 0 和 ON a.id = b.id WHERE b.deleted = 0 执行结果一致。但这是优化器“碰巧”能等价重写的结果,不是语言保证的行为。
使用场景:写 JOIN 查询时,应把“关联逻辑”和“业务筛选”分开处理——前者放 ON,后者放 WHERE。否则换到其他数据库(比如某些老版本 MySQL 或 Presto),或加了复杂子查询、CTE 后,行为可能不一致。
-
ON应只放关联条件(如外键匹配、分片键对齐),保持语义清晰 -
WHERE放业务过滤(如时间范围、状态、权限字段),方便后续复用或下推 - 如果把
b.deleted = 0写进ON,以后想改成 LEFT JOIN 就得同步改逻辑,容易漏
多表 JOIN 时 ON 条件顺序影响可读性和执行计划
当连续 JOIN 三张以上表时,ON 条件写在哪一对之间,会影响人肉理解顺序,也会影响优化器估算中间结果集大小。尤其在 WHERE 有强过滤条件时,提前在靠前的 ON 里限制右表,能显著减少中间数据量。
例如:查“活跃用户最近3天的订单”,users → orders → order_items,如果把 orders.created_at >= '2024-06-01' 放在 users JOIN orders 的 ON 里,比放在最后的 WHERE 更早生效。
- 优先把高选择性(高过滤率)的条件,放在它所归属的那对表的
ON子句中 - 避免在
ON里引用前面未 JOIN 的表字段(MySQL 会报错,PostgreSQL 允许但语义难懂) - 别依赖“优化器会自动调整顺序”——它通常按 FROM 后顺序构建连接树,人工控制更稳
EXPLAIN 看不出 ON/WHERE 差异?得看 rows 和 filtered
光看 EXPLAIN 输出的 type 或 key 不够,关键要看 rows(预估扫描行数)和 filtered(预估过滤后剩余比例)。LEFT JOIN 场景下,把条件从 WHERE 挪到 ON,常会让右表的 rows 变小,而左表 rows 不变——这就是“提前剪枝”的证据。
容易踩的坑:有些同学看到 Extra 里有 Using where 就以为条件生效了,其实它可能只是对连接后的临时结果做二次过滤,性能已经受损。
- 对比两个版本的
EXPLAIN FORMAT=JSON,重点看rows_examined_per_scan和filter_condition字段位置 - 在 PostgreSQL 里用
EXPLAIN (ANALYZE, BUFFERS)观察实际Rows Removed by Filter - 别只测单条 SQL——JOIN 条件位置变化会影响整个查询计划,连带影响索引选择和并行度
最麻烦的不是语法对错,是 LEFT JOIN 里混用 WHERE 做右表过滤,线上跑了一年都没问题,直到某天加了个新索引或者升级了小版本,执行计划突变,空数据开始冒出来。










