approx_count_distinct 比 count(distinct) 快,因其采用 hyperloglog++ 等概率算法估算基数,仅维护约12kb固定大小 sketch 结构,内存不随数据量增长,而后者需构建哈希表或排序,内存线性增长且耗时高。

APPROX_COUNT_DISTINCT 为什么比 COUNT(DISTINCT) 快?
因为它根本不去“真正去重”,而是用 HyperLogLog++(HLL++)这类概率算法估算基数,只维护一个固定大小的内存结构(通常几 KB),而不是把亿级 ID 全 load 进内存做哈希或排序。
关键区别在于:
-
COUNT(DISTINCT)必须收集所有非 NULL 值,构建哈希表或排序,内存占用随唯一值数量线性增长——user_id 有 5 亿不同值,哈希表就可能占数 GB -
APPROX_COUNT_DISTINCT只用一个 12KB 左右的 sketch 结构(如 HLL++ 密集型),无论输入是 10 万还是 10 亿行,内存开销几乎不变 - 它不保证精确,但误差可控:默认相对标准偏差约 0.8–1.5%,即 1 亿真实 distinct 值,结果在 9920 万~1.015 亿之间
哪些数据库支持 APPROX_COUNT_DISTINCT?参数差异要注意
不是所有数据库都叫这个名字,函数名、参数和默认精度差别挺大,写错就报错或返回 NULL:
- PostgreSQL:用
approx_count_distinct()(需安装hll扩展),或直接hll_cardinality(hll_add_agg(user_id)) - MySQL 8.0+:原生支持
APPROX_COUNT_DISTINCT(user_id),不接受误差参数 - Databricks / Spark SQL:
approx_count_distinct(col, relativeSD),relativeSD可设 0.01(更准但略慢)到 0.1(更快但误差稍大) - ClickHouse:
uniq(user_id)是近似版(HLL),uniqExact(user_id)才是精确版(会爆内存) - Oracle:从 12.1.0.2 起支持
APPROX_COUNT_DISTINCT(user_id),忽略 NULL,返回NUMBER
WHERE 条件写错会让 APPROX_COUNT_DISTINCT 白费力气
函数本身再快,也救不了低效的过滤逻辑。常见翻车点:
- 写成
WHERE DATE(oper_time) = '2026-08-11'→ 时间索引失效,先全表扫描再计算近似值,耗时全花在 IO 上 - 索引是
(user_id),但查询带WHERE biz_channel = 'pay' AND oper_time >= '2026-08-01'→ 无法覆盖,仍要回表或扫全索引 - 正确做法:建联合索引
(biz_channel, oper_time, user_id),让 WHERE 和聚合字段一起被覆盖 - 如果业务允许,把时间条件下推到预聚合层,比如每天跑一次
INSERT INTO daily_stats SELECT biz_channel, approx_count_distinct(user_id) FROM raw WHERE dt = '2026-08-11' GROUP BY biz_channel
多个近似去重同时出现,别以为能自动复用 sketch
写成 SELECT approx_count_distinct(uid), approx_count_distinct(aid), approx_count_distinct(pid) FROM log GROUP BY dt 看似省事,实际代价不小:
- Spark SQL 会为每个字段单独维护一个 HLL sketch,内存占用 ×3
- ClickHouse 的
uniq()每个字段独立计算,不共享中间状态 - 真要降资源,得手动合并:比如把 uid/aid/pid 拼成
concat(uid, '_', aid, '_', pid)再算一次approx_count_distinct(),但这改变了语义,仅适用于“组合唯一”场景 - 更稳妥的方式是分步物化:先用
SELECT dt, uid FROM log WHERE ...写入临时表,再分别对各字段跑近似统计
APPROX_COUNT_DISTINCT 快的本质。但它的价值完全建立在“业务真能接受误差”这个前提上;一旦需要精确结果,再快的近似函数也只是障眼法。











