null = null 返回 unknown,导致join无法匹配;需用is not distinct from、coalesce或(a.id = b.user_id) or (a.id is null and b.user_id is null)实现null安全比较。

NULL = NULL 返回 UNKNOWN,不是 TRUE
JOIN 的 ON 条件只保留表达式求值为 TRUE 的行,而 NULL = NULL 在 SQL 三值逻辑中结果是 UNKNOWN,不满足匹配要求。哪怕左右两边都是 NULL,也不会连上——这不是数据库 bug,是 SQL 标准强制行为。
典型表现:左表某行的 id 是 NULL,右表某行的 user_id 也是 NULL,但 LEFT JOIN ... ON a.id = b.user_id 后,右表字段依然全为 NULL(即“未匹配”状态),而非填充那行数据。
怎么让两个 NULL 真正匹配上?
必须显式绕过默认比较逻辑,不能依赖 =。不同数据库支持方式不同:
- PostgreSQL / MySQL 8.0.16+:用
IS NOT DISTINCT FROM,例如ON a.id IS NOT DISTINCT FROM b.user_id - SQL Server / Oracle(旧版)/ 大多数 MySQL 版本:改用
COALESCE统一兜底值,例如ON COALESCE(a.id, -1) = COALESCE(b.user_id, -1),注意选一个业务中绝对不出现的值 - 通用写法(兼容性最强但难优化):
ON (a.id = b.user_id) OR (a.id IS NULL AND b.user_id IS NULL)
为什么把条件写在 WHERE 里会让 NULL 匹配问题更隐蔽?
很多人想“查有用户但没订单的记录”,写成 LEFT JOIN orders o ON u.id = o.user_id WHERE o.id IS NULL,这本身没问题;但如果顺手加上 AND o.status = 'paid',整条语句就失效了——因为 o.status = 'paid' 在 o.id IS NULL 时恒为 UNKNOWN,该行直接被过滤掉。
真正该做的:
- 右表的业务筛选(如
status、deleted_at IS NULL)一律移到ON子句里:LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid' - 确保用于
WHERE ... IS NULL判断的字段确实是可空的、且能代表“未匹配”,优先选右表主键或外键字段(如o.id),别用o.name这类可能被设为NULL也可能被设为空字符串的字段
关联字段为 NULL 的真实来源往往被忽略
别急着改 SQL,先确认 NULL 是不是数据本身的问题:
- 查右表外键字段是否大量为
NULL:SELECT COUNT(*) FROM orders WHERE user_id IS NULL - 看左表关联字段有没有被意外设为
NULL:SELECT COUNT(*) FROM users WHERE id IS NULL - 检查 ETL 或导入过程是否把空字符串、占位符(如
'N/A')错误转成了NULL - 如果字段类型是
VARCHAR,用HEX(user_id)查不可见字符,20是空格,09是制表符——这些都会让隐式转换失败,间接导致“像 NULL 一样不匹配”
最麻烦的情况是:你修复了 NULL,但没意识到 ON 里还混着 TRIM() 或 UPPER() ——这类函数不仅让索引失效,还可能因大小写规则或截断逻辑引入新的不匹配。处理 NULL 安全比较,优先靠语义清晰的 IS NOT DISTINCT FROM 或 COALESCE,而不是靠清洗函数兜底。










