「非已支付」的数字对不上:NULL 上场
你汇报「非已支付状态的订单」时数字对不上--运营说还有一批 status 为空的脏数据,而 status <> 2 把它们全漏掉了。
学 · 40 min
01三值逻辑:TRUE / FALSE / UNKNOWN,以及 WHERE 只保留 TRUE
场景你用 status 不等于 2 统计「非已支付」,漏掉了一大批单。漏掉的那批 status 是 NULL--这不是 bug,是 SQL 的设计。
NULL 参与任何比较,结果既不是 TRUE 也不是 FALSE,而是第三个值 UNKNOWN。而 WHERE 只放行 TRUE--UNKNOWN 和 FALSE 一样被丢弃。所以
status <> 2查不到 status 为 NULL 的行:不是「不等于 2」不成立,是「结果未知」就不放行。select null = null, -- NULL(UNKNOWN) null <> null, -- NULL(UNKNOWN) not null; -- NULL(UNKNOWN) -- TRUE AND NULL = NULL;TRUE OR NULL = TRUE;FALSE OR NULL = NULL易错结论先背下来:NULL 和任何值比较(包括和自己)都得 UNKNOWN,永远不会是 TRUE。
02IS NULL、IS NOT NULL、IS DISTINCT FROM
场景既然比较运算符对 NULL 失灵,判空就得有专门的语法。
is null/is not null是唯一能对 NULL 做出 TRUE/FALSE 判断的写法。进阶是is distinct from:把 NULL 当成一个具体值参与比较,「不等于某值(连 NULL 也算)」的正确写法。select count(*) from orders where status is null; -- 状态为空的脏数据 select count(*) from orders where status <> 2; -- 漏掉 NULL 行 select count(*) from orders where status is distinct from 2; -- 正确的「非已支付」易错「不等于某值」的需求永远先想到 IS DISTINCT FROM,<> 只对非 NULL 行负责。
03NULL 参与比较、算术、排序时分别得到什么
场景脏数据的 NULL 会顺着各种运算悄悄传染,得知道传到哪一步会变成什么。
算术:
null + 1 = null,传染。聚合:sum/avg把 NULL 当「这行不参与」。排序:PG 默认升序 NULL 排最后、降序排最前,可以用nulls first / last显式指定。select null + 1; -- null select 1 + 2 + null; -- null:一个环节空,整条链空 select id, status from orders order by status desc nulls last; -- 脏数据固定垫底易错各数据库对 NULL 排序位置不统一(MySQL 恒排最前),写报表想稳就显式写 nulls first/last。
04count(*) vs count(col) 初见:NULL 不被 count(col) 数到
场景同一张表,三个 count 数出三个数,老板以为你在做假账。
count(*)数行,一行算一个;count(列)只数该列非空的行。两个数字相减 = 该列为 NULL 的行数--这本身就是个查脏数据的小技巧。select count(*), -- 50000:总行数 count(status), -- ≈49478:有状态的 count(paid_at) -- ≈34968:付过钱的 from orders;易错avg / sum 跳过 NULL 意味着「平均值」的分母变小了--分母口径问题 D4 展开。
练 · 65 min
- 用
select null = null, null <> null, not null验证三值逻辑,做出 AND/OR 真值表参考答案
关键结论:NULL 参与任何比较都得到 UNKNOWN,而 WHERE 只保留 TRUE。真值表两行:TRUE AND NULL=NULL、TRUE OR NULL=TRUE;FALSE AND NULL=FALSE、FALSE OR NULL=NULL。
select null = null, null <> null, not null; -- 三列结果都是 NULL(即 UNKNOWN) - 查 status 为空的订单数;对比
status <> 2的行数,解释差额来自哪参考答案
差额 = status 为 NULL 的行数:NULL <> 2 的结果是 UNKNOWN,不是 TRUE,被 WHERE 丢弃。「不等于某值」要用 IS DISTINCT FROM。
select count(*) from orders where status is null; -- ≈522 select count(*) from orders where status <> 2; -- 漏掉 status 为 NULL 的行 select count(*) from orders where status is distinct from 2; -- 正确的「非已支付」 - 对比
count(*)/count(status)/count(paid_at)三个数字,解释差异参考答案
count(*) 数行,count(列) 只数该列非空的行;两个数字的差就是该列 NULL 的行数。
select count(*), count(status), count(paid_at) from orders; -- 50000 / ≈49478 / ≈34968 - paid_at 为 NULL 的订单在业务上是什么含义?写一句话进 notes.md
参考答案
业务含义:这单还没走完支付流程(待支付或已取消)。「没发生的事」在关系模型里用 NULL 表示,而不是 0 或空字符串--0 是「发生了、金额为零」,含义完全不同。
- 用
coalesce(status, '未知')把脏数据标出来,统计各状态订单数参考答案
coalesce 两个参数类型必须一致,int 先 ::text;也可以用 CASE status WHEN ... 映射成中文。
select coalesce(status::text, '未知') as 状态, count(*) as 单数 from orders group by 1 order by 单数 desc;
col <> 'A' 为什么查不到 col 为 NULL 的行」,并写出正确写法。