一次被 `NOT IN` 坑到的排查记录

32 阅读2分钟

今天在查数据时,遇到了一个让我一度怀疑数据库出问题的“诡异”现象。

背景很简单:
我有两张表,t1t2
t1 表里有 1677 条记录,每条都有一个非空的 code 字段。
我想找出那些 code 不在 t2 表中的记录。

于是,我先试了下 IN

SELECT * FROM t1 WHERE code IN (SELECT code FROM t2);

结果返回了 1675 条 —— 看起来没问题,说明有 2 条没匹配上。

那接下来自然就是用 NOT IN 找出这 2 条“漏网之鱼”:

SELECT * FROM t1 WHERE code NOT IN (SELECT code FROM t2);

结果……0 条?!

这不对啊!1677 - 1675 = 2,怎么 NOT IN 一条都捞不出来?

一开始我还以为是数据重复、类型不一致,甚至怀疑是不是缓存或者事务隔离的问题。但反复检查后发现,t1.code 都是干净的非空字符串,t2.code 看起来也正常。

直到我突然想起一个老生常谈但极易被忽略的坑:NOT IN 遇到 NULL 会失效

原来,t2.code 字段里其实混进了几条 NULL 值。而 SQL 的逻辑是这样的:

如果子查询的结果中包含哪怕一个 NULL,那么 NOT IN 的整个条件就会变成“未知”(UNKNOWN),而 SQL 只返回 WHERE 条件为 TRUE 的行,UNKNOWN 被当作 FALSE 处理 —— 所以最终一条都不返回。

举个例子:

SELECT * FROM t1 WHERE code NOT IN (SELECT code FROM t2);

等价于:

SELECT * FROM t1 WHERE code <> val1 AND code <> val2 AND ... AND code <> NULL;

code <> NULL 永远是 UNKNOWN,整个表达式也就永远不成立。

知道原因后,解决就简单了。只要在子查询里排除掉 NULL

SELECT * FROM t1 
WHERE code NOT IN (SELECT code FROM t2 WHERE code IS NOT NULL);

这次,稳稳地返回了 2 条,正是我想要的结果。


小结

  • INNULL 不敏感,能正常工作;
  • NOT INNULL 极其敏感,一旦子查询含 NULL,结果可能完全“消失”;
  • 更安全的替代方案还有 NOT EXISTSLEFT JOIN ... IS NULL,它们天然规避了 NULL 陷阱。

这次踩坑再次提醒我:看似简单的 SQL,背后可能藏着逻辑陷阱。尤其是在处理来自 ETL、外部系统或历史遗留表的数据时,NULL 总是悄无声息地埋雷。

下次再写 NOT IN,我一定先问一句:子查询里有没有 NULL


希望这篇记录能帮到同样被 NOT IN 背刺过的你。