针对大数据表去重计数缓慢,可根据精度容忍度选择优化方案:可接受1-2%误差时采用HyperLogLog(内存小、速度快);要求精确时通过覆盖索引、GROUPBY子查询或分桶优化;低基数场景可用bitmap聚合;固定维度可用预计算或物化视图以空间换时间。
COUNT进阶续:超大表去重计数极致优化
时间:2026-06-04
浏览:157
文章目录
上周聊了 COUNT(*) 在大表上的近似计数与 HyperLogLog,这周继续深入 COUNT 的进阶话题——去重计数。你是不是也遇到过这样的场景:想查一下昨天的独立访客数,或者统计本周的活跃设备量,一个 COUNT(DISTINCT user_id) 下去,10亿行的表跑了半小时还没出来?这种时候,硬等肯定不是办法。
其实,去重计数的优化,关键还是要看你的业务场景对精度的容忍度有多高。我们先来快速划分一下:
| 场景 | 需求 | 可接受误差 |
|---|---|---|
| 运营报表、趋势图 | DAU、MAU | 1-2% |
| 财务、库存、对账 | 精确金额、订单数 | 0% |
下面按误差从大到小,聊聊几种主流的优化手段。
方案一:近似去重(HyperLogLog)—— 要快,误差1-2%能接受
HyperLogLog,说白了就是一种概率算法,用极其固定的内存(大概16KB)去估算去重元素的个数。背后的逻辑其实挺有意思的:它把每个元素哈希一下,然后统计哈希值二进制表示里前导零的最大长度,通过这个长度来反推去重总数。内存占用小,速度极快,误差又可控,简直是这类场景里的利器。
适用场景: UV、DAU、独立IP、搜索词去重统计等,只要不涉及真金白银,基本都能用。
实现方式:
- Redis HyperLogLog(最常用)
import redis
r = redis.Redis()
for user_id in logs:
r.pfadd("daily_uv:2026-06-02", user_id)
uv = r.pfcount("daily_uv:2026-06-02") # 误差1%以内
- PostgreSQL + hll扩展
CREATE EXTENSION hll;
SELECT hll_cardinality(hll_add_agg(hll_hash_integer(user_id))) FROM logs;
方案二:精确去重,但用索引优化 —— 必须准,也要尽量快
如果误差一丁点都不能有,那我们就得在索引设计上动脑筋,尽量少扫描数据。
技巧1:覆盖索引
COUNT(DISTINCT user_id) 其实只需要 user_id 这一列。如果 (user_id) 上有索引,InnoDB 可以直接去扫索引而不是全表,I/O 开销能降不少。
-- 确保user_id有索引
CREATE INDEX idx_user_id ON logs(user_id);
SELECT COUNT(DISTINCT user_id) FROM logs;
技巧2:GROUP BY 代替 DISTINCT
在一些数据库里,用 GROUP BY 做子查询,外层再 COUNT(*),有时会比直接的 COUNT(DISTINCT) 更快,具体得看优化器的脾气。
SELECT COUNT(*) FROM (SELECT user_id FROM logs GROUP BY user_id) t;
实测对比(1000万行,user_id有索引):
| 写法 | 耗时 |
|---|---|
COUNT(DISTINCT user_id) | 12秒 |
GROUP BY子查询 | 11秒(差异不大) |
技巧3:分桶计数
如果数据分布比较均匀,可以按某个维度分桶,每个桶分别去重计数,最后再求和。比如按日期分区,每天分别做 COUNT(DISTINCT) 再累加,前提是桶间没有重复数据。
方案三:bitmap聚合 —— 极速精确去重(限低基数场景)
如果去重的列基数很低,比如性别、状态、类型这种只有几个值的字段,那 bitmap 就是神器。每个值对应一个 bit 位,多个值之间做 OR/AND 操作,快得飞起。
实现方式: 用 PostgreSQL 的 bitmap 扩展,或者 MySQL 的 SET 类型都可以。
适用场景: 标签系统、权限判断、漏斗分析里的“是否完成某动作”这类判断。
方案四:预计算/物化视图 —— 以空间换时间
如果去重的维度是固定的,比如每日 DAU,那就提前算好存下来,查询时直接读结果,省时省力。
实现方式:
- 每日定时任务,算好前一天的
COUNT(DISTINCT user_id),写入统计表 - 或者用物化视图(PostgreSQL 原生支持,MySQL 需要借助第三方工具)
| 方案 | 实时性 | 存储成本 | 适用场景 |
|---|---|---|---|
| 实时COUNT(DISTINCT) | 实时 | 低 | 小表或低频查询 |
| HyperLogLog | 实时 | 极低 | 可接受误差的高频查询 |
| 预计算表 | 非实时(T+1) | 中 | 固定报表、趋势图 |
| 物化视图 | 准实时(可刷新) | 中 | 综合报表 |
优化决策树
真实案例:某APP日活统计
- 数据量:每日约5000万独立设备ID
- 要求:实时展示当天DAU,可接受1%误差
- 方案:用 Redis HyperLogLog,每条日志
pfadd,实时pfcount - 结果:内存占用约12KB/天,响应时间 <10ms,误差 <1.5%
如果要求绝对精确,那就走 T+1 预计算:凌晨算好前一天精确的 COUNT(DISTINCT device_id) 存入 MySQL,白天查直接读结果。
总结
去重计数的优化,从来没有什么“银弹”。关键还是根据业务对精度、实时性和成本的要求,做出最合理的取舍。HyperLogLog 是误差容忍场景下的利器,bitmap 适合低基数,预计算适合固定报表。把这些方案都装进工具箱,你就能在“快”和“准”之间,找到那个最佳的平衡点。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。