MySQL 8.0+ 的 APPROX_COUNT_DISTINCT() 是 COUNT(DISTINCT) 的别名,无近似能力;真实可行方案是采样估算(如 RAND() 过滤)或预计算(如日粒度 UV 表),后者性能提升最显著。

MySQL 8.0+ 没有原生 HyperLogLog 支持,APPROX_COUNT_DISTINCT() 是假的——它只是 COUNT(DISTINCT) 的别名,不加速也不近似。
MySQL 8.0 的 APPROX_COUNT_DISTINCT() 其实是空壳
你执行 EXPLAIN SELECT APPROX_COUNT_DISTINCT(user_id) FROM logs,看到的执行计划和 COUNT(DISTINCT user_id) 完全一样:照样建哈希表、照样全字段扫描、照样爆内存。官方文档没明说,但源码和实测都证实——这个函数名是预留的,目前未实现近似逻辑。
常见错误现象:
- 误以为加了
APPROX_前缀就自动走采样或 sketch 算法 - 在慢查询日志里发现该语句耗时和原始
COUNT(DISTINCT)无差异 - 监控显示
Sort_merge_passes或Created_tmp_disk_tables暴涨
真能用的近似方案只有采样 + 推算
当业务允许 ±5% 误差(比如用户活跃度趋势、渠道效果初筛),可手动构造采样估算。核心不是“算法多先进”,而是控制采样偏差和置信区间。
实操建议:
- 用
TABLESAMPLE SYSTEM (1)(MySQL 8.0.22+)抽 1% 行,注意它按页采样,对倾斜数据不友好;更稳的方式是加RAND()过滤:WHERE RAND() - 对采样结果跑
COUNT(DISTINCT user_id),再乘以 100 得到估算值 - 加
LIMIT 100000防止采样集过大拖慢子查询 - 避免在
user_id有严重长尾分布时直接乘倍数——先看采样中COUNT(*)和COUNT(DISTINCT)比值,若接近 1,说明去重率高,乘法误差小;若比值
真正有效的优化永远落在预计算和结构改造上
近似只是权宜之计。线上大表(千万级以上)的 COUNT(DISTINCT) 查询,90% 的性能问题根源不在算法,而在没有把“去重”这件事提前切出事务主链路。
关键路径选择:
- 每天凌晨用
INSERT INTO daily_uv SELECT DATE(log_time), COUNT(DISTINCT user_id) FROM logs WHERE log_time >= '2026-04-13' AND log_time 落库,查时直接 <code>SELECT uv FROM daily_uv WHERE dt = '2026-04-13' - 如果要支持任意时间范围,把
log_time和user_id组成联合索引(log_time, user_id),让GROUP BY log_time能走索引下推,避免临时表 - 极端高频场景(如实时看板),改用 Redis 的
PFADD在写入日志时同步打点,查时PFCOUNT——但注意PFADD不保证精确,且无法回溯原始user_id
最常被忽略的一点:COUNT(DISTINCT) 的瓶颈往往不在去重本身,而在于它强制触发全表扫描后,还要维护一个无法下推的哈希状态。预计算表哪怕只存天粒度,也能把响应时间从秒级压到毫秒级——而所有“算法优化”的尝试,都在和这个事实硬刚。


















