我写了个查询所有非 active 用户的 SQL,结果状态为 NULL 的用户竟然一个都没查出来,报表数据莫名少了一大截,我对着 SQL 里 NULL 参与比较结果是 UNKNOWN 这个三值逻辑坑排查大半天的复盘

一个让我对 SQL 里的 NULL 彻底改观的坑,诡异在查询条件逻辑上看完全正确(查所有 status 不是 active 的用户),返回结果却悄悄漏掉了一部分本该符合的数据,而漏的正是 status 为 NULL 的行,这种静默漏数据在报表里尤其致命。用户表 status 有 active、inactive 和一些 NULL,我写 SELECT * FROM users WHERE status != active,期望查出所有非 active(包括 inactive 和 NULL)的用户,实际只查出了 inactive 的,status=NULL 的一个都没出来,报表非活跃用户数少了一大截。深究 SQL 三值逻辑才解谜:NULL 表示未知(不是 0、不是空串、不等于任何东西甚至不等于它自己);SQL 是三值逻辑条件结果有 TRUE/FALSE/UNKNOWN 三种;NULL 参与任何比较(=/!=)结果都是 UNKNOWN;而 WHERE 只保留结果为 TRUE 的行,FALSE 和 UNKNOWN 都过滤掉;所以 NULL != active 是 UNKNOWN 被过滤,NULL 行消失不是因为它等于 active 而是因为 NULL!=active 是 UNKNOWN 而非 TRUE;更要命 NULL=active 也是 UNKNOWN,NULL 行既不在 = 也不在 != 的结果里两边都不沾。这篇从故障现场、三值逻辑与 NULL 比较真相、正解(显式 OR col IS NULL、COALESCE 兜底、判 NULL 用 IS NULL 绝不用=NULL、NOT IN 含 NULL 返回空改 NOT EXISTS、聚合函数对 NULL)、NULL 其他坑(=NULL、NOT IN、运算传染、COUNT、唯一约束、ORDER BY、CASE、JOIN)、NULL 操作结果速查表、为何 NULL 易坑、写条件决策图与铁律、用把条件放进 SELECT 看清 UNKNOWN 的实验,到 NULL 该不该用的争论与权衡。核心领悟:现实信息常不是非真即假还有未知缺失第三态,三值逻辑是为诚实严谨对待不确定性而设计、不允许用二元直觉糊弄未知,要显式处理未知这一态;把空/NULL/缺失当头等大事专门处理;调试 WHERE 不符预期把条件挪进 SELECT 让黑盒判定变可见列;对有争议特性不站队而是理解其价值与代价按场景审慎取舍,能 NOT NULL 就 NOT NULL。

我写了个查询所有非 active 用户的 SQL,结果状态为 NULL 的用户竟然一个都没查出来,报表数据莫名少了一大截,我对着 SQL 里 NULL 参与比较结果是 UNKNOWN 这个三值逻辑坑排查了大半天的复盘

这是一个让我对 SQL 里的 NULL "彻底改观"的坑。它的诡异之处在于:我的查询条件逻辑上看完全正确("查出所有状态不是 active 的用户"),可它返回的结果却悄悄漏掉了一部分本该符合条件的数据——而漏掉的那部分,正是状态为 NULL 的行。这种"静默漏数据",在统计报表里尤其致命。

事情起于一个用户状态报表。我需要统计"所有非活跃(status 不是 'active')的用户"。用户表的 status 字段,有的值是 'active'、'inactive',还有一部分历史数据是 NULL(从没设置过状态)。我写下了这个看起来天经地义的查询:

-- users 表: status 字段有 'active'、'inactive'、以及一些 NULL
-- 我想查"所有非 active 的用户"
SELECT * FROM users WHERE status != 'active';

-- 我的预期: 查出所有 status 不是 'active' 的用户
--   —— 包括 status='inactive' 的, 也包括 status=NULL 的(NULL不就是"不是active"吗?)

-- 实际结果: 只查出了 status='inactive' 的!
--   status=NULL 的用户, 一个都没查出来! 💥
--   → 报表里"非活跃用户"数量, 比真实的少了一大截(少了所有NULL状态的)

