COUNT(DISTINCT) 变慢是因为必须全量加载非NULL值进内存去重,无法跳过或索引加速;高基数导致内存溢出、落盘、OOM;多字段去重引发数据膨胀与Shuffle瓶颈;近似算法或物化预计算才是合理解法。

COUNT(DISTINCT) 变慢,不是因为语法写错了,而是它必须把所有非 NULL 值全加载进内存去重 —— 这一步没法跳过,也没法靠索引绕开。
为什么 COUNT(DISTINCT) 必须全量扫描且无法用索引加速
即使你给 user_id 建了单列索引,COUNT(DISTINCT user_id) 仍大概率走全表扫描。原因有三:
- 数据库要排除
NULL值,而 B+ 树索引默认不存NULL,优化器无法仅靠索引确认“哪些行是NULL” - 去重需要比对全部值,索引只加速查找,不加速集合合并;哈希表或排序过程仍得在内存里做
- 若查询带
WHERE dt BETWEEN '2026-01-01' AND '2026-05-31',但索引是(user_id)而非(dt, user_id),就无法覆盖,被迫回表或扫全索引
高基数字段会让内存和磁盘一起崩溃
当 user_id 是亿级唯一值(比如订单号、设备 ID),COUNT(DISTINCT) 的哈希表会膨胀到 GB 级:
- MySQL 默认
innodb_buffer_pool_size不够大时,哈希表溢出 → 写磁盘临时表 → I/O 暴增 - PostgreSQL 的
work_mem设太小,同样触发落盘,且排序阶段变慢 - Spark SQL 中,每个 task 都要维护自己的
HashSet,节点内存打满后直接OOM
多个 COUNT(DISTINCT) 同时出现,数据会“自我复制”
写成 SELECT dt, COUNT(DISTINCT uid), COUNT(DISTINCT aid), COUNT(DISTINCT pid) FROM t GROUP BY dt,问题更严重:
- Spark 会插入
Expand节点,1 行原始数据变成 3 行(每行标记用于哪个字段去重) - 30 个字段?那就是 1 行 → 30 行,Shuffle 数据量翻 30 倍,网络和磁盘全卡死
- Hive/Spark 中单个
COUNT(DISTINCT)的 shuffle 瓶颈,常因热点 key(如测试账号)导致大量数据路由到同一 reducer
真正难的从来不是“怎么写”,而是“要不要精确”
如果业务能接受 ±1% 误差,APPROX_COUNT_DISTINCT(user_id, 0.01)(Spark 3.0+)、hll_cardinality(PostgreSQL)、uniq(ClickHouse)几乎不占内存;如果必须精确,就得提前物化 —— 比如每天跑一次 INSERT INTO daily_uv SELECT dt, COUNT(DISTINCT user_id) FROM raw_log WHERE dt = '2026-09-16',查时直接读物化表。没人会在实时查询里硬扛亿级 COUNT(DISTINCT),这不是调优问题,是架构选择问题。

















