LEFT JOIN 返回的结果莫名少了行?原因多半是 WHERE 子句中混入了右表的筛选条件,把本该保留的 NULL 行全部过滤掉了。正确的做法是将右表条件移至 ON 子句;多层 JOIN 时更需留意,外层 WHERE 一旦涉及右表字段,整行就会直接消失。

WHERE 里写了右表字段,LEFT JOIN 就失效了
最容易被忽略的陷阱:把本应属于“匹配逻辑”的条件,随手丢进了 WHERE。例如写 LEFT JOIN orders o ON u.id = o.user_id WHERE o.status = 'paid',看起来是想查询“已支付订单 + 无订单的用户”,实际上只保留了那些有订单且状态为 'paid' 的行——所有 o.status IS NULL 的行(也就是没有订单的用户)全被排除掉了。
原因很简单:WHERE 是在 JOIN 完成后的中间结果集上进行过滤,而 NULL = 'paid' 永远不会成立,整行直接被剔除。正确的做法是把这个条件移到 ON 里:LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid'。这样一来左表行全部保留,未匹配上的字段仍然是 NULL,但行不会丢失。
多层嵌套尤其危险。比如三层 LEFT JOIN,只在最外层 WHERE 加一个 t3.type = 'active',前两层的“左表全保留”就形同虚设——只要 t3 未匹配上,整行就直接消失,连前面两层的结果也跟着被葬送。
连接字段本身就不匹配
ON 条件不成立时,LEFT JOIN 只会默默填上 NULL,既不报错也不提示。常见情况有三种:
NULL参与等值比较永远返回UNKNOWN,而不是TRUE。所以u.id = o.user_id里只要有一端是NULL,匹配必然失败。- 类型不一致:比如
users.id是INT,orders.user_id却是VARCHAR,里面存了'123 '(带空格)。隐式转换后比较立刻出错。 - 不可见字符:用
SELECT user_id, HEX(user_id) FROM orders WHERE user_id LIKE '%123%'查看HEX结果,看到20就是空格,09是制表符。这类字符肉眼根本无法察觉。
安全的写法是显式处理:ON u.id = CAST(o.user_id AS SIGNED) 或者 ON u.id = TRIM(o.user_id)。注意不要在 ON 里用 UPPER() 这类函数——索引会失效,还可能因大小写规则导致匹配失败。
用 ISNULL 或 COALESCE 填空时踩坑
填空并不是万能解药,函数选错或参数用错,可能掩盖问题甚至引入新 Bug:
ISNULL是 SQL Server 特有的,只接受两个参数,返回类型完全继承第一个参数——ISNULL(name, 'not_found_yet')在name是VARCHAR(10)时会截断成'not_found_'。- MySQL 里
ISNULL()是判断是否为NULL的函数,不能用来填空;得用IFNULL()或标准COALESCE()。 COALESCE支持多参数,类型推导更严谨:COALESCE(o.total, o.backup_total, 0)。但要注意NULL参与任何算术运算(比如+、-)结果仍然是NULL,填空必须在计算前完成。
嵌套 JOIN 中某一层先断掉,结果全是 NULL
写 SELECT * FROM t1 LEFT JOIN (t2 LEFT JOIN t3 ON ...) ON ... 时,如果子查询或括号内的 JOIN 本身因为条件过严返回空集,外层 LEFT JOIN 就只能拿到一堆 NULL——看起来像是“整个结果为空”,其实是中间某层先断了链。
调试时不要一上来就改 SQL,先验证基础事实:
- 查左表本身有没有数据:
SELECT COUNT(*) FROM t1 - 单独查右表匹配是否存在:
SELECT * FROM t2 WHERE ref_id = 123(挑一个t1里已知的 ID) - 用
SELECT *跑一遍原始LEFT JOIN,观察右表字段是否批量为NULL——这是匹配失败最直观的信号。 - 拆开验证:
SELECT * FROM t2 LEFT JOIN t3 ON ...先跑一次,确认是否有数据;再拿结果和t1关联。
真正难调试的,从来不是“为什么有 NULL”,而是“为什么没数据却看起来像有数据”。比如多层嵌套中某一层先因为条件过严返回空集,外层只能拿到一堆 NULL,但你盯着最终结果看,根本看不出中间断在哪一环。