我盯着这个结果,百思不得其解。status != 'active' 的意思不就是"status 不等于 active"吗?一个 status 是 NULL 的用户,它的 status 显然不是 'active' 啊,凭什么不被查出来?我反复确认那些 NULL 状态的用户确实存在、确实 status 是 NULL,可它们就是顽固地不出现status != 'active' 的结果里。在报表场景下,这意味着我统计的"非活跃用户数"是错的、偏少的——一个会误导决策的、静默的数据缺失。

第一件事:看清真相——SQL 是三值逻辑,NULL 和任何值比较都是 UNKNOWN

我去深入理解了 SQL 中 NULL 的语义和"三值逻辑",才终于解开这个"NULL 行神秘消失"之谜——SQL 里的 NULL 表示"未知(unknown)",而不是"某个具体的值";NULL任何值做比较(包括 !==),结果都不是 true 也不是 false,而是第三种值:UNKNOWN;而 WHERE 只保留结果为 true 的行,结果为 UNKNOWN 的行会被过滤掉

SQL 三值逻辑与 NULL 比较的真相

# 1. SQL 里的 NULL 不是"一个值", 而是表示"未知 / 缺失(unknown)":
#    - 它不等于 0, 不等于空字符串 '', 不等于任何东西, 甚至不等于它自己!

# 2. SQL 是【三值逻辑(three-valued logic)】: 一个条件的结果有三种:
#    - TRUE(真)、FALSE(假)、UNKNOWN(未知)
#    (普通语言只有 true/false 两值, 这是SQL一个根本不同点)

# 3. NULL 参与任何比较, 结果都是 UNKNOWN:
#    NULL = 'active'    → UNKNOWN  (不是 false!)
#    NULL != 'active'   → UNKNOWN  (不是 true!)
#    NULL = NULL        → UNKNOWN  (NULL 不等于它自己!)
#    NULL > 5           → UNKNOWN
#    → 因为"未知的东西"和任何东西比, 结果当然也是"未知"。

# 4. ★ WHERE 子句的规则: 只保留条件结果为 TRUE 的行!
#    - 结果是 FALSE 的行: 过滤掉(正常)
#    - 结果是 UNKNOWN 的行: 也【过滤掉】! (不当作true)

# 5. 所以 WHERE status != 'active' 对一个 status=NULL 的行:
#    - NULL != 'active' → 结果是 UNKNOWN
#    - WHERE 只留 TRUE → UNKNOWN 被过滤掉 → 这行【不出现】在结果里!
#    - → 这就是为什么 status=NULL 的用户"消失"了:
#         不是因为它"等于active", 而是因为"NULL != active"是UNKNOWN而非TRUE。

# 6. 同样的坑: status = 'active' 也查不出 NULL 行(NULL='active'也是UNKNOWN);
#    所以 NULL 行既不在 =的结果里, 也不在 !=的结果里——它两边都不沾!

# 核心: SQL是三值逻辑(true/false/unknown), NULL表示未知, 它和任何值比较(=/!=)都得UNKNOWN;
#   WHERE只保留结果为TRUE的行, 所以NULL行在 = 和 != 的条件里都会被过滤掉, 造成静默漏数据。

真相大白,我恍然大悟。原来 SQL 里的 NULL 不是"一个值",而是表示"未知(unknown)"——它不等于 0、不等于空字符串、不等于任何东西,甚至不等于它自己。而 SQL 是三值逻辑:一个条件的结果有 TRUE、FALSE、UNKNOWN 三种(不像普通语言只有 true/false)。关键在于:NULL 参与任何比较结果都是 UNKNOWN(NULL = 'active'NULL != 'active'NULL = NULL 全是 UNKNOWN);而 WHERE 子句只保留结果为 TRUE 的行——结果为 FALSE 或 UNKNOWN 的行都被过滤掉。所以 WHERE status != 'active' 对一个 status=NULL 的行:NULL != 'active' 结果是 UNKNOWN、被过滤掉,这行不出现在结果里。NULL 行"消失"的真相,不是因为它"等于 active",而是因为"NULL != active"是 UNKNOWN 而非 TRUE。更要命的是:status = 'active' 也查不出 NULL 行(NULL='active' 也是 UNKNOWN)——NULL 行既不在 = 的结果里、也不在 != 的结果里,两边都不沾!

第二件事:正解——用 IS NULL 显式处理 NULL,或 COALESCE 兜底

