left join后左表数据“消失”是因为where中对右表字段加了非空条件,导致null行被过滤;正确做法是将筛选条件移至on子句,如left join orders o on u.id = o.user_id and o.status = 'paid'。

LEFT JOIN后左表数据“消失”了,大概率是WHERE里写了右表字段
LEFT JOIN本身不会丢左表数据,但一旦在WHERE子句中对右表字段加非空条件(比如WHERE o.status = 'paid'),数据库会先完成连接,再过滤——而o.status为NULL的行直接被筛掉,结果等效于INNER JOIN。
真正要保留左表全部记录,同时只关联特定右表数据,必须把筛选条件挪到ON子句里:
-
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid'→ 没有已支付订单的用户仍显示,o.*全为NULL -
LEFT JOIN orders o ON u.id = o.user_id WHERE o.status = 'paid'→ 只返回有已支付订单的用户,其余左表行消失 - 若需同时查“已支付订单”和“无订单用户”,
AND必须紧贴ON,不能进WHERE
多表LEFT JOIN时,ON条件必须指向当前左结果集的字段
写A LEFT JOIN B LEFT JOIN C时,SQL按从左到右执行:(A LEFT JOIN B)的结果成为下一次LEFT JOIN C的“左表”。这时C的ON条件不能写a.id = c.a_id(a可能已在中间结果中不可见或歧义),而应写ab.a_id = c.a_id(ab是前一步结果的别名)。
- 每层
JOIN的ON只管本次匹配,不继承上层右表的NULL状态 - 别名务必清晰:用
u、o、p代替t1、t2,避免字段来源混淆 - 如果第二层需要依赖右表
B的某个字段做关联,但该字段在B中可能为NULL,就得提前用COALESCE或子查询处理,否则ON b.x = c.y会因b.x IS NULL失效
LEFT JOIN后右表字段参与计算,必须显式处理NULL
o.amount * 1.1这种表达式遇到o.amount IS NULL,结果就是NULL,不是0。聚合函数如SUM(o.amount)默认忽略NULL,但COUNT(*)和COUNT(o.amount)行为不同——前者统计所有行,后者只计非NULL值。
- 算术运算前用
COALESCE(o.amount, 0)兜底,比在应用层判断更可靠 - 统计关联数量用
COUNT(o.id),不是COUNT(*);前者只数右表有匹配的行数 - 判断是否匹配成功,优先用连接键是否为
NULL:例如o.order_id IS NOT NULL,而不是o.total_amount IS NOT NULL(金额本身允许为空)
性能差不是LEFT JOIN的锅,而是ON字段缺索引
LEFT JOIN本身不强制扫描右表全量,但若ON用的字段(如orders.user_id)没建索引,数据库大概率走嵌套循环,左表每行都要扫一遍右表,数据量一上去就卡住。
- 确保右表的关联字段有单列索引或复合索引前缀(如
(user_id, status)) -
EXPLAIN看执行计划,重点确认type是不是ref或eq_ref,不是ALL - 如果右表条件复杂(如时间范围+状态),考虑用子查询或CTE预先过滤,再LEFT JOIN,比堆砌多个
AND在ON里更易优化
最常被忽略的是:LEFT JOIN的语义正确性,远比写法“看起来像”重要。你到底要“完整主表+可选副表”,还是“主表中副表存在才有效”——前者用LEFT JOIN + 条件放ON,后者其实该换INNER JOIN。










