postgresql-hll扩展通过流式累积哈希实现近似去重,误差±1–2%,提速几十倍;需CREATE EXTENSION启用,写入侧预聚合更高效;MySQL 8.0+的APPROX_COUNT_DISTINCT仅为别名,无实际加速。

PostgreSQL 用 postgresql-hll 扩展替代 COUNT(DISTINCT)
直接替换就能提速几十倍,前提是接受 ±1–2% 误差。它不是“加速版 COUNT DISTINCT”,而是完全不同的数据结构:把去重过程变成 hll_hash_bigint() + hll_add_agg() 的流式累积。
实操建议:
- 先启用扩展:
CREATE EXTENSION hll;(需 superuser 权限) - 建表时预留
hll类型字段,比如uv_hll hll,避免每次查都从原始列重建 - 写入侧打点比查询侧计算更高效:在日志写入时就执行
hll_add_agg(hll_hash_bigint(user_id))并存入聚合表,查时只调hll_cardinality() - 别盲目调高精度参数——
hll_add_agg(hll_hash_bigint(user_id), 11, 5, 1024)中的 11 是 log2(m),m 越大内存占用翻倍,但误差仅从 1.2% 降到 0.9%,通常不值得
MySQL 8.0+ 不要信 APPROX_COUNT_DISTINCT()
这个函数名是空壳,执行计划和 COUNT(DISTINCT) 完全一致,照样建哈希表、照样爆内存、照样慢。官方文档没明说,但源码里它就是个 alias。
能用的只有两种路:
- 采样估算:用
WHERE RAND() 抽 1%,再乘以 100;但注意如果抽样中 <code>COUNT(*) / COUNT(DISTINCT)比值 - 预计算落地:每天跑一次
INSERT INTO daily_uv SELECT DATE(log_time), COUNT(DISTINCT user_id) FROM logs WHERE ... GROUP BY DATE(log_time),查时走主键索引,毫秒返回 - 别碰
TABLESAMPLE SYSTEM (1):它按页采样,对时间倾斜或用户 ID 分布不均的数据偏差极大
ClickHouse 和 MaxCompute 的原生 HLL 支持
这两个系统把 HLL 当基础能力做进引擎了,不用装扩展、不用手写 hash,开箱即用且默认精度合理。
关键区别点:
- ClickHouse 用
uniq()(HLL)、uniqCombined()(自动切分内存)、uniqHLL12()(固定 12bit 精度),推荐日常用uniq(),误差约 1%,性能碾压COUNT(DISTINCT) - MaxCompute 要走完整 sketch 生命周期:
HLL_COUNT_INIT()→ 写入 →HLL_COUNT_MERGE_PARTIAL()→HLL_COUNT_EXTRACT();适合需要复用中间结果的场景,比如按天 merge 后再算周 UV - 所有 HLL 函数都不支持反查原始值——你只能知道“大概多少个”,不能列出是哪些 user_id
Spark SQL 的 COUNT(DISTINCT) 底层代价被严重低估
它根本不是单次哈希去重,而是通过 Expand + 多轮 HashAggregate 实现的“膨胀-分组-合并”流程。一个 COUNT(DISTINCT a), COUNT(DISTINCT b) 会让数据量翻 3 倍以上,shuffle 数据暴增。
更稳的写法是手动拆解:
- 先用
SELECT DISTINCT a FROM t WHERE ...得到唯一值集,再COUNT(*),避免 Expand 膨胀 - 如果 a 和 b 都来自同一张大表,优先考虑
GROUP BY分治:比如按小时GROUP BY dt后分别去重再SUM(),比全量去重快一个数量级 - 确认是否真需要精确值——Spark 也支持
approx_count_distinct(),误差控制在 2% 内,但必须显式启用,不会自动 fallback
真正卡住性能的往往不是算法本身,而是没把去重动作从 OLTP 主链路里剥离出去。HLL 再快,也救不了还在每秒写入时同步跑 COUNT(DISTINCT) 的设计。



