搞懂了原理,正解就清晰了:判断 NULL 必须用 IS NULL / IS NOT NULL;想让"非 active"包含 NULL,要显式把 NULL 情况写进条件,或用 COALESCE 给 NULL 一个默认值

-- ====== 正解一: 显式把 NULL 情况加进条件 ======
SELECT * FROM users
WHERE status != 'active' OR status IS NULL;   -- ★ 显式带上 status IS NULL
-- ✓ 现在 status='inactive' 和 status=NULL 的行都查出来了

-- ====== 正解二: 用 COALESCE / IFNULL 给 NULL 一个默认值再比 ======
SELECT * FROM users
WHERE COALESCE(status, '') != 'active';        -- 把 NULL 当成 '' 来比
-- ✓ NULL 被转成 '', '' != 'active' 是 TRUE, 查得出
-- (注意: 对 status 用了函数可能影响索引, 数据量大时权衡; 见前面索引那篇)

-- ====== 判断 NULL 必须用 IS NULL / IS NOT NULL ======
-- ✗ WHERE status = NULL      -- 永远查不出任何行(NULL=NULL是UNKNOWN)!
-- ✓ WHERE status IS NULL     -- 这才是判断"是NULL"的正确写法
-- ✗ WHERE status != NULL     -- 也永远不成立
-- ✓ WHERE status IS NOT NULL -- 判断"不是NULL"

-- ====== ★ NOT IN 含 NULL 的大坑 ======
-- 如果子查询/列表里【含有 NULL】, NOT IN 会返回【空结果】!
-- ✗ SELECT * FROM a WHERE id NOT IN (SELECT pid FROM b);
--    若 b.pid 里有 NULL → 整个查询返回空! (因为 id != NULL 是 UNKNOWN)
-- ✓ 改用 NOT EXISTS, 或在子查询里过滤掉 NULL:
--    SELECT * FROM a WHERE NOT EXISTS (SELECT 1 FROM b WHERE b.pid = a.id);
--    或 ... NOT IN (SELECT pid FROM b WHERE pid IS NOT NULL);

-- ====== 聚合函数对 NULL 的处理(也要知道) ======
-- COUNT(*) 算所有行; COUNT(col) 不算col为NULL的行!
-- SUM/AVG 忽略 NULL; AVG 是 sum/非null的个数(不是/总行数)

-- 核心: 判NULL用IS NULL/IS NOT NULL(绝不用=NULL); 想让!=条件包含NULL要显式 OR ... IS NULL
--   或COALESCE兜底; 特别警惕 NOT IN 含NULL返回空集(改NOT EXISTS); 聚合函数也各自处理NULL。

修复的核心,是"用 IS NULL 显式处理 NULL,别指望 =/!= 能正确对待它"正解一:显式把 NULL 加进条件——WHERE status != 'active' OR status IS NULL,把 NULL 情况显式带上正解二:COALESCE/IFNULL 兜底——COALESCE(status, '') != 'active' 把 NULL 当成 '' 来比(但对列用函数可能影响索引,要权衡)判断 NULL 必须用 IS NULL/IS NOT NULL(= NULL 永远查不出任何行)。还有一个大坑:NOT IN 含 NULL——如果子查询/列表含 NULL,NOT IN 会返回空结果(因为 id != NULL 是 UNKNOWN);改用 NOT EXISTS,或在子查询里 WHERE pid IS NOT NULL 过滤掉 NULL聚合函数也各自处理 NULL:COUNT(*) 算所有行、COUNT(col) 不算 col 为 NULL 的行;SUM/AVG 忽略 NULL归根结底:判 NULL 用 IS NULL/IS NOT NULL(绝不用 =NULL);想让 != 条件包含 NULL 要显式 OR ... IS NULL 或 COALESCE 兜底;特别警惕 NOT IN 含 NULL 返回空集。

第三件事:NULL 相关的其他常见坑

排查后我把 SQL 里 NULL 相关的其他常见坑也系统梳理了一遍,它们都源自 NULL 的"未知"语义和三值逻辑。

SQL NULL 相关的其他常见坑

# 1. != / = 漏掉NULL行(本文): WHERE col!='x' 不含NULL。→ OR col IS NULL。

# 2. = NULL 判断: WHERE col = NULL 永远空。→ 用 IS NULL。

