AVG()的分母是COUNT(col)而非COUNT(*),即自动剔除NULL值参与计算,导致结果偏离业务预期的“全体平均”。

AVG() 的分母不是你直觉里的行数
AVG(score) 实际等价于 SUM(score) / COUNT(score),而不是 SUM(score) / COUNT(*)。只要某行 score 是 NULL,它就既不参与求和,也不参与计数——但这一行仍算在总样本里。比如 10 条记录中有 3 条 score IS NULL,AVG 只用剩下的 7 条算,分母是 7,不是 10。
结果“偏大”是相对业务预期而言的:你想算“全体用户的平均得分”,但 AVG 悄悄把离线用户、未答题用户、录入失败用户全剔除了,只剩高意愿/高活跃用户的分数,均值自然被拉高。
- 传感器表中
temperature IS NULL表示设备离线,AVG() 直接跳过,等于默认这些时段“不存在”,温度曲线就失真 - 订单表
amount IS NULL表示未下单,AVG(amount) 却只在已下单用户中算,LTV 模型会严重高估单客价值 - 分组后嵌套 AVG(),再加外部 WHERE,分母可能既不是原始表行数,也不是当前分组的 COUNT(*),而是更小的 COUNT(score)
COALESCE(AVG(col), 0) 和 AVG(COALESCE(col, 0)) 完全是两回事
前者只兜底整组为 NULL 的情况(比如某用户无任何评分),返回 0;后者把每个 NULL 强制转成 0 再参与计算——分子加了 0,分母却还是 COUNT(col),没变大,语义已扭曲。
真正要按总人数摊薄,得写 SUM(COALESCE(amount, 0)) / COUNT(*)。别指望套一层 AVG() 就能自动修正分母。
-
AVG(COALESCE(score, 0)):把“未打分”当成“打了 0 分”,但分母仍是非 NULL 行数 → 均值虚低,且掩盖缺失问题 -
COALESCE(AVG(score), 0):只解决全 NULL 组返回 NULL 导致应用层报错的问题,不改变计算逻辑 -
IFNULL(AVG(score), 0)(MySQL)或COALESCE(AVG(score), 0)(通用)必须显式加,否则 Java 的getDouble()或 Python 的row[0]会直接崩
怎么一眼看出 AVG() 背后藏了多少坑
光看 AVG(x) 的结果永远不知道分母是多少。同一列,COUNT(*)、COUNT(x)、COUNT(NULLIF(x, 0)) 可能完全不同,而你只盯着那个平均数。
每次写完 AVG 查询,务必并列查这三列:
SELECT AVG(x), COUNT(*), COUNT(x), COUNT(NULLIF(x, 0)) FROM t;
- 如果
COUNT(x)远小于COUNT(*),说明缺失严重,不能直接拿平均值做决策 - 如果
COUNT(NULLIF(x, 0))明显小于COUNT(x),说明存在大量 0 值,需确认 0 是真实数据还是占位符 - 视图或子查询里嵌套 AVG(),外部再加 WHERE,分母已经不是你直觉里的“这组该有多少人”——唯一靠谱的办法,就是把 COUNT 一起查出来对照着看
同一列里不同 NULL 的业务含义不能混着处理
有的 NULL 是“未发生”(如新用户还没下单),有的是“不适用”(如学生用户无社保缴纳记录),有的是“录入失败”(ETL 抽取异常)。统一 COALESCE(col, 0),等于把三类问题强行压成一个数字,后续谁都看不出哪块出了问题。
更稳妥的做法是用 CASE WHEN 主动归类:
-
CASE WHEN status = 'pending' THEN NULL ELSE amount END—— 把“待支付”明确排除,而非让 NULL 模糊承担 -
CASE WHEN col IS NULL THEN 'not_applicable' WHEN col = 0 THEN 'zero_amount' ELSE 'valid' END—— 让 NULL 的语义可追溯 - 报表字段大量为 NULL,优先检查 JOIN 类型是否引入了多余 NULL(比如 LEFT JOIN 未匹配到右表),而不是急着补 0

















