写 SQL 做统计时,最常见的误判之一,就是看到 COUNT 结果与预期不一致,第一反应认为是数据库“漏数”了。实际上,多数问题都出在语义理解:COUNT(*)、COUNT(列名)、COUNT(DISTINCT 列名) 统计的对象并不一样。
这篇文章会集中讲清三件事:什么时候 COUNT(列名) 会比 COUNT(*) 小,为什么 COUNT(DISTINCT 列名) 可能返回 0 或明显偏小,以及在 GROUP BY、LEFT JOIN 之后为什么看起来“每组都只有 1”或“人数对了但 SQL 很危险”。
先分清:`COUNT(*)` 和 `COUNT(列名)` 到底在数什么
COUNT(列名) 通常会比 COUNT(*) 小,原因不在数据库异常,而在于两者统计口径不同:

COUNT(*)统计当前结果集中的所有物理行COUNT(列名)只统计该列值不为NULL的行
因此,这两个结果的差值,往往就是该列中 NULL 的数量。
SELECT COUNT(*) AS total_rows,
COUNT(user_id) AS non_null_user_id,
COUNT(*) - COUNT(user_id) AS null_user_id
FROM events;这里还有几个很容易混淆的点:
- 空字符串
''会被计入COUNT(列名) - 数值
0会被计入COUNT(列名) - 布尔
false也会被计入COUNT(列名) - 只有数据库层面真正的
NULL,才会被COUNT(列名)跳过 - 字符串
'NULL'不是NULL,不会被排除
所以,COUNT 从来不会“漏数”,它只是严格按规则统计。
为什么在 `LEFT JOIN` 后更容易看错
在 LEFT JOIN 场景里,很多人会把 COUNT(右表.id) 误当成左表总数。其实它统计的是“成功关联到右表的行数”。
SELECT COUNT(users.id) AS user_rows,
COUNT(events.id) AS matched_event_rows
FROM users
LEFT JOIN events ON users.id = events.user_id;上面这类 SQL 中,COUNT(events.id) 不是在数 users 原始基数,而是在数右表匹配成功的记录数。右表没匹配上的那些行,由于 events.id 为 NULL,不会被计入。
为什么 `COUNT(DISTINCT 列名)` 会返回 0 或偏小
COUNT(DISTINCT 列名) 不只是去重,它还会彻底忽略 NULL。这意味着多个 NULL 不会被算成一个唯一值,而是直接当作“不存在”。
- 整列全是
NULL,返回0 - 只有一行非
NULL,其余全是NULL,返回1 - 如果埋点日志中的
user_id大量缺失并存为NULL,活跃用户数就会明显偏低
SELECT COUNT(DISTINCT user_id) AS active_users
FROM events;如果你的业务需要把 NULL 也纳入去重统计,就必须显式转换,例如:
SELECT COUNT(DISTINCT COALESCE(user_id, ''))
FROM events; 这类写法的关键不是“修复 COUNT”,而是先明确业务上是否真的希望把缺失值视为一个独立类别。
为什么 `GROUP BY + COUNT` 看起来每组都等于 1
如果你发现每个分组的 COUNT(*) 都返回 1,通常不是 COUNT 出错,而是分组粒度过细。换句话说,GROUP BY 中混入了高唯一性字段,导致每组只剩下一行。
最常见的错误写法是:
SELECT category, order_id, COUNT(*)
FROM orders
GROUP BY category, order_id;如果 order_id 本身就是主键,那么每个分组天然只有 1 行,结果当然几乎全是 1。
排查时可以重点检查这些字段是否被放进了 GROUP BY:
idcreated_atuuid- 其他唯一或近似唯一字段
更合理的业务分组,应该保留真正的分析维度,例如:
SELECT date, region, COUNT(*)
FROM events
GROUP BY date, region;而不是把像 event_id 这样的高基数字段一起带进去:
SELECT date, region, event_id, COUNT(*)
FROM events
GROUP BY date, region, event_id;一个很实用的自检方法
当你怀疑分组过细时,可以对比 COUNT(*) 和 COUNT(DISTINCT user_id)。如果前者远大于后者,通常说明结果集中存在明显重复,问题可能出在分组方式,也可能出在关联放大。
`JOIN` 后直接写 `COUNT(DISTINCT)`,是在救火还是埋雷
COUNT(DISTINCT) 有时能把结果“修正回来”,但这并不代表 SQL 写对了。很多场景下,它只是掩盖了错误的 JOIN 逻辑,代价是性能下降,指标也更难解释。
例如:
SELECT COUNT(DISTINCT users.id)
FROM users
LEFT JOIN events ON users.id = events.user_id;假设 1 个用户对应 10 条事件,那么 LEFT JOIN 后底层行数已经膨胀了 10 倍。虽然 COUNT(DISTINCT users.id) 看起来返回了正确人数,但查询过程本身已经变重,后续再叠加筛选、分组或更多关联时,风险会继续放大。
更稳妥的写法,通常是先在子查询中完成去重,再做外层统计:
SELECT COUNT(*)
FROM (
SELECT DISTINCT user_id
FROM events
WHERE ...
) t;使用这种方式时,还应确保关联字段或去重字段有索引,例如 events.user_id,否则子查询本身也可能很慢。
另外,应该尽量避免在 LEFT JOIN 后直接写 COUNT(DISTINCT 右表.列),尤其是在右表匹配失败会产生大量 NULL 的情况下。因为这时你看到的不是“原始右表分布”,而是关联结果集中过滤掉 NULL 之后的统计结果。
最后一个常被忽略的点:`COUNT` 统计的是当前结果集
很多统计偏差真正难查,不是因为 SQL 报错,而是因为它安静地返回了一个“看起来合理”的数字。
需要特别注意的是,NULL 是否被计入,是在 WHERE、GROUP BY 等处理之后,基于当前结果集再判断的。也就是说,COUNT(列名) 呈现的是查询结果中该列的非空数量,并不是原始表里这列的完整分布。
一旦在复杂查询中把这件事混淆,偏差就不会报错,而是会直接进入报表、埋点分析和业务指标中。
排查 COUNT 异常时,优先看这 4 件事
- 先确认你要的是
COUNT(*)、COUNT(列名),还是COUNT(DISTINCT 列名) - 检查目标列里是否存在真正的
NULL,而不是空字符串或字符串'NULL' - 确认
GROUP BY是否带入了id、uuid、created_at这类高唯一性字段 - 检查
JOIN是否放大了结果集,别用COUNT(DISTINCT)直接掩盖关联问题
把这几步先过一遍,绝大多数“COUNT 结果不对”的问题,都能更快定位到原因。