# 3. NOT IN 含NULL返回空: 子查询有NULL → 整个NOT IN空集。→ NOT EXISTS。

# 4. NULL 拼接/运算: NULL + 1 = NULL, CONCAT 某些库里 NULL 拼接结果变NULL。
#    → 用 COALESCE 兜底。

# 5. COUNT(col) vs COUNT(*): COUNT(col)不算NULL, COUNT(*)算所有行。

# 6. 唯一约束与NULL: 多数库里唯一索引允许多个NULL(NULL!=NULL), 可能不符预期。

# 7. ORDER BY 里NULL的位置: NULL排最前还是最后, 各库默认不同(NULLS FIRST/LAST)。

# 8. CASE WHEN col = NULL: 同样不成立, 要 WHEN col IS NULL。

# 9. JOIN 时NULL不匹配: a.x = b.x 时, NULL不会和NULL匹配上(NULL=NULL是UNKNOWN)。

# 共同根源: NULL 表示"未知", SQL用三值逻辑(true/false/unknown)处理它;
#   它和"具体的值"行为完全不同, 用对待普通值的直觉去对待NULL, 就会处处出错。

# 核心: NULL是"未知"不是"值", 涉及它的比较都得UNKNOWN; 判NULL用IS [NOT] NULL、
#   != 条件记得带NULL、警惕NOT IN/聚合/JOIN对NULL的特殊行为; 能用NOT NULL约束就尽量用。

排查让我把 NULL 的其他坑也梳理清了。一、!=/= 漏掉 NULL 行(本文)。二、=NULL 判断永远空(用 IS NULL)。三、NOT IN 含 NULL 返回空(改 NOT EXISTS)。四、NULL 拼接/运算结果变 NULL(COALESCE 兜底)。五、COUNT(col) 不算 NULL六、唯一约束允许多个 NULL七、ORDER BY 里 NULL 位置各库不同八、CASE WHEN col = NULL 不成立九、JOIN 时 NULL 不匹配 NULL它们的共同根源是:NULL 表示"未知",SQL 用三值逻辑处理它;它和"具体的值"行为完全不同,用对待普通值的直觉去对待 NULL 就处处出错核心是:NULL 是"未知"不是"值";判 NULL 用 IS [NOT] NULL、!= 条件记得带 NULL、警惕 NOT IN/聚合/JOIN 对 NULL 的特殊行为;能用 NOT NULL 约束就尽量用下面这张图,是这次 NULL 行漏查的成因与解法:

第四件事:NULL 参与各种操作的结果速查表

这次踩坑后,我把 NULL 参与各种操作的结果整理成一张表,涉及 NULL 时对照。

操作 结果 说明
NULL = 任何值 UNKNOWN 不是true也不是false
NULL != 任何值 UNKNOWN 所以 != 条件漏NULL行
NULL = NULL UNKNOWN NULL不等于它自己!
NULL IS NULL TRUE 判断NULL唯一正确方式
NULL + 5 / 运算 NULL NULL传染整个表达式
COUNT(含NULL列) 不计NULL COUNT(*)才算所有行
NOT IN (含NULL) 空结果 大坑, 改NOT EXISTS

这张表把 NULL 的"脾气"钉死了。核心是:NULL 几乎"污染"了所有它参与的操作——比较得 UNKNOWN(被 WHERE 过滤)、运算得 NULL(传染整个表达式)、NOT IN 含它就返回空;唯一能正确"抓住"NULL 的,是 IS NULL它给我的最大启发是:NULL 在 SQL 里是一个"特殊到需要被单独对待"的存在;它不遵循""的常规逻辑,而是有一整套自己的、基于"未知"语义的规则;任何"把它当成一个普通值"的处理,都可能出错这其实反映了一个更深的设计哲学问题——"如何表示'没有值/缺失'":SQL 选择用 NULL 这个"特殊标记 + 三值逻辑"来表示"缺失/未知",这个设计有其道理(能区分"0"和"没填"、"空字符串"和"不知道"),但也带来了巨大的复杂性和无数的坑;以至于很多数据库专家(如 C.J. Date)都强烈批评 NULL、主张尽量避免使用它这给我的实践启示是:在设计表结构时,要认真对待"这个字段允不允许 NULL"这个决定——能用 NOT NULL 约束 + 一个明确的默认值(如 '' 或 0 或一个表示"未设置"的特定值)来避免 NULL 的,就尽量避免;只在"'未知/缺失'本身是一个有意义的、需要和其他值区分的状态"时,才有意识地使用 NULL,并在查询时时刻警惕它的三值逻辑从源头审慎决定"是否允许 NULL"、能避则避——是减少这类坑的根本之道。

