2051 字
约 6 分钟
19
S08-A26 · `NULL` 的三值逻辑:为什么"不等于"筛不出空值

一条"清理脚本"写了 DELETE FROM t WHERE status != 'paid',跑完后"未支付"的记录还在——因为那些记录的 status 是 NULL,而"NULL != 'paid'"的结果不是 true,而是 unknown。于是它们"既不在 = 'paid' 里,也不在 != 'paid' 里".

SQL 的 NULL 不是"空字符串",也不是"零",而是"未知"。于是它的比较遵循"三值逻辑":true / false / unknown——而 WHERE 只保留 true,unknown 被当成"不满足".

1. 三值逻辑的三条规则

表达式 结果
NULL = NULL unknown
NULL != 'x' unknown
NOT unknown unknown

第二项的"心理陷阱"是"日常语言的直觉":人会说"它不等于 paid,所以该删"——而 SQL 说的是"我不知道它等不等于 paid",于是"不动它".
第二项是"清理脚本失效"的直接原因:
"不等于"在 NULL 上"既不是真也不是假"**——**于是它"被筛掉了".

"三值逻辑"在"关联条件"里也会"吞行",而它比 WHERE 更难发现:

-- 反例:关联键可能为 NULL,行会"消失"
SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
-- 若 o.user_id 为 NULL:该订单"不匹配任何用户",于是"不出现在结果里"
-- 判据:关联键"是否可能为 NULL"——可能,就要用 LEFT JOIN 或"先补默认值"

2. 六种"被 NULL 坑"的写法

写法 实际含义
col != 'x' 不含 NULL 行
NOT (col = 'x') 同上
col NOT IN (1,2) 列表里有 NULL 时全 unknown
COUNT(col) 不计 NULL
SUM(col) 忽略 NULL
col = '' 只匹配"空串",不含 NULL

第三项最"炸":NOT IN (1, NULL) 会"一行都不返回"第三项的"为什么"值得记住:NOT IN 等价于"对每个元素做 <> 然后取合取",而"任一元素为 NULL 时该项为 unknown"——**unknown 与任何值合取仍是 unknown.
——**因为"col 与 NULL 比较"永远是 unknown".

"三值逻辑"可以用"一次小实验"看见,比读文档快:

-- 三行实验:看 unknown 怎么被吞
SELECT NULL = NULL, NULL != 'x', NOT (NULL = 'x');        -- 全为 NULL
SELECT COUNT(*) FROM t WHERE status != 'paid';             -- 不含 NULL 行
SELECT COUNT(*) FROM t WHERE status IS NULL OR status != 'paid';  -- 含
-- 判据:两行计数"不等" → 表里有 NULL

3. "要包含 NULL"的三种写法

写法 说明
col IS NULL OR col != 'x' 最直观
COALESCE(col,'') != 'x' 简洁(会"用默认值替代")
col <=> 'x'(MySQL) NULL 安全等于(NULL <=> NULL 为真)

第一项的"写法"要注意"括号":(col IS NULL OR col != 'x')——少了括号,AND 优先级会让条件"变成另一个意思".
第一项是"最稳"的:
"显式写出两种情形"**——**而它"读起来"就告诉下一个人"这里考虑过 NULL".

"报表给 0 而不是空"这件事,要"在 SQL 层兜住",而不是"在前端判空":

-- 三种"空变 0"
1 COALESCE(SUM(amount), 0)                 -- 最常用
2 IFNULL(SUM(amount), 0)                   -- MySQL 同义
3 SUM(COALESCE(amount, 0))                 -- 把"未知"当 0(语义不同,慎用)
-- 判据:1/2 是"没有行时给 0";3 是"把未知改成 0"——两者别混

4. "聚合"上的三种陷阱

陷阱 表现
AVG(col) 忽略 NULL 分母变小(不是"当 0")
COUNT(*) vs COUNT(col) 差"NULL 行数"
SUM(col) 全 NULL 时返回 NULL 不是 0

第三项会让"报表里出现空值":"本月无销售"的 SUM 是 NULL,而"0"要"COALESCE(SUM(x),0)".

"唯一 + 非空"的组合要"一起加",否则"约束只挡一半":

-- 反例:只加唯一索引(允许多个 NULL)
ALTER TABLE user ADD UNIQUE KEY uk_phone (phone);
-- 正解:先"非空 + 默认值",再"唯一"
ALTER TABLE user MODIFY phone VARCHAR(20) NOT NULL DEFAULT '';
-- 判据:唯一键上的字段"是否可能为 NULL"——可能,就要一起改

