NOT IN 为什么查不出数据:NULL 与三值逻辑
很多“排除名单”查询看起来很直白:候选表里有 A、B、C、D,黑名单里有 A、B,于是使用 NOT IN 得到 C、D。问题是,黑名单一旦混入一行 NULL,同一条 SQL 可能一个结果也没有。
原因不在 NOT IN 自己偷偷改了条件,而在 SQL 的比较结果有三种:TRUE、FALSE 和 UNKNOWN。NULL 表示未知值,任何普通值与 NULL 比较都不会得到 TRUE 或 FALSE,而会得到 UNKNOWN。WHERE 只保留条件为 TRUE 的行,所以 UNKNOWN 会和 FALSE 一起被过滤。
先把数据固定下来
下面的独立 SQLite 内存数据库只用来复现语义。候选表有四个代号,排除表先放 A、B。
— SQL —
DROP TABLE IF EXISTS candidates;
DROP TABLE IF EXISTS blocked;
CREATE TABLE candidates (candidate TEXT NOT NULL);
CREATE TABLE blocked (value TEXT);
INSERT INTO candidates(candidate) VALUES (‘A’), (‘B’), (‘C’), (‘D’);
INSERT INTO blocked(value) VALUES (‘A’), (‘B’);
— SQL 结束 —
没有 NULL 时,NOT IN 符合直觉
SELECT candidate FROM candidates WHERE candidate NOT IN (SELECT value FROM blocked) ORDER BY candidate;
结果是 C、D。对 C 来说,C 不等于 A 且 C 不等于 B,整个 NOT IN 条件为 TRUE。A、B 命中名单,条件为 FALSE。
把查询拆成逻辑表达式可以看得更清楚:x NOT IN (a, b) 等价于 NOT (x = a OR x = b)。只要名单中的某一项匹配,里面的 OR 就是 TRUE,取 NOT 后变成 FALSE。
NULL 让结果进入 UNKNOWN
DELETE FROM blocked;
INSERT INTO blocked(value) VALUES (‘A’), (‘B’), (NULL);
SELECT candidate FROM candidates WHERE candidate NOT IN (SELECT value FROM blocked) ORDER BY candidate;
这次结果为空。对 C 而言,C = A 是 FALSE,C = B 是 FALSE,C = NULL 是 UNKNOWN。FALSE OR FALSE OR UNKNOWN 仍为 UNKNOWN;外层 NOT 之后还是 UNKNOWN。WHERE 不会保留 UNKNOWN。
A、B 仍然会被排除,因为它们分别与 A、B 得到 TRUE,OR 的结果先确定为 TRUE,再取 NOT 得到 FALSE。D 和 C 一样,都会卡在 UNKNOWN 上。
可以直接把逐行结果打印出来:
SELECT candidate, candidate NOT IN (SELECT value FROM blocked) AS not_in_result FROM candidates ORDER BY candidate;
不同数据库对布尔值的显示形式可能不同,但判断依据一致:只有 TRUE 能通过 WHERE。
SUM、COUNT 的经验不能套过来
COUNT(column) 忽略 NULL、COUNT(*) 统计行,这些是聚合函数的规则。NOT IN 遇到 NULL 则是三值逻辑规则。看到一列能存 NULL,不能直接推断所有 SQL 操作都会忽略它。应该先明确当前表达式是比较、逻辑运算还是聚合。
NOT EXISTS 更适合表达“没有匹配行”
如果业务意思是“候选值不存在于排除表”,可以写成相关子查询:
SELECT c.candidate FROM candidates AS c WHERE NOT EXISTS (SELECT 1 FROM blocked AS b WHERE b.value = c.candidate) ORDER BY c.candidate;
这条语句只关心是否找到一行真正相等的值。名单里的 NULL 不会与 C 或 D 匹配,因此 C、D 能保留下来;A、B 仍会被排除。
NOT EXISTS 不会自动解决所有数据问题。若 blocked.value 的业务含义不允许未知值,仍应在写入、约束或清洗阶段处理 NULL。查询写法和数据质量是两层责任。
什么时候可以继续用 NOT IN
当排除子查询的列有可靠的 NOT NULL 约束,或者子查询显式过滤了 NULL,NOT IN 可以保持简洁:
SELECT candidate FROM candidates WHERE candidate NOT IN (SELECT value FROM blocked WHERE value IS NOT NULL) ORDER BY candidate;
这相当于先把未知项排除,再做集合差集。若你无法证明子查询不会返回 NULL,NOT EXISTS 通常更容易让读者按“是否存在匹配行”理解。
把验证放进实验
本文配套的 NULL 真值实验室提供四种场景:普通 A、B 名单,A、B 加 NULL,只有 NULL,以及空名单。每行同时展示 NOT IN 和 NOT EXISTS 的结果,并标出 TRUE、FALSE、UNKNOWN。WHERE 结果只保留 TRUE,所以可以直接观察 UNKNOWN 被丢弃的过程。
实验使用固定的 SQLite 内存数据库和小型字符串集合,没有连接生产库,也没有把样例当成性能结论。真实系统还要检查列约束、数据清洗、索引和执行计划。参考 SQLite 官方 SQL Language Expressions 中的 IN、NOT IN、NULL 规则,以及 PostgreSQL 官方 Subquery Expressions 中的 EXISTS 说明。正文不设置外部可点击链接,以适应社区链接白名单。
用真值表对照 NOT IN 与 NULL,理解 UNKNOWN。
- 点赞
- 收藏
- 关注作者
评论(0)