第五件事:为什么 NULL 这么容易坑人

这次让我反思了 NULL "坑人"的深层原因。

原因 说明
语义反直觉 "NULL!=active 不为真", 和日常逻辑完全相反
三值逻辑陌生 多数语言两值逻辑, SQL多了个UNKNOWN
静默漏数据 不报错, 只是结果少了行, 极难察觉
测试数据常无NULL 测试表里常没NULL, 测不出
到处都有特殊行为 =/!=/NOT IN/聚合/JOIN/ORDER BY都特殊

这张表道出了 NULL "难缠"的原因。核心是:NULL 的语义(未知)、三值逻辑(多了 UNKNOWN)本身就反直觉、陌生;而它出错时又是"静默漏数据"(不报错、只是结果少了行),且测试数据里常常没有 NULL,让它极难在开发期被发现;再加上它在几乎每种操作里都有特殊行为,坑点遍布——这些因素叠加,让 NULL 成了 SQL 里最容易、最隐蔽的错误来源之一它给我的深刻启发是:处理"缺失/未知/空"这种"边缘但普遍存在"的情况,是编程中一个永恒的、极易被忽视的难题;无论是 SQL 的 NULL、还是其他语言里的 null/None/nil/Optional,"如何表示和处理'没有值'"都是一个充满陷阱的领域——因为"没有值"这个状态,天然地不遵循"有值"时的那些规则这让我形成一个习惯:无论写什么代码(SQL 查询、业务逻辑、数据处理),都要主动地、显式地去思考和处理"这里如果是空/NULL/缺失,会怎样?"这个情况;不能默认"数据总是有值的",而要把"空值的处理"当成一个需要专门考虑的、第一等的逻辑分支,而不是一个"顺带就处理了"的边角料把"空/NULL/缺失的情况"当成头等大事来专门处理——是这个 NULL 坑教给我的、贯穿一切数据处理的核心严谨性。

第六件事:写 SQL 条件时,我现在的判断习惯

现在每当我写一个 WHERE 条件、或处理可能含 NULL 的列,我都会按这张图先想清楚:

这张图的精髓,是"涉及可能有 NULL 的列时,想清楚 NULL 行该不该被选中、并显式处理"列有 NOT NULL 约束就正常写;可能有 NULL 就警惕——=/!= 等比较会漏 NULL 行,该选中就显式 OR col IS NULL 或 COALESCE,判断是不是 NULL 用 IS NULL 绝不用 =NULL,NOT IN 子查询要过滤 NULL 或改 NOT EXISTS这套习惯,让我写 SQL 时,从"按直觉写条件"变成了"先问这列有没有 NULL、NULL 行怎么处理"——核心始终是:NULL 是未知不是值,比较得 UNKNOWN 被 WHERE 过滤;涉及 NULL 的列要显式处理,别让它静默漏数据。

我立下的几条规矩

这场"NULL 行神秘消失"的事故,换来了我写 SQL 时,刻进骨子里的几条铁律:

  1. SQL 是三值逻辑,有 UNKNOWN。这是理解 NULL 一切行为的根基。
  2. NULL 和任何值比较都是 UNKNOWN。=、!= 对 NULL 都不为真。
  3. WHERE 只保留 TRUE 的行。UNKNOWN 的行(含 NULL)被静默过滤。
  4. 判 NULL 只能用 IS NULL/IS NOT NULL。=NULL 永远查不出。
  5. != 条件要选中 NULL,显式 OR col IS NULL。或 COALESCE 兜底。
  6. 警惕 NOT IN 含 NULL 返回空集。改 NOT EXISTS。
  7. 设计表时能 NOT NULL 就 NOT NULL。从源头减少 NULL 的坑。

附:一段亲眼看清 NULL 三值逻辑的实验

口说无凭。下面这段 SQL,用一张小表把 NULL 的三值逻辑行为彻底演示清楚,你可以直接在数据库里跑一遍:

