正确做法是分子用COUNT(DISTINCT b)、分母用COUNT(*),二者均在GROUP BY同一分组内计算;需注意NULL处理、数据库兼容性及大数据量下的近似计算优化。

GROUP BY 后怎么算每个分组里某字段去重后的占比
直接用 COUNT(DISTINCT ...) 除以总行数会出错——因为分母必须是当前分组的总行数,不是全表。正确做法是把分母写成 COUNT(*)(同一分组内),分子用 COUNT(DISTINCT ...),两者都在同一 GROUP BY 上下文中执行。
常见错误现象:SELECT a, COUNT(DISTINCT b)/COUNT(*) FROM t GROUP BY a 看似合理,但某些数据库(如旧版 MySQL)在严格模式下可能报错,或因隐式类型转换导致结果为 0;更隐蔽的问题是,如果 b 有 NULL,COUNT(DISTINCT b) 会忽略它,而 COUNT(*) 不会,这个差异必须心里有数。
- 分母永远用
COUNT(*),不是COUNT(b),除非你明确想排除b为 NULL 的行 - 分子
COUNT(DISTINCT b)在绝大多数主流 SQL 引擎(PostgreSQL、SQL Server、MySQL 5.7+、BigQuery、Trino)中都支持,但 SQLite 需要 3.25+ 版本 - 如果字段是字符串且含空格/大小写差异,先确认是否需要
TRIM(UPPER(b))再去重,否则统计口径会偏高
如何避免 NULL 干扰占比计算
COUNT(DISTINCT ...) 天然跳过 NULL,但有时你希望把 NULL 当作一个独立取值来统计(比如“未填写”也算一种状态)。这时候不能依赖原生 DISTINCT,得手动编码。
典型场景:用户表里 country 字段大量为 NULL,你想知道“明确填了国家”和“完全没填”的各自占比。
- 把 NULL 显式转成字符串:
COUNT(DISTINCT COALESCE(country, 'NULL')) - 如果只想单独看 NULL 占比,用
AVG(CASE WHEN country IS NULL THEN 1.0 ELSE 0.0 END)更清晰 - 注意
COALESCE(country, 'NULL')中的字符串字面量不能和真实数据冲突,比如真实国家名里真有 'NULL' 就得换别的标记,如 ''
大数据量下 COUNT(DISTINCT) 性能很慢怎么办
在亿级表上对高基数字段(如 user_id)做 COUNT(DISTINCT),容易触发磁盘 spill 或 OOM,尤其在 Presto/Trino 或旧版 Hive 上。
- 优先考虑近似计算:
APPROX_COUNT_DISTINCT(user_id)(BigQuery、Trino、Spark SQL 支持),误差通常 - 如果必须精确,且字段有业务规律(比如按天分区),可先
GROUP BY date, user_id去重,再外层按 date 统计,减少单次 DISTINCT 输入规模 - 避免在 WHERE 条件前就 SELECT DISTINCT,先过滤再聚合——
WHERE status = 'active'要写在 GROUP BY 之前
跨数据库兼容写法要注意什么
标准 SQL 的 COUNT(DISTINCT x) 兼容性其实不错,但细节差异藏得深:
- Oracle 12c 以前不支持
COUNT(DISTINCT ...)与GROUP BY混用,得套一层子查询 - SQL Server 对
DISTINCT在聚合函数中的位置敏感,COUNT(DISTINCT x)合法,但SUM(DISTINCT x)不合法 - MySQL 5.7 开启
ONLY_FULL_GROUP_BY时,SELECT 列必须在 GROUP BY 中出现或被聚合包裹,别漏掉非分组字段 - PostgreSQL 允许在 SELECT 中混用非分组字段(只要启用了 9.1+ 的 functional dependency 检测),但别依赖它,写法还是保持严格
最保险的写法始终是:所有非聚合字段都出现在 GROUP BY 子句中,所有聚合表达式都显式写出,不要指望引擎自动推断。










