RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW 按真实时间滑动,ROWS 按行数滑动;RANGE 要求 ORDER BY 列为原生 TIMESTAMP/DATETIME 且无重复值,否则需先去重或打散。

滑动窗口怎么定义时间范围:ROWS vs RANGE 的关键区别
直接说结论:RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW 才能真正按「时间」滑动,ROWS 按行数滑动,和小时无关。很多同学用 ROWS BETWEEN 59 PRECEDING AND CURRENT ROW 试图模拟一小时,但数据不均匀时(比如某分钟没用户、某分钟突增 1000 个),结果完全失真。
PostgreSQL 和 MySQL 8.0+ 支持 RANGE 配合 INTERVAL,但必须满足两个前提:
-
ORDER BY列必须是TIMESTAMP或DATETIME类型(不能是INT时间戳) - 该列不能有重复值,否则
RANGE会把同秒/同毫秒的多行全算进来,导致窗口膨胀 - 如果存在重复时间,得先用
ROW_NUMBER() OVER (PARTITION BY event_time ORDER BY user_id)打散,再套外层窗口
PostgreSQL 实现示例:带去重的每小时活跃用户数
活跃用户指「在当前时刻往前推一小时内至少发生一次行为的用户」,不是「当前小时内的用户数」——这是常见误解。所以得用 COUNT(DISTINCT user_id) + 窗口,但窗口函数本身不支持 DISTINCT,必须换思路:
推荐做法:先按 (user_id, event_time) 去重(防刷),再对每个用户打上「该用户在本小时内最早出现的时间」,最后聚合。更实用的写法是用子查询 + LATERAL 或直接用非窗口解法;但如果坚持窗口,可这样迂回:
SELECT
event_time,
COUNT(DISTINCT user_id) OVER (
ORDER BY event_time
RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW
) AS active_users_1h
FROM (
SELECT DISTINCT user_id, event_time
FROM user_events
WHERE event_time >= NOW() - INTERVAL '24' HOUR
) dedup;注意:COUNT(DISTINCT ...) 在窗口中是合法的(PostgreSQL 14+,MySQL 8.0.22+),但性能较差。高并发场景建议改用近似算法 APPROX_COUNT_DISTINCT(user_id)(BigQuery / Spark SQL)或预计算布隆过滤器。
MySQL 8.0 的坑:RANGE 不支持 INTERVAL?其实是支持的,但语法要严丝合缝
MySQL 报错 ERROR 3586 (HY000): Window 'w' uses an unsupported frame specification,往往是因为:
-
ORDER BY列用了UNIX_TIMESTAMP(event_time)—— 必须用原生DATETIME,不能包装函数 -
INTERVAL写成INTERVAL 1 HOUR(缺引号)→ 正确是INTERVAL '1' HOUR - 字段名含下划线或大小写混用,又没加反引号,导致解析失败
一个能跑通的最小例子:
SELECT event_time, COUNT(DISTINCT user_id) OVER w AS active_users_1h FROM user_events WINDOW w AS ( ORDER BY event_time RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW );
替代方案:当窗口函数受限时,用自连接或生成时间序列
如果数据库版本太老(如 MySQL 5.7)、或需要兼容 Hive/Spark SQL 3.0 以下,RANGE + INTERVAL 不可用,就得绕开:
- 用
LEFT JOIN自连接:对每条记录,找t2.event_time >= t1.event_time - INTERVAL '1' HOUR AND t2.event_time 的所有行为,再 <code>GROUP BY t1.event_time+COUNT(DISTINCT t2.user_id)—— 数据量大时很慢 - 先生成每分钟/每小时的时间点序列(用
UNION ALL或递归 CTE),再用JOIN ... ON event_time BETWEEN slot_start AND slot_end关联,最后聚合 —— 更可控,也方便加维度(如按地区切分) - 业务层缓存最近一小时的
user_id集合(Redis Set),每次新事件到来时ZADD+EXPIRE,读取直接SCARD—— 实时性最好,但脱离 SQL 生态
真正难的不是写出来,而是确认「活跃」的语义是否包含跨天、是否容忍延迟、以及下游能否接受 5–10 秒的计算抖动 —— 这些比窗口语法影响更大。

