-- 造一张含 NULL 的小表
CREATE TABLE t (id INT, status VARCHAR(20));
INSERT INTO t VALUES (1,'active'), (2,'inactive'), (3, NULL);

-- ===== 1. != 漏掉 NULL 行 =====
SELECT * FROM t WHERE status != 'active';
--   只返回 id=2(inactive)! id=3(NULL)漏了 ★

-- ===== 2. = 也查不出 NULL 行 =====
SELECT * FROM t WHERE status = 'active';
--   只返回 id=1。id=3(NULL)同样不在这里
--   → id=3 既不在 != 也不在 = 的结果里!"两边不沾"

-- ===== 3. 直接看比较的结果是 UNKNOWN(NULL) =====
SELECT id, status,
       status = 'active'  AS eq_active,    -- id=3 这列是 NULL(代表UNKNOWN)
       status != 'active' AS ne_active,    -- id=3 这列也是 NULL(UNKNOWN)
       status IS NULL     AS is_null       -- id=3 这列是 1/true ← 唯一能抓住NULL的
FROM t;
--   id=3 行: eq_active=NULL, ne_active=NULL, is_null=true
--   → 亲眼看到: =和!= 对NULL都返回NULL(UNKNOWN), 只有 IS NULL 是true

-- ===== 4. 正解: 显式带上 NULL =====
SELECT * FROM t WHERE status != 'active' OR status IS NULL;
--   返回 id=2 和 id=3 ✓ 这才是完整的"非active"

-- ===== 5. NOT IN 含 NULL 的空集陷阱 =====
SELECT * FROM t WHERE id NOT IN (1, 2, NULL);
--   返回【空】! 因为 id != NULL 是 UNKNOWN, 整个条件无法为TRUE
SELECT * FROM t WHERE id NOT IN (1, 2);          -- 去掉NULL
--   返回 id=3 ✓

-- 核心: 跑一遍, 亲眼看到 != 漏NULL行、=也漏、比较结果是NULL(UNKNOWN)、
--   只有IS NULL能抓住它、NOT IN含NULL返回空——SQL三值逻辑一次彻底看清。

这段实验 SQL,是我这次踩坑后整理的"NULL 行为图鉴"。它最有价值的是第 3 个查询——它把比较的结果直接当成一列查出来:你能亲眼看到 id=3(NULL)那一行,status = 'active' 这列是 NULL、status != 'active' 这列也是 NULL(这两个 NULL 就代表了 UNKNOWN),而只有 status IS NULL 这列是 true。这一下就把抽象的"三值逻辑""比较结果是 UNKNOWN"变成了你能在结果集里直接看到的、具体的 NULL/true这正是我想用这段 SQL,留给每个写 SQL 的人的核心方法:当你对一个 WHERE 条件"到底会不会选中某种行"拿不准时(尤其涉及 NULL、复杂布尔逻辑),一个绝佳的技巧是——把那个条件表达式从 WHERE 里挪到 SELECT 列里,作为一列查出来;这样你就能对每一行,清清楚楚地看到那个条件对它求值的结果(true/false/NULL),而不用在脑子里猜因为WHERE 的过滤是"黑盒"的(你只看到最终留下的行,看不到"每行的条件结果是什么、为什么被留下或过滤");而把条件放进 SELECT,就把这个黑盒打开了,让"条件对每一行的判定过程"变得可见、可验证把判断条件挪进 SELECT、让 WHERE 的"黑盒判定"变成可见的列——这份"把过滤逻辑显式化、可视化"的技巧,是我调试一切"WHERE 条件结果不符预期"问题(NULL 漏行、布尔逻辑错、隐式转换)最顺手的法门。

补充:NULL 该不该用——一个一直有争论的话题

这次踩坑后,我顺带去了解了一下数据库领域关于 NULL 的争论,发现这其实是个由来已久、至今未息的话题,也让我对"该怎么对待 NULL"有了更平衡的看法。

一派观点(以关系模型理论家 C.J. Date 为代表)强烈反对 NULL:他们认为 NULL 引入的三值逻辑,破坏了关系模型简洁优美的二值逻辑基础,带来了无穷无尽的反直觉行为和坑(就像本文这个),主张完全不用 NULL,而是用"特殊的默认值"或"拆分出额外的表"来表示"缺失"。另一派则认为,NULL 是表达"真实世界中信息确实缺失/未知"的最自然、最诚实的方式——现实中数据就是会缺,用一个"假的默认值"(比如用 0 表示"没填年龄")去冒充"缺失",反而会引入更隐蔽的错误(0 岁和"没填"是两回事)。

