我写了个 SQL 想查出状态为空的记录、用了 WHERE status = NULL,结果一行都查不出来,我又写了个 NOT IN 子查询,这次更怪、整个结果集凭空变成了空,排查半天才明白 SQL 里的 NULL 根本不能用等号去比的深度复盘
这是一次让我对"'不知道'参与的比较,结果也是'不知道'"有了刻骨认知的事故。我有张表,有些记录的 status 字段是空(NULL),我想把这些"状态为空"的记录查出来。我很自然地写了 WHERE status = NULL——状态等于空嘛,顺理成章。
可结果让我懵了:这条查询一行都没返回,哪怕表里明明有一堆 status 为 NULL 的记录。我以为是数据问题,直接查全表,那些 NULL 记录清清楚楚就在那。我又换了个思路,想查"status 不在某个状态列表里"的记录,写了个 WHERE status NOT IN (SELECT status FROM ...) 的子查询——结果更诡异:整个结果集凭空变成了空,一行都没有,可我明明知道有大把记录该被选中。我对着这两条"看着没毛病、却查不出东西"的 SQL 百思不得其解,直到去补了 SQL 对 NULL 的处理规则,才恍然大悟:SQL 里的 NULL 不是一个"值",而是表示"未知(unknown)";而任何东西和 NULL 用比较运算符(=、!=、< 等)比较,结果都不是 true、也不是 false,而是第三种值"unknown"(SQL 用的是三值逻辑)。WHERE 只返回结果为 true 的行,unknown 被当作"不满足"过滤掉——所以 status = NULL 永远不为 true、一行都查不出;而 NOT IN 的列表里只要混进了一个 NULL,整个 NOT IN 表达式就再也不可能为 true(因为"不等于那个未知"是 unknown),于是结果集整个塌成空。
故障现场:= NULL 永不为真、NOT IN 含 NULL 全空
我把这两个"NULL 把查询搞空"的现象还原出来,问题一目了然:
-- 想查 status 为空的记录: 用 = NULL —— 一行都查不出!
SELECT * FROM orders WHERE status = NULL; -- ✗ 返回空(即使有大量 status 为 NULL 的行)
-- 原因: status = NULL 的结果不是 true 也不是 false, 而是 unknown
-- WHERE 只保留结果为 true 的行 → unknown 被过滤 → 一行不返回
-- 正解: 判断 NULL 要用 IS NULL / IS NOT NULL(不是 = / !=)
SELECT * FROM orders WHERE status IS NULL; -- ✓ 查出 status 为空的行
SELECT * FROM orders WHERE status IS NOT NULL; -- ✓ 查出 status 非空的行
-- 更隐蔽的坑: NOT IN 子查询里混进 NULL → 整个结果集变空!
SELECT * FROM orders
WHERE status NOT IN (SELECT status FROM blacklist); -- ✗ 若 blacklist 里有 NULL → 全空!
-- 原因: NOT IN (a, b, NULL) 等价于 status<>a AND status<>b AND status<>NULL
-- 而 status<>NULL 永远是 unknown → 整个 AND 链永远不为 true → 全被过滤
-- 正解: NOT IN 子查询过滤掉 NULL, 或改用 NOT EXISTS
SELECT * FROM orders o
WHERE NOT EXISTS (SELECT 1 FROM blacklist b WHERE b.status = o.status); -- ✓ 不受 NULL 影响
-- 或: ... NOT IN (SELECT status FROM blacklist WHERE status IS NOT NULL);
-- 其他被 NULL 坑的地方:
-- COUNT(列) 不统计 NULL 行(COUNT(*) 才算所有行)
-- SUM/AVG 忽略 NULL; 字符串拼接含 NULL 结果可能为 NULL
-- a = b 当 a、b 都是 NULL 时也是 unknown(NULL 不等于 NULL!)
SELECT NULL = NULL; -- 结果是 NULL(unknown), 不是 true!
看着"= NULL 永远空、NOT IN 含 NULL 全空、连 NULL = NULL 都不是 true",我才彻底明白:SQL 的 NULL 不是"一个表示空的值",而是表示"未知"——它代表"这里有个值,但我不知道是什么"。而 SQL 用的是三值逻辑(true / false / unknown):任何拿 NULL 去做的比较(=、!=、<……),结果都是 unknown(因为"未知"和任何东西比,结果当然也是未知)。而 WHERE、JOIN ON 这些只保留结果为 true 的行,unknown 被当成"不满足"丢弃。于是 status = NULL 永远不为 true(查不出 NULL 行);NOT IN 列表含 NULL 时,"不等于那个未知"也是 unknown,整个条件永远不为 true,结果塌成空;甚至 NULL = NULL 也不是 true(两个"未知"是否相等,当然也"未知")。我以为 NULL 是个能用等号比较的普通空值,其实它是个"未知",而和未知比较,只会得到未知——既非真也非假,在 WHERE 里就等于被否定。
第一件事:搞懂 SQL 的三值逻辑——NULL 是未知,比较结果是 unknown
冷静下来,我去把"SQL 的 NULL 与三值逻辑"这一课认真补了,才明白这些"查询变空"的根源:
【SQL 的 NULL 与三值逻辑(为什么 = NULL / NOT IN 出意外)】
NULL 不是"值", 而是表示"未知(unknown)"——"这有个值, 但不知道是啥"
SQL 用【三值逻辑】: 一个条件的结果可以是 true / false / unknown
- 任何用比较运算符(= != < > 等)和 NULL 比较, 结果都是 unknown
status = NULL → unknown(不是 false!)
status != NULL → unknown
NULL = NULL → unknown(两个未知是否相等? 未知!)
- WHERE / JOIN ON / HAVING 只保留结果为 true 的行;
unknown 和 false 一样被【过滤掉】
由此产生的经典坑:
1. WHERE x = NULL / x != NULL: 永远 unknown → 查不出任何行
→ 判 NULL 必须用 IS NULL / IS NOT NULL
2. NOT IN (含 NULL 的列表/子查询): 整个结果集变空
→ x NOT IN (a, NULL) = x<>a AND x<>NULL = (...) AND unknown = 永不 true
→ 子查询过滤掉 NULL, 或改用 NOT EXISTS(不受 NULL 影响)
3. NULL 参与运算/聚合:
- 算术: NULL + 5 = NULL; 字符串拼接含 NULL 可能整个 NULL(看数据库)
- 聚合: COUNT(列) 不算 NULL 行(COUNT(*) 算所有); SUM/AVG/MAX 忽略 NULL
- 排序: NULL 的排序位置因数据库而异(用 NULLS FIRST/LAST 控制)
4. UNIQUE 约束: 多数数据库允许多个 NULL(NULL 不等于 NULL)
正确处理 NULL:
- 判空: IS NULL / IS NOT NULL(绝不用 = / !=)
- 取默认: COALESCE(x, 默认值) / IFNULL / ISNULL
- NOT IN 防 NULL: 过滤掉 NULL 或用 NOT EXISTS
- 建表时该非空的列加 NOT NULL 约束, 从源头减少 NULL 带来的意外
核心: NULL 是"未知", 和未知比较得到的还是"未知"(既非真也非假),
而 WHERE 把"非真"都过滤掉——所以涉及 NULL 必须特殊处理
这一下点醒了我:我把 SQL 的 NULL 当成了"一个表示空的普通值"、以为能用 = 去比;可 NULL 表示的是"未知",而 SQL 用三值逻辑——任何和"未知"的比较,结果都是"未知(unknown)",既不是 true 也不是 false。而 WHERE 只保留 true 的行,把 unknown 当"不满足"过滤掉。所以 = NULL 永远查不出、NOT IN 混进 NULL 就全空——这些不是 bug,是三值逻辑的必然。判断 NULL 得用专门的 IS NULL,而不是用对付普通值的 =。不是 SQL 不讲理,是我用"对待已知值"的方式,去对待了一个"表示未知"的特殊标记。
第二件事:正解——判空用 IS NULL,NOT IN 防 NULL,源头加 NOT NULL
找到根因,正解就清晰了:判断 NULL 一律用 IS NULL / IS NOT NULL(绝不用 =/!=);NOT IN 的子查询/列表要过滤掉 NULL、或干脆改用 NOT EXISTS(不受 NULL 影响);取值兜底用 COALESCE;建表时该非空的列加 NOT NULL 约束,从源头减少 NULL 带来的意外。
-- 判空: 用 IS NULL / IS NOT NULL, 不用 = / !=
SELECT * FROM orders WHERE status IS NULL; -- ✓
SELECT * FROM orders WHERE status IS NOT NULL; -- ✓
-- 既要非某值、又要包含 NULL 行: 显式处理 NULL(!= 会漏掉 NULL 行)
SELECT * FROM orders WHERE status != 'done'; -- ✗ 会漏掉 status 为 NULL 的行!
SELECT * FROM orders WHERE status != 'done' OR status IS NULL; -- ✓ 把 NULL 行也带上
-- NOT IN 防 NULL: 过滤 NULL, 或用 NOT EXISTS
SELECT * FROM orders o WHERE o.status NOT IN
(SELECT status FROM blacklist WHERE status IS NOT NULL); -- ✓ 过滤掉 NULL
-- 更推荐 NOT EXISTS(天然不受 NULL 影响、且常更高效)
SELECT * FROM orders o WHERE NOT EXISTS
(SELECT 1 FROM blacklist b WHERE b.status = o.status); -- ✓
-- 取值兜底: COALESCE(取第一个非 NULL) / IFNULL(MySQL) / ISNULL(SQLServer)
SELECT COALESCE(status, '未知') AS status FROM orders; -- NULL → '未知'
SELECT COALESCE(amount, 0) + 100 FROM orders; -- 防 NULL+数 = NULL
-- 聚合注意: COUNT(*) 算所有行, COUNT(列) 不算该列为 NULL 的行
SELECT COUNT(*), COUNT(status) FROM orders; -- 两个数可能不同!
-- 源头治理: 该非空的列加 NOT NULL 约束 + 默认值, 减少 NULL 的产生
-- CREATE TABLE orders ( status VARCHAR(20) NOT NULL DEFAULT 'pending', ... );
-- 排序控制 NULL 位置(数据库间默认不同)
SELECT * FROM orders ORDER BY status NULLS LAST; -- NULL 排最后(PG/Oracle)
这套做法的精髓,是承认 NULL 是"未知"这个特殊性质,在涉及它的每个地方都显式、特殊地处理它:判空用专门的 IS NULL;想"不等于某值但保留 NULL 行"就显式 OR ... IS NULL;NOT IN 防 NULL 或换 NOT EXISTS;取值用 COALESCE 兜底;聚合时清楚 COUNT(列) 会跳过 NULL。而最治本的,是建表时给该非空的列加 NOT NULL 约束——从源头不让意外的 NULL 进来,后面就少很多三值逻辑的坑。不是要消灭 NULL(它表达"未知"有其用),而是别用对待普通值的方式对待它。
【处理 SQL 的 NULL, 几条原则】
1. 判空一律 IS NULL / IS NOT NULL, 绝不用 = / !=(那永远是 unknown)
2. != 某值会漏掉 NULL 行; 要带上 NULL 行就显式 OR x IS NULL
3. NOT IN 子查询/列表防 NULL: 过滤掉 NULL, 或改用 NOT EXISTS
4. 取值兜底用 COALESCE/IFNULL; 防 NULL 参与运算(NULL+数=NULL)
5. 聚合: COUNT(*) 算所有行, COUNT(列)/SUM/AVG 跳过 NULL
6. 源头: 该非空的列加 NOT NULL 约束+默认值, 减少 NULL 的产生
7. 记住: NULL 是"未知", 和它比较结果是 unknown, WHERE 把非真都过滤
第三件事:其他"特殊值打破常规比较/逻辑"的同类坑
顺着"表示'未知/特殊'的值不遵守常规比较"这条线,我把同类的坑都梳理了一遍:
第一个,浮点 NaN 不等于自己。和 SQL NULL 异曲同工——NaN == NaN 为 false(各语言),判断要用 isNaN 等专门方法,别用 ==。
第二个,JS 的 null/undefined 与 == 的隐式转换。null == undefined 为 true 但 null === undefined 为 false,各种隐式转换规则不直观,要用严格相等并显式判空。
第三个,空字符串/0/false 被当成"空"误判。用真值判断("if (x)")时,0、""、false 都被当假,容易误伤合法值。要显式判断到底要排除什么。
第四个,不同系统对"缺失"表示不一(NULL/空串/默认值/哨兵值)。跨系统时"没有值"的表示不统一,比较和判空容易出错。要在边界统一规范化。
第四件事:NULL 在各种操作下的行为,一张表对照
我把 NULL 在常见 SQL 操作下的行为整理成一张表,这是我现在写涉及 NULL 的查询时的依据:
| 操作 | 对 NULL 的行为 | 正确做法 |
|---|---|---|
| x = NULL / x != NULL | 结果 unknown, 永不为 true | 用 IS NULL / IS NOT NULL |
| x != '某值' | 漏掉 x 为 NULL 的行 | 加 OR x IS NULL |
| NOT IN (含 NULL) | 整个结果集变空 | 过滤 NULL 或 NOT EXISTS |
| NULL + 数 / 拼接 | 结果常为 NULL | COALESCE 兜底 |
| COUNT(列) | 不统计 NULL 行 | 要算所有行用 COUNT(*) |
| NULL = NULL | unknown(不是 true) | 判相等也要特殊处理 NULL |
| UNIQUE 约束 | 多数允许多个 NULL | 需唯一非空加 NOT NULL |
这张表让我看清:NULL 几乎在每个常规操作里都"不按常理出牌"——比较得 unknown、被 != 漏掉、让 NOT IN 塌空、被 COUNT 跳过。这都源于它表示"未知"、SQL 用三值逻辑。所以涉及 NULL 的每一处,都得想一想它会不会出意外、要不要特殊处理。
第五件事:我对"NULL 就是个空值"的几个想当然
这次事故,本质是我把 NULL 当成了"一个能正常比较的空值"。把这些想当然列出来,每一条都值得警惕:
| 我曾经的想当然 | 事故教我的真相 |
|---|---|
| "查空值用 WHERE x = NULL" | = NULL 永远 unknown, 查不出; 要用 IS NULL |
| "x != '某值' 会包含所有别的行" | 会漏掉 x 为 NULL 的行; 要 OR x IS NULL |
| "NOT IN 子查询很安全" | 子查询含 NULL 会让整个结果集变空 |
| "NULL = NULL 应该是 true" | 是 unknown; 两个未知是否相等也未知 |
| "COUNT(列) 和 COUNT(*) 一样" | COUNT(列) 不算 NULL 行, 两者可能不同 |
| "NULL 就是个表示空的普通值" | 它表示未知, SQL 用三值逻辑, 比较得 unknown |
第六件事:写涉及可空列的 SQL 时,我现在的自检习惯
现在每当我写涉及可空列的 SQL、或排查"查询结果异常地空/少了行",我都会先按这张图问自己:
这张图的精髓,是"涉及可空列就先想 NULL 怎么处理;判空用 IS NULL、NOT IN 防 NULL、!= 补 IS NULL、取值用 COALESCE"。写时就判空用 IS NULL、NOT IN 改 NOT EXISTS、该非空的列加 NOT NULL、排查就看结果异常空是不是 = NULL 或 NOT IN 含 NULL 在作怪。这套习惯,让我从"NULL 是个能比较的空值"变成了"NULL 是未知、涉及它必须特殊处理"——核心始终是:SQL 的 NULL 不是值而是表示"未知",SQL 用三值逻辑(true/false/unknown),任何用比较运算符(=/!=/>)和 NULL 比较结果都是 unknown(NULL=NULL 也是 unknown),而 WHERE/JOIN ON/HAVING 只保留结果为 true 的行、把 unknown 当不满足过滤掉;所以 status=NULL 永远查不出、NOT IN 列表含 NULL 整个结果集变空、x!='值' 会漏掉 NULL 行、COUNT(列) 跳过 NULL;正解是判空用 IS NULL/IS NOT NULL 而非 =/!=、想保留 NULL 行就显式 OR x IS NULL、NOT IN 过滤 NULL 或用 NOT EXISTS、取值用 COALESCE 兜底、该非空的列建表加 NOT NULL 约束从源头减少 NULL。
我立下的几条规矩
这场"= NULL 查不出、NOT IN 含 NULL 全空"的事故,换来了我写 SQL 时,刻进骨子里的几条铁律:
- SQL 的 NULL 表示"未知",用三值逻辑;任何和 NULL 的比较结果都是 unknown(NULL=NULL 也是)。
- WHERE 只保留结果为 true 的行,unknown 和 false 一样被过滤——所以 = NULL 永远查不出。
- 判空一律用 IS NULL / IS NOT NULL,绝不用 = / !=。
- x != '某值' 会漏掉 x 为 NULL 的行;要带上 NULL 行就显式 OR x IS NULL。
- NOT IN 的子查询/列表含 NULL 会让整个结果集变空;过滤掉 NULL 或改用 NOT EXISTS。
- 取值用 COALESCE/IFNULL 兜底防 NULL 传染;COUNT(列)/SUM/AVG 跳过 NULL,COUNT(*) 算所有。
- 该非空的列建表时加 NOT NULL 约束+默认值,从源头减少 NULL 带来的三值逻辑意外。
附:一段把"NULL 三值逻辑"摆清楚的小实验
这是我后来跑的一段小实验,把 NULL 在各种比较下的结果直接查出来——它把"未知"这个抽象的三态变成了眼见为实的对照,现在我也常拿它给同事讲清这个坑:
-- 直接查这些比较的结果, 看 NULL 怎么把 true/false 变成"未知(NULL)"
SELECT
(1 = 1) AS a, -- true
(1 = 2) AS b, -- false
(1 = NULL) AS c, -- NULL(unknown!) ← 不是 false
(1 != NULL) AS d, -- NULL(unknown!) ← 也不是 true
(NULL = NULL) AS e, -- NULL(unknown!) ← 两个未知是否相等? 未知
(NULL IS NULL) AS f; -- true ← 只有 IS NULL 才能确凿地判出 NULL
-- 输出: a=true b=false c=NULL d=NULL e=NULL f=true
-- 用一张小表看 WHERE 如何过滤掉 unknown 的行
CREATE TEMP TABLE t(id INT, status VARCHAR(10));
INSERT INTO t VALUES (1,'done'), (2,'pending'), (3, NULL);
SELECT count(*) FROM t WHERE status = 'done'; -- 1
SELECT count(*) FROM t WHERE status = NULL; -- 0 ← NULL 行没被选中!
SELECT count(*) FROM t WHERE status != 'done'; -- 1(只有 pending, 漏了 NULL 行!)
SELECT count(*) FROM t WHERE status IS NULL; -- 1 ← 正确查出 NULL 行
SELECT count(*) FROM t WHERE status != 'done' OR status IS NULL; -- 2 ← 带上 NULL 行
-- NOT IN 含 NULL 的塌空演示
SELECT count(*) FROM t WHERE status NOT IN ('done', NULL); -- 0 ← 整个塌空!
SELECT count(*) FROM t WHERE status NOT IN ('done')
AND status IS NOT NULL; -- 1 ← 显式排除 NULL
这段实验把这次的教训摆得明明白白:1 = NULL、1 != NULL、NULL = NULL 查出来全是 NULL(也就是 unknown)——它们既不是 true 也不是 false;只有 IS NULL 才返回确凿的 true。再看那张小表:= NULL 选不出 NULL 行、!= 'done' 漏掉了 NULL 行、NOT IN ('done', NULL) 整个塌成 0,而 IS NULL 和显式 OR ... IS NULL 才查对。跑完这段我才真正在脑子里刻下:NULL 不是"一个能比的空值",而是一个会把 true/false 拖成"未知"的特殊态;凡是它参与的判断,我都得用三态的眼光重新审一遍。把这些比较的结果亲手查出来、看着 1 = NULL 返回的不是 0 而是 NULL,比记任何"判空用 IS NULL"的口诀都更让我对三值逻辑刻骨铭心。
这件事过后,我把项目里所有涉及可空列的查询都过了一遍,重点找 = NULL、!= 某值、NOT IN 子查询这几类写法,果然又揪出几处:一处用 status != 'closed' 统计"未关闭"工单、悄悄漏掉了一批 status 为 NULL 的、一处 NOT IN 子查询因为偶尔混进 NULL 而时不时返回空。我把它们逐一补上了 IS NULL 处理、或改成 NOT EXISTS,还把几个该非空的列加上了 NOT NULL 约束。那种把一类"看着对、却在 NULL 面前悄悄出错"的隐患一次清干净的踏实,是这次查询凭空变空换来的最实在的回报。
更让我警醒的,是这类坑骗人的方式:SQL 不报任何错——语法完全合法、查询正常返回,只是返回的结果悄悄少了该有的行、或凭空变成空。它不崩、不报错,只是给你一个错误的结果集,而你若不仔细核对总数,根本意识不到出了问题。这种沉默地返回错误结果的 bug,比直接报错难发现得多;从此我对任何涉及可空列的统计和筛选,都会多核一次总数对不对得上。
我也借这次机会在团队里立了条小约定:建表时凡是业务上不该为空的列,一律加 NOT NULL 约束并给默认值;凡是写 NOT IN 子查询,默认改用 NOT EXISTS;判空一律 IS NULL。三条小约定,从源头和写法两端一起堵住 NULL 的三值逻辑坑。把自己踩过的坑沉淀成别人不必再踩的规则,大概是复盘最值得做的那部分。
说到底,这次不过是把 = NULL 换成 IS NULL、给 NOT IN 补了道防线,可它让我真正记住的,是别用非黑即白的二元思维去对待一个允许存在灰色地带的世界。数据里有未知、有缺失,这是常态而非例外;承认这第三态的存在、并为它单独想一步,远比假装世界只有是和非要可靠。这份对未知的敬畏,比记住任何一条 SQL 语法都受用。
如今写下一个涉及可空列的条件之前,我会先在心里过一遍:这列要是 NULL,我这条件会把它怎么样——选中、漏掉、还是让结果整个塌空?就这一遍预演,常常就是一个查得准的结果集,和一个看着正常、却悄悄少了一批数据的结果集之间,全部的距离。
这件小事也让我对 SQL 这套看似简单的查询语言多了一分敬畏:它的很多行为(NULL 的三值逻辑、不带 ORDER BY 的顺序、隐式类型转换)都不是凭直觉就能想对的,而恰恰是这些反直觉的角落,藏着最容易让结果悄悄出错的陷阱。把这些角落一个个摸清,是写出能信得过的查询绕不开的功课。
毕竟,数据库不会因为我心里想的是查空值就替我把 = NULL 理解成 IS NULL;它只会忠实地按三值逻辑算出 unknown、再默默把这些行过滤掉。我能做的,就是在写下条件之前,先替那个沉默的未知,想好它该何去何从。
写在最后
回头看,这场由"用 = 比较 NULL"引发的"查询凭空变空"事故,真正教给我的,远不止"判空用 IS NULL"这一个技巧。它让我对"当一个体系里引入了'未知'这种特殊状态后, 它会悄悄地改变这个体系里'判断'的规则——很多我们习以为常的'非真即假'的二元判断, 会多出一种'说不准'的第三态; 而如果我们还用'非真即假'的老习惯去处理它, 就会在那些'说不准'的地方, 得到出乎意料的结果",有了一次刻骨的体会。我栽跟头,是因为我用'非真即假'的二元思维, 去处理一个'引入了第三态(未知)'的系统——我以为一个条件不是成立(true)就是不成立(false), status = NULL 不成立, 那它至少该被 != 之类选中吧;我没意识到, NULL 带来的"未知", 让比较有了第三种结果 unknown: status = NULL 既不是 true 也不是 false, 而是"说不准"; 而 WHERE 这种"只放行确凿为真的"规则, 把"说不准"和"确凿为假"一视同仁地挡在了门外;于是我那些建立在"非真即假"假设上的查询(= NULL、NOT IN、!=), 在"说不准"面前纷纷得出了我没料到的空结果。这让我领悟到一个关于"未知、三态逻辑与二元思维"的深刻认知:一旦一个系统里允许"未知/缺失/不确定"的存在, 它的逻辑就从"非真即假"的二元, 变成了"真/假/未知"的三元; 而"未知"有它自己的传染规则: 任何和"未知"结合的判断, 结果往往也是"未知";危险在于, 我们的直觉根深蒂固地是"二元的"——我们默认"不是 A 就是非 A"、"取反就能得到补集"; 可在有"未知"的三元世界里, 这些都不再成立(取反"未知"还是"未知"、"未知"既不在 A 里也不在非 A 里);所以凡是处理"可能存在未知/缺失"的数据或逻辑时, 都要切换到"三态思维": 显式地、单独地处理"未知"这一态, 而不是想当然地用二元的"非此即彼"把它归到某一边。这给了我一种看待"一切'带有未知/缺失状态的数据与判断'之事"时的清醒:每当我对一批"可能含有未知/缺失"的数据做判断、筛选、取反时, 要追问"这里面有'未知'这一态吗?我的判断把'未知'当成什么处理了——是当假过滤了、还是漏掉了、还是让整个结果塌了?我是不是该把'未知'单独拎出来显式处理?"——用三态思维显式处理"未知", 而不是用二元的非真即假想当然地把它归边;"识别系统中的未知态、用三态逻辑显式处理它而非套用二元思维", 是写对 SQL、也是处理一切'含缺失数据'之事的关键。认清 NULL 是未知、和它比较得 unknown、判空要用 IS NULL 而非 =——这,是我用一次查询凭空变空的事故,换来的、关于数据库、也关于如何用三态思维对待未知的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次写下 WHERE x = NULL、或用 NOT IN 一个可能含空的子查询时,先想想"这里有 NULL 吗?它会不会让我的条件变成永远说不准、结果凭空变空?",并换上 IS NULL 或 NOT EXISTS,那我对着那两条"看着没错却查不出东西"的 SQL 折腾的大半天,就值了。
—— 别看了 · 2026