七日滚动活跃用户指对任意一天dt统计dt-6至dt共7天内至少登录一次的用户数,需基于时间范围去重计数,不能用简单group by或rows窗口,因存在日期缺失、跨天重复、引擎兼容性及count(distinct)不支持窗口等问题。

什么是七日滚动活跃用户,为什么不能用 GROUP BY
七日滚动活跃用户指:对任意一天 dt,统计从 dt - 6 到 dt 这连续 7 天内至少登录过一次的用户数。它不是固定周期(比如每周一到周日),而是每天都要往前滑动 7 天窗口重新计算。
直接 GROUP BY dt 算每日 UV 再用自连接或子查询“手动”拉前 7 行,逻辑易错、性能差,且难以处理用户跨多天重复出现的问题——你需要的是去重计数,不是简单求和。
用 ROWS BETWEEN 6 PRECEDING AND CURRENT ROW 会出错
这是最常见的误解:以为只要写 ROWS BETWEEN 6 PRECEDING AND CURRENT ROW 就能拿到 7 天数据。但这个语法只适用于已按日期排序后的行序列,它统计的是「当前行及前面 6 行」,不是「当前日期往前推 7 天」。如果某天没数据(比如 2024-05-10 没用户登录),那这天在结果里就直接消失,窗口会跳过它,导致实际覆盖天数不足 7 天。
正确做法必须基于时间范围(RANGE BETWEEN INTERVAL '6' DAY PRECEDING AND CURRENT ROW),但注意:只有 PostgreSQL 和某些新版 MySQL(8.0.2+)支持 RANGE + INTERVAL;MySQL 5.7 和旧版不支持,SQL Server 也不支持该写法。
- PostgreSQL 可用:
RANGE BETWEEN INTERVAL '6 days' PRECEDING AND CURRENT ROW - MySQL 8.0.2+ 可用:
RANGE BETWEEN INTERVAL 6 DAY PRECEDING AND CURRENT ROW - Spark SQL / BigQuery 支持类似语法,但需确认版本
- SQL Server 必须改用
LEFT JOIN或 CTE 构造日期维度表
兼容性最强的写法:先生成完整日期序列,再 LEFT JOIN 用户行为
绕过窗口函数限制的稳妥方案是显式构造 7 天窗口。核心思路是:对每个目标日期 dt,生成其对应的 [dt-6, dt] 区间,再关联用户行为表,最后去重计数。
以 PostgreSQL 为例(其他引擎只需微调日期函数):
SELECT d.dt, COUNT(DISTINCT u.user_id) AS active_users_7d FROM ( SELECT DISTINCT dt FROM events ) d CROSS JOIN LATERAL ( SELECT user_id FROM events e WHERE e.dt BETWEEN d.dt - INTERVAL '6 days' AND d.dt ) u GROUP BY d.dt ORDER BY d.dt;
关键点:
-
CROSS JOIN LATERAL是 PostgreSQL/MySQL 8.0+ 的能力,让子查询能引用外层d.dt;MySQL 5.7 需改用JOIN+ 子查询或临时表 - 如果原始表
events缺少某些日期(如节假日无数据),上面写法仍会漏掉这些dt—— 此时应先用generate_series()(PG)或递归 CTE(MySQL/SQL Server)补全日期维度 -
COUNT(DISTINCT user_id)在大数据量下可能慢,可考虑预聚合用户日志(如先算每日 UV 表,再滚动合并)
性能陷阱:COUNT(DISTINCT) 在窗口里不能直接用
有人试图写 COUNT(DISTINCT user_id) OVER (RANGE ...),这是无效语法 —— 标准 SQL 不允许 DISTINCT 出现在窗口函数的聚合中(PostgreSQL 报错 ERROR: DISTINCT is not supported for window functions,MySQL 直接语法错误)。
所以必须把去重逻辑放在聚合层(GROUP BY)或用子查询/CTE 提前摊平数据。没有捷径,也不能靠加索引绕过。
真正影响性能的不是窗口本身,而是「对每一天都扫描一遍 7 天数据」带来的重复 I/O。如果日活百万级,建议先按 (dt, user_id) 建联合索引,并考虑物化中间结果(例如每日跑一个 daily_uv 表,再基于它滚动计算)。
滚动窗口的本质是时间维度上的“滑动去重”,数据库原生支持有限,得根据引擎能力选路径——别迷信一个 SQL 解决所有问题。










