COUNT(DISTINCT)在大数据量下易OOM或卡死,因其依赖内存哈希表去重,内存占用与唯一值数量正相关;应改用APPROX_DISTINCT、两阶段聚合或预计算HLL/Bitmap等降维方案。

COUNT(DISTINCT) 在大数据量下不是“慢一点”,而是容易直接 OOM 或卡死——尤其当去重字段基数高、无索引、或跨多表 JOIN 后计算时。根本解法不是调优单条 SQL,而是换思路:用近似、预计算、或结构降维。
为什么 COUNT(DISTINCT) 在大数据场景下会崩
它底层必须构建哈希表或排序去重,内存占用与 distinct 字段的唯一值数量正相关。比如 10 亿行用户行为日志中统计 user_id 的 UV,若活跃用户有 5000 万,Spark 或 Hive 就得在内存里维护 5000 万个键值对,shuffle 数据量爆炸,极易触发 GC、OOM 或长尾 task。
常见症状包括:
- 执行计划里出现
HashAggregate+ 大量Exchange,且 shuffle write 超过 10GB - 任务卡在 stage 2/3 长时间不动,executor 日志反复报
java.lang.OutOfMemoryError: Java heap space - 同一查询在小表上秒出,数据量翻 10 倍后耗时呈指数增长(非线性)
用 APPROX_DISTINCT 替代 COUNT(DISTINCT)(Presto/Trino/StarRocks)
这是最直接的“精度换性能”方案,底层基于 HyperLogLog(HLL),误差率通常控制在 1–2%,内存占用恒定(KB 级),不随基数增长。
实操要点:
- Presto/Trino 写法:
APPROX_DISTINCT(user_id),支持APPROX_DISTINCT(x, e)指定误差容忍(如e = 0.01) - StarRocks 推荐用
HLL_UNION_AGG(hll_hash(user_id)),需先建物化视图或写入时预计算 HLL 列 - 注意:
APPROX_DISTINCT不能用于WHERE或JOIN条件,只适合最终聚合展示 - 别在 WHERE 子句里嵌套它,例如
WHERE APPROX_DISTINCT(id) > 1000是语法错误
改写为 GROUP BY + COUNT(Spark SQL 自动优化,Hive 需手动)
Spark SQL 2.x+ 默认对 COUNT(DISTINCT) 做两阶段聚合(先 GROUP BY 去重,再全局计数),但 Hive 不会——它默认走单 Reduce,极易倾斜。
手动优化 Hive/旧版 Spark 的写法:
- 错:
SELECT COUNT(DISTINCT user_id) FROM events - 对(Hive 必须):
SELECT COUNT(*) FROM (SELECT user_id FROM events GROUP BY user_id) t - 如果同时算多个 distinct 字段(如
user_id和product_id),不要写两个子查询,改用UNION ALL+ 标记列合并,避免重复扫描 - 确保
GROUP BY字段上有索引(OLAP 场景下优先建 Bitmap 索引或 HLL 列)
提前物化 HLL 或 Bitmap 列(长期收益最大)
把实时去重压力转移到写入或 ETL 阶段,查时变成 O(1) 聚合。适合 UV 类指标高频查询的场景。
关键动作:
- 写入时计算:
HLL_INIT(user_id)(StarRocks)、bitmap_agg(to_bitmap(user_id))(Doris)、或 Spark 中用functions.hll_init_agg("user_id") - 建物化视图(StarRocks):
CREATE MATERIALIZED VIEW uv_by_day AS SELECT dt, HLL_UNION_AGG(hll_user) FROM logs GROUP BY dt - 注意 HLL 不支持交集/差集的精确计算(如“昨日新增 UV”需额外逻辑),Bitmap 更灵活但存储更大
- 别在明细表上直接加 HLL 列后还频繁
GROUP BY其他字段——这会让物化失效,要按查询维度预聚合
真正卡住人的,往往不是不会写 COUNT(DISTINCT),而是没意识到:当数据量超过千万行、去重字段基数超百万时,“精确”本身就成了性能毒药。选近似、做预计算、或者把去重逻辑下沉到写入链路——这三个方向里,至少得选一个落地,否则永远在调 executor memory 和 spark.sql.adaptive.enabled。


















