COUNT(DISTINCT)在高基数场景下变慢,因其需构建哈希表或排序结构去重,导致内存激增、触发磁盘临时表或shuffle爆炸;应优先用APPROX_COUNT_DISTINCT等近似函数,或下沉至ETL层预聚合。

为什么 COUNT(DISTINCT) 在高基数场景下容易变慢
COUNT(DISTINCT) 本质需要构建哈希表或排序结构来去重,当 column 的唯一值数量(即基数)高达百万甚至千万级时,内存占用陡增,可能触发磁盘临时表(如 MySQL 的 tmp_table_size 不足),或导致 Spark/Trino 中 shuffle 数据爆炸。这不是函数写错了,而是底层去重机制的自然代价。
- 高基数列(如
user_id、request_id)直接套用COUNT(DISTINCT column)很可能让查询从秒级升到分钟级 - PostgreSQL 在小数据集上用
GROUP BY+COUNT(*)拆解反而更慢,因为引入了额外分组开销 - Hive/Spark SQL 中若未开启向量化(
hive.vectorized.execution.enabled=true),COUNT(DISTINCT)的执行计划几乎无法优化
用 APPROX_COUNT_DISTINCT 替代(精度可接受时)
多数业务场景并不需要精确到个位数的去重结果——比如“DAU 估算”“页面 UV 趋势分析”,误差率 ±1% 完全可接受,这时应优先用近似函数。
- MySQL 8.0+ 支持
APPROX_COUNT_DISTINCT()(基于 HyperLogLog),内存恒定,速度提升 3–5 倍 - PostgreSQL 推荐扩展
hll,用hll_cardinality(hll_add_agg(column)) - Spark SQL 直接用
approx_count_distinct(column, 0.01),第二个参数是最大相对误差,默认 0.05 - 注意:
APPROX_COUNT_DISTINCT不能用于主键校验、计费等强一致性场景
拆分 + 分布式预聚合(适合离线宽表)
如果必须精确结果,且数据量持续增长,硬扛单次 COUNT(DISTINCT) 是下策。更可持续的做法是把去重逻辑下沉到 ETL 层。
- 每日增量计算当日的
user_id去重集合(如用 Redis 的PFADD或 Hive 的collect_set()),存为daily_uv_set表 - 周/月统计时,用
SELECT COUNT(*) FROM (SELECT DISTINCT user_id FROM daily_uv_set WHERE dt BETWEEN '2024-01-01' AND '2024-01-07')—— 此时去重对象已是压缩后的每日集合,而非原始日志行 - 关键点:避免在
WHERE条件中对高基数列做函数操作(如WHERE DATE(created_at) = '2024-01-01'),这会让分区裁剪失效,导致全表扫描
MySQL 中的 GROUP_CONCAT 陷阱与替代方案
有人试图绕过 COUNT(DISTINCT),改用 GROUP_CONCAT(DISTINCT column) 再算长度,这是危险操作:
-
GROUP_CONCAT受限于group_concat_max_len(默认仅 1024 字符),高基数下直接截断,结果错误 - 即使调大该值,字符串拼接本身消耗大量内存和 CPU,比原生
COUNT(DISTINCT)更慢 - 正确替代:用
SELECT COUNT(*) FROM (SELECT DISTINCT column FROM t WHERE ...)子查询,让优化器有机会走索引覆盖(例如INDEX(status, user_id))
高基数去重真正的瓶颈往往不在语法,而在数据组织方式——是否提前物化了去重集合、是否规避了运行时哈希膨胀、是否接受了合理误差。盲目调优 COUNT(DISTINCT) 的参数,不如先问一句:这个数,真的必须精确吗?

















