APPROX_COUNT_DISTINCT适用于高基数列(如user_id)的近似去重计数,基于HyperLogLog算法,内存恒定约12KB、误差≤2%,比COUNT(DISTINCT)快且不溢出,但不适用于低基数、小表或需精确结果的场景。

APPROX_COUNT_DISTINCT 适合高基数列的近似计数
当列中唯一值数量极大(比如用户ID、设备ID、URL路径),用 COUNT(DISTINCT column) 会显著拖慢查询,甚至触发内存溢出或超时。此时应改用数据库原生支持的近似去重函数,而非硬扛精确计算。
PostgreSQL、BigQuery、Spark SQL 和较新版本的 Hive 都提供 APPROX_COUNT_DISTINCT(或别名如 APPROX_COUNT_DISTINCT / NDV / HLL 函数),底层基于 HyperLogLog(HLL)算法,误差通常控制在 ±1% 以内,内存占用恒定且极低。
- PostgreSQL:直接使用
APPROX_COUNT_DISTINCT(column)(需启用pg_stat_statements或安装扩展如hll) - BigQuery:
APPROX_COUNT_DISTINCT(column)是默认推荐方式,比COUNT(DISTINCT)快 3–10 倍 - Hive/Trino:优先用
APPROX_COUNT_DISTINCT(column);若不可用,可退化为NDV(column)(Hive 3+)或approx_distinct(column)(Trino) - MySQL 8.0+ 不原生支持,需借助
GROUP BY+ 采样或迁移到 ClickHouse(uniq())等替代方案
为什么不能对高基数列强行用 COUNT(DISTINCT)
COUNT(DISTINCT) 在执行时需构建完整哈希表存储所有唯一值,内存消耗与唯一值数量线性增长。例如:10 亿行数据含 5 亿个不同 user_id,该操作可能占用数十 GB 内存,并引发 spill-to-disk,严重拖慢响应。
典型错误现象包括:
- 查询卡在 “Executing” 状态超过 10 分钟无返回
- 日志中出现
java.lang.OutOfMemoryError: Java heap space(Hive/Spark) - PostgreSQL 报错
ERROR: out of memory或触发work_mem限制告警 - BigQuery 显示 “Resources exceeded during query execution”
APPROX_COUNT_DISTINCT 的误差与可控性
近似函数不是“随便估”,而是有明确误差边界和可调精度的工程方案。HLL 类算法的相对标准误差约为 1.04 / sqrt(m),其中 m 是寄存器数量——数据库通常已设合理默认值(如 BigQuery 默认 m=2^15)。
你不需要手动调参,但需知道这些事实:
- 误差是统计意义上的,不偏向多估或少估,多次运行结果波动小
- 不支持
WHERE条件下嵌套去重(如APPROX_COUNT_DISTINCT(CASE WHEN status='active' THEN user_id END)在部分引擎中会报错,应改用条件聚合包裹) - 无法与窗口函数联用(
APPROX_COUNT_DISTINCT() OVER (...)语法非法) - 若业务强依赖精确值(如财务对账),仍必须用
COUNT(DISTINCT)并接受性能代价
替代方案:采样 + COUNT(DISTINCT) 仅作估算参考
当数据库不支持 APPROX_COUNT_DISTINCT,又无法忍受全量去重开销时,可人工采样估算,但要注意陷阱:
- 用
TABLESAMPLE BERNOULLI(1)(PostgreSQL)或LIMIT+RAND()(MySQL)做随机抽样,再算COUNT(DISTINCT),最后乘以抽样比例反推 —— 但该法在分布倾斜时误差极大(比如 99% 的user_id集中在 1% 的时间窗口) - 更稳妥的是分桶后分别近似再合并,例如按
user_id % 100分 100 组,每组用HLL合并,最终 union 各组 sketch —— 这已接近原生APPROX_COUNT_DISTINCT的实现逻辑 - 简单
LIMIT 100000后去重毫无意义:它只反映头部数据分布,对高基数列完全失真
真正要落地,优先确认你用的数据库版本是否已内置可靠近似函数;否则,与其花时间调采样,不如推动升级或换引擎 —— 近似去重不是“将就”,而是针对高基数场景的标准解法。











