用 count(distinct)/count() 计算分组唯一值比率时,需处理分母为0、整数除法截断、null忽略等风险,并注意性能瓶颈与统计意义;推荐cast或乘1.0转换类型,结合count(*)评估样本量。

用 COUNT(DISTINCT) / COUNT() 计算分组内唯一值比率
直接除法就能算出每个分组中唯一值占比,但必须注意分母为 0 的风险和整数除法截断问题。比如想看每个 category 下不同 user_id 占比,核心就是:COUNT(DISTINCT user_id) 除以 COUNT(*)。
- 务必用
CAST(... AS FLOAT)或乘1.0避免整数除法(尤其在 PostgreSQL、SQL Server 中) - 如果某组全为空值(
user_id IS NULL),COUNT(DISTINCT user_id)返回 0,但COUNT(*)不为 0,结果是 0 —— 这符合语义,无需额外处理 - MySQL 8.0+ 和 PostgreSQL 支持窗口函数,但此处用
GROUP BY更直观、更通用
NULL 值是否参与 DISTINCT 计算?
COUNT(DISTINCT column) 默认忽略 NULL —— 这是 SQL 标准行为,不是 bug。如果你的业务要求把 NULL 当作一个“值”来统计多样性(比如缺失本身也是一种类型),就得手动补进去。
- 写成
COUNT(DISTINCT COALESCE(column, 'NULL_MARKER'))是常见做法,但要注意'NULL_MARKER'不能和真实数据冲突 - 更稳妥的是用条件聚合:
COUNT(DISTINCT CASE WHEN column IS NULL THEN -1 ELSE column END),前提是column是数值型;字符串可用md5(null)或固定 UUID 模拟 - 别用
GROUP BY column+COUNT(*)再拼回来——逻辑复杂且易错,不如一步算清
性能瓶颈常出现在 DISTINCT 的中间结果膨胀
当分组内行数多、去重字段长(如长文本、JSON 字段),COUNT(DISTINCT) 可能触发大量内存排序或临时磁盘表,尤其在 MySQL 或旧版 SQLite 中。
- 先确认是否真需要精确去重:如果只是粗略评估多样性,可用 HyperLogLog 类似算法(如 PostgreSQL 的
approx_count_distinct(),或 ClickHouse 的uniq()) - 加复合索引没用 ——
COUNT(DISTINCT)几乎不走索引,优化重点在减少分组前的数据量(加WHERE过滤)或拆分大分组(如按时间分区) - 避免在同一个查询里对多个字段同时做
COUNT(DISTINCT a), COUNT(DISTINCT b)—— 多数引擎会分别跑两遍去重,代价翻倍
用窗口函数对比组内 vs 全局多样性时的陷阱
想比较“某品类用户去重率”和“全站用户去重率”,容易误用窗口函数导致逻辑错误。比如写 AVG(COUNT(DISTINCT user_id)) OVER() 是非法的 —— 聚合不能嵌套窗口。
- 正确做法是两层查询:外层
GROUP BY category算各组比率,内层用子查询或 CTE 算全局比率,再 JOIN 或用标量子查询 - PostgreSQL 支持
count(DISTINCT user_id) FILTER (WHERE TRUE) OVER (),但这是语法糖,实际仍需先物化全局去重结果 - 别指望
SELECT DISTINCT ON能替代 —— 它只取每组首行,和计数完全无关
COUNT(DISTINCT)=2 就得 100%,但样本太小,统计意义弱。建议搭配 COUNT(*) 一起输出,人工判断置信度。**











