首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >一个 NULL,让你的查询"静默失败"了

一个 NULL,让你的查询"静默失败"了

原创
作者头像
OneSQL
发布2026-05-18 17:53:10
发布2026-05-18 17:53:10
840
举报

一条"灵异"的 SQL

故事是这样的。

前几天,一个金融项目 POC 现场的开发同学急匆匆地找到我,说他遇到了一个"灵异现象"。

有一条 SQL,逻辑简单到不能再简单。查询 A 表中那些 ID 不在 B 表结果集里的记录。

代码语言:javascript
复制
SELECT * FROM t1
WHERE id NOT IN (SELECT id1 FROM t2 WHERE condition);

按常理,B 表只覆盖了部分 ID,A 表应该能返回几千行数据才对。但实际执行结果却是——

空集。

没有报错。没有超时。没有异常提示。数据就像凭空蒸发了一样。

我当时就愣了一下。

这种"静默失败"往往比报错更让人头秃。因为报错至少告诉你出事了,而静默失败会让你怀疑,是不是业务真的没有数据。

坦率的讲,这个坑我见过太多次了。凶手永远只有一个。


为什么一个 NULL 能毁掉一条 SQL

先说第一个问题。

为什么一个 NULL 能毁掉一条 SQL。

经过排查,问题的根源藏在一个不起眼的细节里。t2.id1 这个列,在某些业务条件下,子查询返回的结果集中包含了 NULL 值。

正是这个 NULL,直接杀死了整个查询。

这要从 SQL 的三值逻辑说起。

在 SQL 标准中,逻辑运算的结果不是 True 和 False 两种,而是三种。

True,条件成立。False,条件不成立。Unknown,我不知道。

而 NULL 在 SQL 里的含义从来不是"无",而是未知。

当你的 SQL 写成 NOT IN (list) 时,数据库实际上把它拆解成了一长串 AND 连接的不等于条件。

假设子查询返回的结果是 1、2、NULL,那么 id NOT IN (1, 2, NULL) 等价于:

代码语言:javascript
复制
id <> 1 AND id <> 2 AND id <> NULL

麻烦就出在最后那个 id <> NULL 上。

在 SQL 规范中,任何值与 NULL 做比较,包括不等于,结果永远是 Unknown。

而由于是 AND 连接,只要其中一项是 Unknown,整个表达式就不可能是 True。

所以,不管你的 t1 表里有多少数据,只要子查询结果集里藏着一个 NULL,逻辑判定就会永远陷入 Unknown 的泥潭。

用一张图来理解。

代码语言:javascript
复制
t1 表:  id = 1, 3, 5, 7, 9
t2 子查询结果: {1, 2, NULL}

逐行判断:
  id=1:  1<>1=F → 整体=False → 不返回
  id=3:  3<>1=T, 3<>2=T, 3<>NULL=Unknown → 整体=Unknown → 不返回
  id=5:  5<>1=T, 5<>2=T, 5<>NULL=Unknown → 整体=Unknown → 不返回
  id=7:  7<>1=T, 7<>2=T, 7<>NULL=Unknown → 整体=Unknown → 不返回
  id=9:  9<>1=T, 9<>2=T, 9<>NULL=Unknown → 整体=Unknown → 不返回

结果: 0 行

每一行都死于同一个凶手。那个 id <> NULL 带来的 Unknown。

说真的,这个坑看着简单,但我见过太多人在这上面浪费半天时间了。


怎么修?两个方案

顺着上面的再聊聊怎么修。

方案一,改写为 NOT EXISTS,我强烈推荐这个。

NOT EXISTS 采用的是相关子查询逻辑,它只关心有没有匹配的行,而不受空值的逻辑陷阱干扰。

代码语言:javascript
复制
SELECT * FROM t1
WHERE NOT EXISTS (
    SELECT 1 FROM t2
    WHERE t2.id1 = t1.id AND condition
);

在同样的测试环境下,NOT EXISTS 顺利返回了 34 条记录,而 NOT IN 依然是 0 条。

方案二,强制过滤子查询的 NULL。

如果你坚持使用 NOT IN,那必须确保子查询的结果集绝对干净。

代码语言:javascript
复制
SELECT * FROM t1
WHERE id NOT IN (
    SELECT id1 FROM t2
    WHERE id1 IS NOT NULL AND condition
);

加一个 IS NOT NULL,世界清静了。


还有一个进阶坑:优化器改写

回到 NOT IN 这块,还有一个进阶坑。

你以为加上 IS NOT NULL 就万事大吉了?还有高级坑在后面等着。

在普通 IN 子查询中,如果确认没有空值,优化器有时会将外部条件比如下推到子查询内部来提升性能。

但对于 NOT IN,如果子查询列包含 NULL,这种下推是非等价的。

子查询不含空值时,下推条件是安全的。子查询包含空值时,下推会导致逻辑完全错误,原本应该返回空集的 SQL 可能会错误地返回数据。

所以,除非你百分之百确定子查询列定义了 NOT NULL 约束,否则永远优先使用 NOT EXISTS 代替 NOT IN。

这应该是每个 DBA 和高级开发的肌肉记忆。


总结:三种写法,一张表说清楚

说到这个,我整理了一个对比表。

写法

安全吗

性能

推荐度

NOT IN (SELECT ...)

含 NULL 即翻车

一般

不推荐

NOT IN (SELECT ... WHERE col IS NOT NULL)

安全

一般

可用

NOT EXISTS (SELECT 1 ...)

天然免疫 NULL

通常更优

首选

SQL 里的 NULL 绝不代表无,它代表的是未知。在 NOT IN 的语境下,这种未知具有极强的传染性,能瞬间瘫痪你的业务逻辑。

下次发现数据莫名其妙查不到,先去看看子查询里是不是混进了脏数据。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一条"灵异"的 SQL
  • 为什么一个 NULL 能毁掉一条 SQL
  • 怎么修?两个方案
  • 还有一个进阶坑:优化器改写
  • 总结:三种写法,一张表说清楚
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档