了解了这场争论,我的体会是:NULL 是一把"双刃剑",它的""和它的"价值"是同源的——正因为它诚实地表示"未知"、不肯对未知的东西妄下判断,所以它才有了那套"反直觉"的三值逻辑;问题不在于 NULL 本身""还是"",而在于我们有没有理解它、并正确地对待它这让我形成了一个更务实的态度:不必走极端(完全禁用 NULL 太教条,滥用 NULL 又处处是坑);而是带着对它的充分理解,审慎地使用——能用 NOT NULL + 有意义的默认值清晰表达的,就别用 NULL;但当"'未知/未设置'本身就是一个需要被如实记录、和其他值区分开的重要状态"时,就坦然地用 NULL,并在所有涉及它的查询里,时刻记得它的三值逻辑、显式地处理它

这场关于 NULL 的争论还给了我一个更普遍的启示:很多技术里的"有争议的特性",往往没有非黑即白的答案;它们之所以"有争议",正是因为它们同时带来了真实的价值和真实的代价;成熟的做法,不是简单地站队"它好/它烂",而是深入理解它的价值与代价分别是什么、在什么场景下价值大于代价、在什么场景下相反,然后在具体情境里做出权衡的、有意识的选择对有争议的技术特性,不站队、而是理解其权衡并按场景审慎取舍——这是这个 NULL 坑,在技术之外给我的一点更成熟的思考方式。

写在最后

回头看,这场由"NULL 行从 != 条件里消失"引发的、报表数据偏少的事故,真正教给我的,远不止"用 IS NULL"这一个技巧。它让我对"'未知'是一种独立于''和''的状态"这个深刻的逻辑思想,以及"用对待确定世界的直觉去处理不确定信息"的危险,有了一次刻骨铭心的体会。我栽跟头,根源是我用一种"非黑即白、非真即假"的二值逻辑直觉,去处理一个本质上是三值逻辑的世界。我想当然地以为:一个 status,要么"等于 active"(真),要么"不等于 active"(假),二者必居其一;所以 != 'active' 就该网罗所有"不等于 active"的。可我忽略了第三种状态:这个 status 是 NULL——它的值根本未知,我们既无法断定它"等于 active",也无法断定它"不等于 active"!对一个未知的值,"它等不等于 active"这个问题的诚实答案,不是""也不是"",而是"不知道(UNKNOWN)"。这让我领悟到一个深刻的认知:现实世界里的信息,常常不是"非真即假"的二元状态,还存在大量"未知、缺失、不确定"的第三态;而 SQL 的三值逻辑,正是为了诚实地、严谨地对待这种"不确定性"而设计的——它拒绝对一个未知的值"武断地猜测"它到底等于还是不等于,而是如实地返回"未知"这其实是一种值得敬佩的严谨:它逼着我们,在处理数据时,不能回避"这里可能是未知的"这个事实,而必须明确地、显式地表达"当值未知时,我希望怎么办"(是当它符合条件、还是不符合、还是单独处理);它不允许我们用"想当然的二元直觉"去糊弄"不确定性"学会用"三值逻辑"的、能容纳"未知"的思维去严谨地处理不确定的信息、并永远显式地处理"未知"这一态——这,是我用一次 NULL 漏数据的事故,换来的、关于 SQL、也关于如何严谨地对待"不确定性"的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次写 != 条件时,先想一句"这列要是 NULL,这行还会被选中吗?",那我对着那份莫名偏少的报表排查的这大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我用 == 比较两个 Integer,小数值时一切正常,某天数值一超过 127 判断就突然失效了,我对着 Java 自动装箱的 Integer 缓存只缓存 -128 到 127 这个坑排查了大半天的复盘

2026-6-2 11:07:16

技术教程

下游一个接口只是变慢了一点,我的整个服务却跟着全部瘫痪、所有请求都卡死,我对着调用下游时没设超时导致请求堆积线程耗尽的级联雪崩这个坑排查大半天的复盘

2026-6-2 11:20:15

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索