大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!
上周我们讲了COUNT(*)在大表上的近似计数与HyperLogLog。这周继续聊COUNT的进阶话题------去重计数 。你一定遇到过这样的需求:"查一下昨天的独立访客数""统计这周活跃设备量",直接用COUNT(DISTINCT user_id),10亿行表跑了半小时还没出结果,怎么办?
去重计数的两种场景
| 场景 | 需求 | 可接受误差 |
|---|---|---|
| 运营报表、趋势图 | DAU、MAU | 1-2% |
| 财务、库存、对账 | 精确金额、订单数 | 0% |
不同场景对精度的要求完全不同。下面的优化手段,按误差从大到小排列。
方案一:近似去重(HyperLogLog)------ 要快,能接受1-2%误差
HyperLogLog是一种概率算法,用固定内存(约16KB)估算去重元素数量。原理:将每个元素哈希,统计哈希值二进制表示中前导零的最大长度,通过这个信息推断去重总数。
适用场景: UV、DAU、独立IP、搜索词去重统计等。
实现方式:
- Redis HyperLogLog(最常用)
ini
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扩展
sql
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。
sql
-- 确保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)更快(取决于优化器):
sql
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适合低基数,预计算适合固定报表。掌握了这些方案,你就能在"快"和"准"之间找到最佳平衡点。
小耶在手,SQL 不愁
还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽......我们下次见~