COUNT(DISTINCT列名)在海量数据下慢,根本原因在于必须全量加载非NULL值进内存去重,无法跳过或索引优化;高基数、多字段、精确性要求均使其性能急剧下降,需权衡近似计算或预聚合。

COUNT(DISTINCT 列名) 在海量数据下慢,不是因为写法错,而是它必须一次性把所有非 NULL 值全加载进内存去重——这一步没法跳过,也没法靠索引绕开。
为什么数据库不能用索引直接返回去重数?
即使你给 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 倍,网络和磁盘全卡死
- ClickHouse 例外:它的
uniq()是流式 HyperLogLog++,不膨胀,但uniqExact()仍会爆内存
真正难的从来不是“怎么写”,而是“要不要精确”。如果业务能接受 ±1% 误差,APPROX_COUNT_DISTINCT(PostgreSQL 的 hll_cardinality、ClickHouse 的 uniq)几乎不占内存;如果必须精确,就得提前物化——比如每天跑一次 INSERT INTO daily_uv SELECT dt, COUNT(DISTINCT user_id) FROM raw_log WHERE dt = '2026-06-13',查时直接 SELECT uv FROM daily_uv WHERE dt = '2026-06-13'。没人会在实时查询里硬扛亿级 COUNT(DISTINCT),这不是调优问题,是架构选择问题。


















