昨天写了个 SQL,逻辑明明没毛病,数据就是查不出来。排查半天,元凶竟是
NULL。
一、现象:我的数据呢?
先看我写的 SQL,很简单:
SQL
WITH tmp_union3 AS (
SELECT * FROM tmp_union
UNION ALL
SELECT * FROM tmp_union2
WHERE id NOT IN (
SELECT DISTINCT id FROM tmp_union
)
)
SELECT * FROM tmp_union3 WHERE id = '23009724';
已知:
tmp_union里没有'23009724'tmp_union2里明明有'23009724'
按道理,NOT IN 应该放行,然后 UNION ALL 合并,最后查出来。
结果:查无此记录。

二、排查:问题出在哪?
先单独跑 NOT IN 这一段:
SQL
SELECT * FROM tmp_union2
WHERE id NOT IN (SELECT DISTINCT id FROM tmp_union);
返回:空结果集。
'23009724' 明明不在 tmp_union 里,凭啥被过滤了?
再查一下 tmp_union 的数据:
SQL
SELECT id FROM tmp_union;

破案了------tmp_union 里有 NULL。
三、原理:一个 NULL,全军覆没
在 PostgreSQL 里,没有unknown,只有null,NULL 代表"未知"
当 NOT IN 的右侧出现 NULL 时:
SQL
'2026' NOT IN (NULL, '2025', '2026')
数据库实际比较:
SQL
'2026' = '2025' → FALSE
'2026' = NULL → NULL -- 不是 FALSE,是 NULL!
关键点来了:
NOT IN 的本质是 AND 串联的等值比较
Plain
'2026' <> NULL AND '2026' <> '2025' AND ...
在 PG 里,NULL AND FALSE 的结果是 NULL,NULL AND TRUE 也是 NULL。
而 WHERE 子句只认 TRUE,NULL 被当成不满足条件。
所以只要 NOT IN 的右侧集合里有一个 NULL,整个条件就塌了,所有行都被过滤。
不是一个,是全部。这就是黑洞。

四、修复:三选一
方法 1:过滤 NULL(改动最小)
SQL
WHERE id NOT IN (
SELECT DISTINCT id FROM tmp_union
WHERE id IS NOT NULL -- 加这一行
)
方法 2:改用 NOT EXISTS(推荐)
SQL
SELECT * FROM tmp_union2 t2
WHERE NOT EXISTS (
SELECT 1 FROM tmp_union t1
WHERE t1.id = t2.id
);
NOT EXISTS 不受 NULL 影响,而且性能通常更好。
方法 3:LEFT JOIN 取差集
SQL
SELECT t2.*
FROM tmp_union2 t2
LEFT JOIN tmp_union t1 ON t1.id = t2.id
WHERE t1.id IS NULL;

五、口诀
看到
NOT IN,先防NULL;能写EXISTS,别写IN
六、总结
这次踩坑就一句话:
NOT IN 遇到 NULL,不是漏几条,是全部消失。
而且不会报错,悄无声息。
你以为是逻辑问题,其实是 NULL 在搞鬼。
以后写 SQL,遇到 NOT IN 多长个心眼,或者直接换成 NOT EXISTS,一劳永逸。
你遇到过类似的 NULL 陷阱吗?评论区聊聊,一起排雷。