惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

A
About on SuperTechFans
有赞技术团队
有赞技术团队
人人都是产品经理
人人都是产品经理
月光博客
月光博客
美团技术团队
博客园 - 聂微东
阮一峰的网络日志
阮一峰的网络日志
WordPress大学
WordPress大学
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园_首页
爱范儿
爱范儿
G
Google Developers Blog
aimingoo的专栏
aimingoo的专栏
T
The Blog of Author Tim Ferriss
MongoDB | Blog
MongoDB | Blog
小众软件
小众软件
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
IT之家
IT之家
I
InfoQ
B
Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
大猫的无限游戏
大猫的无限游戏
T
Tailwind CSS Blog
F
Fortinet All Blogs

Aimee's Blog

秒杀怎么防机器人:验证码之外的几道防线 防薅羊毛怎么做:号码风控、设备指纹与分层策略 接口限流怎么选:固定窗口、滑动窗口、令牌桶 批量打包下载怎么设计:流式 ZIP 与异步任务 图片处理怎么做:异步处理与 CDN 实时参数 大文件上传怎么设计:分片、断点续传、秒传 报表可重跑的设计:快照、水位线与幂等 大数据量导出怎么做:流式写入与异步任务 排行榜的两类需求,难度差了一个数量级 内容审核怎么选:先审后发,还是先发后审 实时热榜怎么设计:Redis ZSet 与热度分值 互关状态怎么保持同步:双向关系的维护与缓存失效 并发点赞的计数设计:原子操作与最终对账 删了父评论,子评论怎么处理——先想清楚用户看到的是什么 定时消息任务触发了两次 消息量大,写扩散还是读扩散——接到这个需求先问清楚规模 同一条消息推送了三次 活动结束了,用户还在收短信 接到注销需求,先问两个问题 微服务与服务拆分:何时拆、怎么拆 异步与事件驱动架构:把协作从「打电话」改成「发消息」 高可用设计:怎么让系统尽量不宕机 可扩展性设计:怎么让系统加机器就能扛更多 缓存架构:多级缓存怎么搭 高并发三板斧:限流、熔断、降级 架构设计到底在设计什么 —— 从单体到微服务的演进 服务成本账:一个服务一个月烧多少钱 可观测性:线上出问题怎么查 API 设计:好接口长什么样 消息队列:为什么要 MQ,以及丢失、重复、顺序怎么破
UV 统计为什么有误差:精确计数与近似计数怎么选
Aimee · 2026-08-12 · via Aimee's Blog

UV 统计为什么有误差:精确计数与近似计数怎么选

「实时看板的 UV 和数仓算出来的对不上」——这是数据统计里最高频的疑问之一。

这不一定是计算错了,而是 UV 本来就分两种做法,精度不同。


PV 和 UV 的区别

PV(Page View):页面访问次数,同一用户多次访问都算,累加即可。

UV(Unique Visitor):独立访客数,同一用户多次访问只算一次。PV 可以直接加,UV 不行——UV 的本质是去重计数。

去重计数的两条路:精确计数和近似计数。


精确计数:COUNT(DISTINCT user_id)

存每次访问记录,查询时用 COUNT(DISTINCT)

SELECT COUNT(DISTINCT user_id) FROM page_view
WHERE page = 'home' AND date = '2024-01-01'

数据量小时没问题。日访问量百万级、统计周期跨几个月,这条 SQL 会扫描几亿行,可能跑几十秒。

用 Redis Set 存今天访问过的用户 ID:

redis.sadd("uv:home:2024-01-01", String.valueOf(userId));
long uv = redis.scard("uv:home:2024-01-01");

1000 万用户,Set 占 100 MB 内存,还能接受——但如果是全站统计、按页面打散,key 数量很多,内存很快撑不住。


近似计数:HyperLogLog

HyperLogLog 是 Redis 内置的近似基数统计算法,误差率约 0.81%,但内存固定只占 12 KB——不管统计多少用户,都是这么大。

// 用户访问时
redis.pfadd("hll:uv:home:2024-01-01", String.valueOf(userId));

// 查询 UV
long uv = redis.pfcount("hll:uv:home:2024-01-01");

// 合并多天的 UV(去重合并)
redis.pfmerge("hll:uv:home:week", 
    "hll:uv:home:2024-01-01", 
    "hll:uv:home:2024-01-02", ...);

PFMERGE 可以把多个 HyperLogLog 合并成一个,合并后查 PFCOUNT 就是多天去重后的 UV——这是 Set 做不到的(合并两个 Set 的内存是两个 Set 之和)。


布隆过滤器:判断"有没有来过"

另一个场景:判断某个用户今天是不是首次访问,用来做"今日新增用户"统计。

用 Redis Set 判断:SISMEMBER,精确,但内存线性增长。

用 Bloom Filter:

// 访问时:已经存在 → 今天来过,不算新增
//         不存在 → 第一次,算新增,然后 add 进去
if (!bloomFilter.mightContain(userId)) {
    bloomFilter.put(userId);
    newUserCount.incrementAndGet();
}

布隆过滤器的特性:不存在一定不存在,存在可能误判(一定存在的会说可能存在)。用于"首次访问"判断,误判意味着少算了几个新增用户,可以接受;如果误判代价很高(比如重复给用户发优惠券),就不能用。


选哪个

场景推荐方案
日 UV,用户量 < 100 万Redis Set,精确
日 UV,用户量 > 100 万HyperLogLog,近似
跨天/多页面聚合 UVHyperLogLog(PFMERGE 去重合并)
判断今日是否首次访问Bloom Filter
需要精确数字(法务/财务)数仓离线计算(COUNT DISTINCT)

精确计数和近似计数不是好坏之分,是速度和内存的取舍。产品需要实时看数但允许小误差,用 HyperLogLog;月底出财务报表必须精确,走数仓离线跑。