LEFT JOIN后WHERE过滤右表字段会筛掉NULL行,使连接退化为INNER JOIN;正确做法是将右表条件移至ON子句,如ON u.id = o.user_id AND o.status = 'paid'。

LEFT JOIN 后 WHERE 条件会把 NULL 行干掉
这是最常踩的坑:写 LEFT JOIN 本意是保留左表所有记录,但一加 WHERE 过滤右表字段,结果左连接变相成了内连接。
比如想查所有用户及其订单数(含 0 单用户),却写了:
SELECT u.id, u.name, COUNT(o.id) AS cnt FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.status = 'paid' -- ❌ 这里过滤了 o.status 为 NULL 的行 GROUP BY u.id, u.name;
结果是只返回有已支付订单的用户,没订单或订单未支付的用户全丢了。
正确做法是把右表的过滤条件挪到 ON 子句里:
-
ON中的条件只影响关联逻辑,不影响左表保留 -
WHERE中的条件作用于最终结果集,对右表字段做判断会筛掉 NULL 行 - 若需同时过滤左表(如只查活跃用户),
WHERE仍可用,但别碰右表字段
多个 LEFT JOIN 时 ON 条件必须独立写清楚
连三张表时,容易误以为第二个 LEFT JOIN 的 ON 能“继承”前一个的关联上下文。其实每个 ON 只管当前连接,必须显式写出全部依赖关系。
例如查用户、订单、订单商品,且只要已发货订单的商品:
SELECT u.name, o.order_no, i.sku FROM users u LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'shipped' LEFT JOIN order_items i ON o.id = i.order_id AND i.qty > 0;
注意两点:
- 第二个
LEFT JOIN的ON里不能省略o.id = i.order_id,不能写成i.order_id IS NOT NULL之类 -
o.status = 'shipped'放在第一个ON里,不是WHERE,否则会丢掉无发货订单的用户 - 如果把
i.qty > 0放到WHERE,会导致整行被过滤(包括o为 NULL 的情况)
需要“左表全量 + 右表满足某条件的聚合”时,用子查询或 CTE 更稳
当过滤逻辑复杂(比如要取每个用户的最新一笔已支付订单),硬塞进 ON 容易出错或性能差。这时优先考虑先过滤右表再关联。
例如查每个用户最新一笔已支付订单的金额:
WITH latest_paid AS (
SELECT user_id, amount,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) rn
FROM orders
WHERE status = 'paid'
)
SELECT u.name, lp.amount
FROM users u
LEFT JOIN latest_paid lp ON u.id = lp.user_id AND lp.rn = 1;好处是:
- 语义清晰:先筛出符合条件的右表数据,再左连
- 避免
ON中嵌套复杂表达式(如DATE_SUB(NOW(), INTERVAL 7 DAY))导致索引失效 - 方便复用:同一个子查询可被多次引用
不过要注意,CTE 在某些老版本 MySQL(FROM (SELECT ...) AS t)。
NULL 值参与计算时要主动处理,别指望数据库自动兜底
左关联后右表字段可能是 NULL,直接用于算术、字符串拼接或比较,结果大概率是 NULL——这往往不是你想要的。
常见场景和写法:
- 计数:用
COUNT(o.id)而不是COUNT(*),前者忽略 NULL,后者统计所有行 - 求和/平均:用
COALESCE(SUM(o.amount), 0)把 NULL 转成 0 - 字符串拼接:MySQL 用
CONCAT_WS(', ', u.name, COALESCE(o.order_no, '')),PostgreSQL 用u.name || COALESCE(' - ' || o.order_no, '') - 条件判断:别写
WHERE o.amount > 100,要写WHERE o.amount > 100 OR o.amount IS NULL(如果业务真需要包含 NULL)
这些细节不处理,查出来数据看着“少了一半”,debug 时容易绕远路。

















