2. 空字符串与NULL混淆:清洗后统计口径不一致的排查

问题定位:同一列缺失值被写成两种表示

某订单明细表 orders 在清洗后需要统计“有效下单用户数”和“缺失手机号订单数”。清洗脚本对缺失手机号做了两件事:一部分记录被写成空字符串 '',另一部分保持数据库原生 NULL。随后用 COUNT(phone) 统计非缺失数、用 phone = '' 统计空串数,结果与原始表行数对不上。

关键差异在于 SQL 的三值逻辑:NULL 参与任何比较都返回 UNKNOWN,phone = '' 不会命中 NULL 行;而 COUNT(phone) 会跳过 NULL,却会把 '' 当作有效值计入。因此“缺失”被拆成两类后,任何单一条件都无法覆盖全部缺失行。

缺失手机号在清洗与统计中的两条分流路径 原始 orders 表 phone 缺失行 统一为 NULL 共 120 行 分支A:写成空字符串 '' COALESCE 兜底 / 导出补空 分支B:保持 NULL 未命中替换规则 phone = '' → 命中 70 行 COUNT(phone) 计入这 70 行 phone IS NULL → 命中 50 行 COUNT(phone) 跳过这 50 行 统计口径偏差对照(示例数据) 缺失总数应为 120 行 仅用 phone = '' 统计 → 70 行,漏掉 50 行 NULL 仅用 phone IS NULL 统计 → 50 行,漏掉 70 行空串 统一后:phone IS NULL OR phone = '' → 120 行,与原始缺失数一致

排查步骤:先量化两类缺失,再统一表示

  1. 分别统计两种缺失表示的行数,确认偏差来源与量级。
  2. 用“缺失总数 = 空串数 + NULL 数”与原始表缺失行数比对,验证是否还有第三种表示(如 'NULL' 字符串、' ' 空格)。
  3. 在清洗阶段统一为 NULL,并同步修正所有下游统计条件。
  4. 重跑聚合,验证“有效数 + 缺失数 = 总行数”恒等式成立。
-- 1. 量化两类缺失(示例数据:总行 1000,缺失 120)
SELECT
  COUNT(*)                                   AS total_rows,
  COUNT(phone)                               AS non_null_rows,
  SUM(CASE WHEN phone = '' THEN 1 ELSE 0 END) AS empty_str_rows,
  SUM(CASE WHEN phone IS NULL THEN 1 ELSE 0 END) AS null_rows,
  SUM(CASE WHEN phone = 'NULL' OR TRIM(phone) = '' THEN 1 ELSE 0 END) AS other_bad_rows
FROM orders;

-- 2. 统一缺失表示:空串、纯空格、字面量 'NULL' 全部归一为 NULL
UPDATE orders
SET phone = NULL
WHERE phone = '' OR TRIM(phone) = '' OR UPPER(phone) = 'NULL';

-- 3. 统一后的统计口径
SELECT
  COUNT(*)                                        AS total_rows,
  COUNT(phone)                                    AS valid_phone_rows,
  SUM(CASE WHEN phone IS NULL THEN 1 ELSE 0 END)  AS missing_phone_rows
FROM orders;
统一表示后,COUNT(phone) 与 phone IS NULL 天然互补,不再需要 OR phone = '' 这类易漏条件;这也是把“缺失”收敛到单一表示的核心收益。

验证一致性:用恒等式与分组交叉核对

仅看总数一致还不够,需按业务维度分组核对,防止两类缺失在不同分组中分布不均导致局部口径仍错。下面用“有效数 + 缺失数 = 分组总数”逐组校验,任何一组不成立即说明该组仍存在未归一的缺失表示。

-- 分组恒等式校验:返回结果应为空集
SELECT region,
       COUNT(*)                                       AS grp_total,
       COUNT(phone)                                   AS grp_valid,
       SUM(CASE WHEN phone IS NULL THEN 1 ELSE 0 END) AS grp_missing
FROM orders
GROUP BY region
HAVING COUNT(*) <> COUNT(phone) + SUM(CASE WHEN phone IS NULL THEN 1 ELSE 0 END);

-- 清洗前后缺失数对比(示例预期:清洗前 120,清洗后仍为 120,仅表示统一)
SELECT 'before' AS stage, COUNT(*) AS missing_cnt
FROM orders_bak WHERE phone = '' OR phone IS NULL
UNION ALL
SELECT 'after', COUNT(*) FROM orders WHERE phone IS NULL;
检查项清洗前(示例)清洗后(示例)判定
空字符串行数700已归一
NULL 行数50120已合并
缺失总数120120一致,未误删
有效数 + 缺失数880 + 120 = 1000880 + 120 = 1000恒等式成立
若清洗后缺失总数小于清洗前,说明归一过程中误删了行(例如把空串行整行删除),需回滚并从备份表重新执行 UPDATE,而不是 INSERT 覆盖。

可复用检查清单

  • 清洗规则中所有“缺失替换”是否统一指向 NULL,而非混用 ''。
  • 下游统计是否仍残留 = '' 条件,需改为 IS NULL。
  • 是否检查了 'NULL' 字面量、纯空格等隐性缺失表示。
  • 是否用分组恒等式校验,而非只看全局总数。
  • 是否保留清洗前备份表,便于缺失数前后对比。