「非已支付」的数字对不上:NULL 上场

你汇报「非已支付状态的订单」时数字对不上--运营说还有一批 status 为空的脏数据,而 status <> 2 把它们全漏掉了。

学 40 min
练 65 min
盘 15 min
共 120 分钟

学 · 40 min

  1. 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。

  2. 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 行负责。

  3. 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。

  4. 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

  1. 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)
  2. 查 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; -- 正确的「非已支付」
  3. 对比 count(*) / count(status) / count(paid_at) 三个数字,解释差异
    参考答案

    count(*) 数行,count(列) 只数该列非空的行;两个数字的差就是该列 NULL 的行数。

    select count(*), count(status), count(paid_at) from orders;
    -- 50000 / ≈49478 / ≈34968
  4. paid_at 为 NULL 的订单在业务上是什么含义?写一句话进 notes.md
    参考答案

    业务含义:这单还没走完支付流程(待支付或已取消)。「没发生的事」在关系模型里用 NULL 表示,而不是 0 或空字符串--0 是「发生了、金额为零」,含义完全不同。

  5. 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 的行」,并写出正确写法。