NULL = NULL 返回 UNKNOWN,不是 TRUE;JOIN 的 ON 条件只保留判断为 TRUE 的行,因此两边均为 NULL 也不匹配,这是 SQL 标准行为,非数据库 bug。

NULL = NULL 返回 UNKNOWN,不是 TRUE
JOIN 的 ON 条件只保留判断结果为 TRUE 的行,而 NULL = NULL 在 SQL 三值逻辑中恒为 UNKNOWN,既不是 TRUE 也不是 FALSE。所以哪怕两表字段都为 NULL,这一行也不会被当作匹配成功——这不是数据库 bug,是所有主流数据库(PostgreSQL、MySQL、SQL Server、Oracle)共同遵守的 SQL 标准行为。
INNER JOIN 会直接丢弃含 NULL 的连接行
只要 ON 中任一比较字段为 NULL,整行就被跳过。例如:
SELECT * FROM orders o INNER JOIN customers c ON o.customer_id = c.id;
若某订单的 o.customer_id 是 NULL,或某客户 c.id 是 NULL,该行永远不会出现在结果里。即使两边都是 NULL,也一样被排除。
- 常见误判:看到“左表有数据、右表字段全为 NULL”,就以为是 LEFT JOIN 写错了——其实可能是 INNER JOIN 本身就把这些行过滤掉了
- 排查方法:把
INNER JOIN临时换成LEFT JOIN,再加WHERE c.id IS NULL,看是否真有匹配不上的记录
LEFT JOIN 保留左表行,但不等于“NULL 能连上”
LEFT JOIN 的作用只是保留左表所有行,并不会让 NULL 和 NULL 自动配对。结果中出现的 NULL 有两种来源:
- 左表字段原本就是
NULL(真实缺失) - 右表字段为
NULL(表示未匹配成功,是 JOIN 机制产生的占位)
这两类 NULL 语义不同,混在一起容易误读。比如 WHERE c.id IS NULL 会同时捕获“右表没匹配上”和“右表匹配上了但 c.id 本身就是 NULL”两种情况,后者往往不是你想要的。
想让 NULL 和 NULL 视为相等,必须显式写逻辑
没有数据库默认开启“NULL 安全比较”,你得自己告诉它:“我需要把两个 NULL 当作相同来处理”。常用方式有:
- 用
IS NOT DISTINCT FROM(PostgreSQL、MySQL 8.0.16+ 支持):ON o.customer_id IS NOT DISTINCT FROM c.id - 手写等价逻辑(通用):
ON (o.customer_id = c.id) OR (o.customer_id IS NULL AND c.id IS NULL) - 用
COALESCE统一兜底(注意选值):ON COALESCE(o.customer_id, -1) = COALESCE(c.id, -1),前提是-1在业务中绝不会作为合法 ID 出现
最后提醒一句:任何在 ON 子句里对字段做函数处理(如 TRIM()、UPPER()、COALESCE()),基本都会让索引失效。如果关联字段很大、性能敏感,优先考虑清洗数据或建函数索引,而不是在查询里硬转。

















