LEFT JOIN 时右表条件必须写在 ON 中以保留左表所有行,若误写在 WHERE 中会导致左表数据被过滤;ON 控制连接逻辑,WHERE 控制筛选逻辑,二者语义与执行阶段不同,影响结果、性能及可扩展性。

LEFT JOIN 时右表条件必须写在 ON 里
想保留左表所有行,又只连上右表满足特定条件的记录,就得把右表的业务条件塞进 ON。比如查所有用户,但只关联「状态为 active」的订单:LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'active'。如果写成 WHERE o.status = 'active',那些没订单或订单非 active 的用户就全被过滤掉了——LEFT JOIN 变相退化成 INNER JOIN。
常见错误现象:结果行数比预期少,左表数据“凭空消失”;用 EXPLAIN 看执行计划,rows 明显变小,Extra 出现 Using where,说明过滤发生在连接后。
- ON 中条件只影响右表哪些行参与连接,不影响左表行数
- WHERE 中引用右表字段(除
IS NULL类判断外)等效于加了AND o.id IS NOT NULL - 别依赖“反正结果一样”,换到 Presto 或老版 MySQL,行为可能不一致
INNER JOIN 下 ON 和 WHERE 结果常一样,但语义不能混
INNER JOIN 里把 o.deleted = 0 放 ON 还是 WHERE,多数情况下结果相同。但这不是 SQL 标准保证的,而是优化器“碰巧”做了等价重写。一旦加了子查询、CTE 或跨多个表 JOIN,行为就可能分裂。
使用场景更关键:你真想表达“只连未删除的订单”,就该写在 ON;如果这是后续筛选逻辑(比如同一查询要复用于 LEFT JOIN 场景),就该挪到 WHERE。
- ON 应只放关联逻辑:外键匹配、分片键对齐、主从关系定义
- WHERE 才放业务规则:时间范围、状态码、权限字段
- 把
o.created_at >= '2024-06-01'放在users JOIN orders的ON里,比放在末尾WHERE更早剪枝,中间数据量更小
多表 JOIN 时 ON 条件顺序直接影响可读性和执行效率
写 FROM users u JOIN orders o ON u.id = o.user_id JOIN order_items i ON o.id = i.order_id,第二个 ON 是修饰 o JOIN i,不是 u JOIN o。缩进不对或堆在一起,人眼极易误读。
性能上,优化器虽能重排顺序,但人工控制更稳。尤其当某张右表过滤性极强(比如只有 0.1% 行满足条件),应优先把它和左表的 JOIN 写在前面,并把高选择性条件放进对应 ON。
- 每个
JOIN后立刻跟它的ON,别统一堆到最后 - 避免在
ON里引用前面未 JOIN 的表字段(MySQL 直接报错,PostgreSQL 允许但语义模糊) - 复合关联条件如
t2.a = t1.a AND t2.b = t3.b,说明表设计或查询结构已有缺陷,应拆到子查询
用 EXPLAIN 验证,而不是靠经验猜
哪怕只是把一个 WHERE 条件挪进 ON,rows 扫描量也可能从 50 万降到 300。但这个变化不会体现在 SQL 语法上,只能靠 EXPLAIN 看。
重点关注三列:type(是否为 ref 或 range)、key(是否命中索引)、rows(预估扫描行数)。如果 Extra 出现 Using join buffer 或 Using temporary,说明连接或过滤方式已出问题。
- 别信“写法顺眼就行”,百万级数据下,差一个条件位置,响应时间可能从 0.02s 涨到 12s
- ON 是建模阶段就该想清楚的约束,WHERE 是呈现前的裁剪——两者控制的是不同阶段的数据流
- 真正容易被忽略的,是条件位置对后续扩展的影响:今天写在 ON 里的业务条件,明天想改成 LEFT JOIN 时,很可能得连带重构整条查询

















