窗口函数中ROWS BETWEEN 29 PRECEDING AND CURRENT ROW按行数而非自然日滚动,无法准确统计过去30天去重用户数;PostgreSQL等支持RANGE BETWEEN INTERVAL '29 days' PRECEDING AND CURRENT ROW,但需先按天去重聚合且event_date为DATE类型;MySQL 8.0+需用LATERAL或自连接模拟。

窗口函数不能直接计算“过去30天”滚动范围
直接用 ROWS BETWEEN 29 PRECEDING AND CURRENT ROW 算的是按行偏移的30行,不是按日期滚动的30天。如果某天没数据,就会漏掉;如果某天有多条记录,又会重复计数。真正要的是「截止当前行日期,往前推30个自然日」的去重用户数,这必须结合 RANGE 或自连接/子查询实现,而多数数据库(如 PostgreSQL、Snowflake)支持 RANGE BETWEEN INTERVAL '29 days' PRECEDING AND CURRENT ROW,但 MySQL 8.0+ 才开始有限支持,且要求排序列是 DATE 或 DATETIME 类型且无重复值(否则 RANGE 会合并相同值的行,导致统计不准)。
PostgreSQL 中用 RANGE + INTERVAL 最简洁
前提是你的活跃事件表有明确的 event_date(DATE 类型)和 user_id,且每天每个用户可能有多次行为。你需要先按天聚合去重用户,再开窗:
SELECT
event_date,
COUNT(DISTINCT user_id) AS daily_active_users,
COUNT(DISTINCT user_id) OVER (
ORDER BY event_date
RANGE BETWEEN INTERVAL '29 days' PRECEDING AND CURRENT ROW
) AS active_users_last_30d
FROM (
SELECT DISTINCT event_date::DATE, user_id
FROM user_events
WHERE event_date >= CURRENT_DATE - INTERVAL '30 days'
) d
GROUP BY event_date
ORDER BY event_date;注意三点:
• 必须先 DISTINCT 去重,否则同一天多个事件会让 COUNT(DISTINCT) 在窗口内失效;
• RANGE 要求 ORDER BY 列是单调递增且类型匹配(DATE 对 INTERVAL);
• 如果 event_date 是 TIMESTAMP,得显式转成 DATE,否则 RANGE 可能不生效或报错。
MySQL 8.0+ 替代方案:用 LATERAL 或相关子查询
MySQL 不支持 RANGE 配合 INTERVAL,也不能在窗口函数里写子查询。稳妥做法是用 LATERAL(8.0.14+)或自连接模拟滚动窗口:
SELECT
t1.event_date,
(SELECT COUNT(DISTINCT t2.user_id)
FROM user_events t2
WHERE t2.event_date >= t1.event_date - INTERVAL 29 DAY
AND t2.event_date <= t1.event_date) AS active_users_last_30d
FROM (SELECT DISTINCT event_date::DATE AS event_date FROM user_events) t1
ORDER BY t1.event_date;实际执行前建议:
• 给 user_events(event_date, user_id) 建联合索引,否则性能极差;
• 如果数据量大,先用物化临时表预聚合每日去重用户数,再对这张小表做自连接;
• 注意 MySQL 的 DATE 计算不自动截断时间部分,若原始字段是 DATETIME,务必用 DATE(event_date) 对齐。
常见错误:COUNT(DISTINCT) 在窗口中被忽略或报错
很多工程师尝试写 COUNT(DISTINCT user_id) OVER (ORDER BY event_date ROWS ...),结果发现数值不变或报错。这是因为:
• 大多数数据库(包括 PostgreSQL、SQL Server)**不支持** COUNT(DISTINCT) 直接用于窗口函数(会报 “DISTINCT is not supported for window functions” 类错误);
• 即使支持(如某些版本的 BigQuery),其语义也不是你想要的“滚动去重”,而是对整个窗口内所有行去重——这跟按天聚合后再滚动完全不同;
• 替代思路只有两个:要么先聚合(推荐),要么用数组/字符串拼接 + 去重长度(低效且有长度限制)。
真正难的不是语法,是意识到「滚动时间窗口 + 去重计数」本质上是个两阶段问题:先按时间粒度归一化,再做范围聚合。跳过第一阶段,后面怎么调窗口参数都白搭。

















