LEFT JOIN中右表过滤条件必须写在ON子句中才能保留左表空匹配行;若误写在WHERE中,则会剔除NULL行,导致逻辑退化为INNER JOIN。

LEFT JOIN 中右表字段的过滤条件写在 ON 还是 WHERE,直接决定左表空匹配行是否保留——写错位置,LEFT JOIN 就会静默退化成 INNER JOIN。
LEFT JOIN 的 ON 只控制右表“能不能连上”
ON 条件在连接阶段生效,它不筛左表,只决定右表哪些行能参与匹配。不满足 ON 的右表行被跳过,但左表行照出,右表字段填 NULL。
-
ON u.id = o.user_id AND o.status = 'paid':只有 status 为 'paid' 的订单才可能连上来;没订单、或订单是 'pending' 的用户,o.*全为NULL,但用户仍在结果里 -
ON u.id = o.user_id AND u.type = 'vip':非 VIP 用户也会出现在结果中,只是不会跟任何订单匹配(o.*全NULL) - ON 中混入右表的非等值条件(如
o.created_at > '2025-01-01')可能导致索引失效,尤其当该字段 NULL 值多时
WHERE 是连接完成后的最终裁剪
WHERE 在整个 JOIN 链执行完才运行,此时中间结果已含大量 NULL。对右表字段做非空判断(如 WHERE o.status = 'paid'),会把所有 o.status IS NULL 的行(即没订单的用户)整行干掉。
- 常见错误:
LEFT JOIN orders o ON u.id = o.user_id WHERE o.paid_at IS NOT NULL→ 没订单的用户全消失 -
WHERE中的!=或<>对NULL永远返回UNKNOWN,所以WHERE o.deleted != 1会漏掉o.deleted IS NULL的行 - 想保留 NULL 行又过滤业务值,得显式写:
WHERE o.status = 'paid' OR o.status IS NULL,但语义已变,慎用
INNER JOIN 里 ON 和 WHERE 看似等效,但靠不住
对 INNER JOIN,ON a.id = b.id AND b.deleted = 0 和 ON a.id = b.id WHERE b.deleted = 0 通常返回相同结果,但这依赖优化器重写能力,不是 SQL 标准保证的行为。
- 跨数据库迁移时风险高:老版 MySQL 或 Presto 可能拒绝 ON 中放非关联条件,或执行计划突变
- 后续想把 INNER JOIN 改成 LEFT JOIN 时,原写在 ON 里的业务条件(如
b.deleted = 0)必须同步挪到 WHERE,否则语义断裂 - 建议统一风格:纯关联逻辑(外键、分片键)放 ON,业务筛选(状态、时间、权限)放 WHERE
多表 LEFT JOIN 时 ON 不跨层,也不能假设左表已被筛
写 A LEFT JOIN B ON ... LEFT JOIN C ON b.id = c.bid AND c.flag = 1,第二个 ON 中的 c.flag = 1 只影响 C 表哪些行能连上来,它完全不感知 B 是否已被前面条件筛过——因为 ON 是逐对执行的,没有“全局中间态”。
- 不能指望
WHERE b.name IS NOT NULL能确保 C 连接时 B 已存在;WHERE 在所有 JOIN 完才执行 - 把本该在 WHERE 筛左表的条件(如
a.category = 'vip')错塞进第二个 ON,会导致 A 行数异常膨胀或空匹配 - 每个 ON 只能引用它左边所有表(A、B)和右边那个表(C),不能引用更右的表,也不能假设左边表已被 WHERE 过滤
真正容易被忽略的是:ON 和 WHERE 的语义边界在多表 JOIN 中会快速模糊,尤其当嵌套层级加深、或条件涉及多个右表字段时。别依赖“看起来一样”,每次加条件前先问一句:这个条件是定义“怎么连”,还是定义“要哪些行”。


