5. "索引与 NULL"的三件事

事实 影响
唯一索引允许多个 NULL(多数库) "唯一"不等于"非空"
IS NULL 可走索引(部分库/版本) 视优化器
col != 'x' 常"用不上索引" 全表扫

第一项是"业务漏洞":"唯一索引"挡不住"多条 NULL"第一项在"迁移"时最容易出事:"加了唯一索引"却没加 NOT NULL——**于是"上线后"数据里"多条 NULL 共存",而"业务以为是唯一的".
——**于是"手机号唯一"要先"NOT NULL".

"NOT IN 换 NOT EXISTS"是最常用的一次性修法,而它"顺便"解决了"列表里有 NULL":

-- 反例:列表含 NULL 时,整条查询返回空
SELECT * FROM a WHERE a.id NOT IN (SELECT b.a_id FROM b);
-- 正解:NOT EXISTS(对 NULL 不敏感)
SELECT * FROM a WHERE NOT EXISTS (SELECT 1 FROM b WHERE b.a_id = a.id);
-- 判据:子查询"可能返回 NULL"时,一律用 NOT EXISTS

6. 排错:五种"NULL 问题"

现象 成因 处理方向
!= 漏行 三值逻辑 加 IS NULL
NOT IN 返回空 列表含 NULL 用 NOT EXISTS
报表"空"而非 0 SUM 全 NULL COALESCE
唯一约束"没生效" 多个 NULL 允许 加 NOT NULL
关联"少了行" = 匹配不到 NULL 用"外连接"或"<=>"

第五项的"修法"要"看语义":"要保留左表全部行"用 LEFT JOIN,"要 NULL 也能配上"用"<=> 或 COALESCE 连接键"——而两者"结果不同",选错就"多行或少行".
第五项在"关联"里很常见:
"a.col = b.col 遇到 NULL 不匹配"**——**于是"左表有、右表空"的行"消失".

7. 边界:什么情况"NULL"是正确表达

场景 为什么 做法
"未知"与"零"要区分(如"未测量"vs"测得 0") 语义不同 用 NULL
可选关联(未关联时无值) 天然 用 NULL
第三方数据缺失 确实未知 用 NULL
"已删除"与"从未有"要区分 业务含义不同 用 NULL + 状态位

判断口径一句话:"这个字段的'没有',是"未知"还是"零/空""。是"未知",NULL 是对的表达(但要"每处比较都记得处理");是"零/空",用"默认值 + NOT NULL"更省心——而"把'未知'塞成 0",会在"平均值/告警阈值"上造成更难发现的错.

8. 自测

  1. NULL 与"空字符串""零"的区别是什么?为什么它需要"三值逻辑"?
  2. 为什么 col != 'x' "筛不出空值"?WHERE 对 unknown 怎么处理?
  3. NOT IN (1, NULL) 为什么会"一行都不返回"?
  4. "要包含 NULL"的三种写法里,哪一种"读起来就告诉下一个人你考虑过"?
  5. SUM 全 NULL 时返回什么?"报表里出现空值"的修法是什么?
  6. 判断"该用 NULL 还是默认值"的那个问题是什么?

补一句"实践"建议:"新表默认 NOT NULL"应当写进建表规范——因为"后期把 NULL 改成 NOT NULL"要"清洗存量数据 + 改所有比较处",成本是"当初不写 NULL"的十倍。而"确实需要未知"的字段,"用 NULL 但配注释"比"用 0 混过去"更诚实——**而"诚实"在数据上,就是"可审计".

而"吞行"这件事,在"对账"里表现为"总数对不上",而在"列表页"里表现为"少了几条"——同一个原因,两种抱怨。

所以"查吞行"应当"先查关联键的 NULL",而不是"先怀疑索引"。

9. 一句话总结

NULL 是"未知",不是"空"——于是"所有比较"都要"多考虑一种情形"。而工程上的稳妥做法只有两条:要么"让字段 NOT NULL + 默认值"(把三值逻辑压回两值),要么"在每个比较处显式写 IS NULL"。而"漏一处"的代价,往往是一条"跑完没报错、但数据没改"的脚本。

S08-A26 · `NULL` 的三值逻辑:为什么"不等于"筛不出空值
https://www.clxhxhhr.top/posts/6703/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。