LEFT JOIN 查出 NULL 是正常行为,因右表无匹配时字段填 NULL;若 WHERE 中过滤右表字段会变相转为内连接,应将条件移至 ON 子句。

LEFT JOIN 为什么查出来一堆 NULL?
LEFT JOIN 的左表记录一定会保留,右表没匹配上的字段全填 NULL —— 这不是 bug,是设计行为。常见于你写了 LEFT JOIN 却忘了在 WHERE 里加右表条件,结果把本该保留的左表空匹配行给过滤掉了。
比如想查“所有用户及其订单数”,但写成:
SELECT u.id, COUNT(o.id) FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.status = 'paid';
这实际变成内连接:WHERE 中对右表 o.status 的限制,会把没订单或订单非 'paid' 的用户全踢掉。正确做法是把右表过滤条件挪到 ON 子句:
SELECT u.id, COUNT(o.id) FROM users u LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid';
-
ON后面可以跟多个条件,用AND连接,它只影响关联逻辑 -
WHERE是对最终结果集过滤,一旦涉及右表字段,就可能破坏 LEFT JOIN 的语义 - 如果真要按右表状态分组统计,
COUNT(o.id)比COUNT(*)安全,因为前者自动忽略NULL
ON 和 USING 有什么区别?能混用吗?
USING 是语法糖,仅当左右表连接字段名完全相同时才可用,比如都叫 user_id;而 ON 更通用,支持任意表达式和不同名字段。
下面两个等价:
SELECT * FROM users u LEFT JOIN orders o USING (user_id);
SELECT * FROM users u LEFT JOIN orders o ON u.user_id = o.user_id;
-
USING会让连接字段在结果中只出现一次(自然合并),ON则左右字段都保留,名字重复时需用别名区分 - 不能在同一个
JOIN中混用ON和USING,MySQL 会报错:ERROR 1064 (42000) - 如果连接字段类型不一致(比如左表是
INT,右表是VARCHAR),USING可能隐式转换失败,ON更可控
LEFT JOIN 太慢怎么办?索引怎么建?
LEFT JOIN 性能瓶颈通常不在“左表扫描”,而在“右表对每条左表记录的匹配查找”。没索引时,右表会被反复全表扫描。
关键原则:右表的 ON 条件字段必须有索引,且顺序要匹配。例如:
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid'
- 优先给右表建联合索引:
INDEX (user_id, status),而不是单列INDEX (user_id) - 如果查询还带
ORDER BY o.created_at,且数据量大,考虑扩展为INDEX (user_id, status, created_at)覆盖排序 - 左表字段一般不需要额外索引(除非本身有
WHERE或ORDER BY),但主键或唯一键天然有效 -
EXPLAIN看type列:要是ALL或index,基本说明右表没走好索引
多个 LEFT JOIN 顺序会影响结果吗?
会,而且影响很实在。MySQL 按从左到右顺序执行 JOIN,前一个 JOIN 的结果作为下一个 JOIN 的“左表”。这意味着中间结果膨胀或过滤,会直接影响后续关联范围。
比如:users → LEFT JOIN orders → LEFT JOIN order_items,如果某用户有 5 个订单,每个订单有 3 个商品,那中间结果就有 15 行;但如果写成 users → LEFT JOIN order_items(没经过 orders 关联),就会因缺少外键约束而产生笛卡尔积或 NULL 匹配错乱。
- 务必保证 JOIN 链路符合业务主外键关系:先连主实体(如
orders),再连子实体(如order_items) - 避免跨层级直接关联(如
users LEFT JOIN order_items),除非你明确做了中间字段补全或用了子查询 - 如果必须多层关联且担心膨胀,可先用派生表或 CTE 把右表聚合好(如预计算每个订单的商品数),再 JOIN,减少中间行数
最常被忽略的一点:LEFT JOIN 的“左”是相对当前这一级而言的,不是整个 SQL 的第一个表。嵌套越深,越容易误以为某个表还是“原始左表”,其实它早被前面的 JOIN 改变了身份。










