left join中右表条件应写在on而非where,否则null行被过滤导致退化为inner join;正确写法是on u.id = o.user_id and o.status = 'paid'。

LEFT JOIN 中右表条件写在 WHERE 会丢掉左表空匹配行
这是最常见也最容易踩坑的场景。比如想查所有用户及其已支付订单,却写了 SELECT * FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.status = 'paid'——结果里根本没有没订单的用户,LEFT JOIN 实际上退化成了 INNER JOIN。
原因很直接:WHERE 是在连接完成后的最终结果集上过滤。只要 o.status 是 NULL(即没订单),整行就被剔除。而 LEFT JOIN 的本意是“左表全保留”,这个语义被 WHERE 破坏了。
- 正确写法是把右表筛选条件挪进 ON:
ON u.id = o.user_id AND o.status = 'paid' - 这样没订单的用户仍会出现在结果中,只是
o.*全为 NULL - 如果同时要筛左表(如只查 VIP 用户),再用 WHERE:
WHERE u.is_vip = 1
INNER JOIN 下 ON 和 WHERE 表面等效,但语义和迁移风险不同
写成 INNER JOIN orders o ON u.id = o.user_id AND o.deleted = 0 或 INNER JOIN orders o ON u.id = o.user_id WHERE o.deleted = 0,多数情况下结果一样。但这只是优化器“帮忙重写”的结果,不是 SQL 标准保证的行为。
问题出在可维护性和跨库兼容性上:
- 把
o.deleted = 0放在 ON 里,后续若想改成LEFT JOIN,必须同步改条件位置,否则逻辑断裂 - 某些老版本 MySQL 或 Presto 不会做条件下推,WHERE 写法可能触发全表扫描或错误过滤
- 多表 JOIN 时,ON 中混入非关联字段(如
AND o.category = 'A')会让中间结果集膨胀,拖慢执行
ON 子句里能写左表字段吗?可以,但作用不是“先筛左表”
比如 LEFT JOIN orders o ON u.id = o.user_id AND u.type = 'vip',这不会提前过滤掉非 VIP 用户;而是说“只允许 VIP 用户尝试去连订单”,非 VIP 用户照样出现在结果里,只是 o.* 全为 NULL。
对比之下:WHERE u.type = 'vip' 才是真过滤——先筛出 VIP 用户,再连订单。
- 需要“所有用户 + 只连 VIP 用户的已支付订单”?组合写:
ON u.id = o.user_id AND o.status = 'paid'+WHERE u.type = 'vip' - ON 中左表条件本质是“限制连接尝试范围”,不影响左表行数
- WHERE 中左表条件才是“决定哪些左表行进入最终结果”
多表 LEFT JOIN 时,ON 必须紧贴对应 JOIN,顺序不能靠猜
写 A LEFT JOIN B ON ... LEFT JOIN C ON ... 时,第二个 ON 只属于第二个 JOIN,它能引用 A 和 B 的字段,但不能依赖 B 已被 WHERE 过滤过的结果——因为 WHERE 是最后才执行的。
典型错误是以为 “B 先被 WHERE 筛过,C 就只跟筛选后的 B 连”,其实不是。B 在 JOIN 阶段还是原始数据,C 的匹配基于完整 B 表(或按其 ON 条件过滤后的 B)。
- 想让 C 只连 B 中满足某条件的记录?条件必须写在第二个
ON里,比如ON b.id = c.b_id AND b.active = 1 - 不要指望第一个
WHERE能影响第二个JOIN的输入 - 复杂链式 JOIN 建议拆成子查询或 CTE,避免逻辑缠绕
实际写 SQL 时,最容易忽略的是执行阶段的不可见性:ON、WHERE、HAVING 各自生效时机不同,但你看到的只是一条语句。一旦涉及外连接,条件放错位置,结果就悄悄变了——而且往往不报错,只少数据。










