rows子句是定义窗口函数物理行边界的关键词,实现严格按行号切片的可控滑动窗口;它与滑动窗口的关系在于:rows提供基于行序的精确范围控制,而range则按值范围聚合,易在等值时间戳场景下失控。

什么是 ROWS 子句,它和滑动窗口有什么关系
ROWS 是定义窗口函数物理边界的关键子句,它告诉数据库“当前行的统计范围在结果集里占多少行”,而不是按时间或值大小来框定。没有 ROWS(或 RANGE),像 SUM() OVER (ORDER BY ts) 默认是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,这在等值时间戳场景下会意外聚合多行——而 ROWS 能严格按行号切片,这才是真正可控的滑动窗口。
常见错误现象:SUM(val) OVER (ORDER BY ts ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) 写成 RANGE,结果发现某秒有10条数据,窗口突然吞掉10行而非3行;或者漏写 ORDER BY,导致 ROWS 失效(PostgreSQL/MySQL 8.0+ 会报错,SQLite 则静默退化为无序聚合)。
ROWS BETWEEN ... AND ... 的合法组合与行为差异
窗口帧的起点和终点必须满足“起点 ≤ 当前行 ≤ 终点”,否则该行的窗口为空(聚合结果为 NULL)。不同数据库对非法偏移的容忍度不同:
-
ROWS BETWEEN 3 PRECEDING AND 1 PRECEDING:合法,但窗口不含当前行,常用于“前3行的均值”这类错位计算 -
ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING:包含当前行及后两行,适合预测类指标(如“未来3天销量总和”) -
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING:整分区全量,等价于不加ROWS但强制按行序——注意这会失去RANGE的值敏感性 - 错误用法:
ROWS BETWEEN 5 FOLLOWING AND 2 PRECEDING(起点 > 终点),PostgreSQL 报错,MySQL 8.0+ 静默忽略整条窗口函数
实战中容易被忽略的排序依赖和 NULL 处理
滑动窗口的行序完全由 ORDER BY 子句决定,不是原始插入顺序。如果 ORDER BY 字段含 NULL,不同数据库默认处理不同:PostgreSQL 把 NULL 排最前,MySQL 8.0+ 排最后,SQLite 默认排最前——这会导致同一语句在不同库结果不一致。
示例场景:按用户点击流统计最近5次点击的平均停留时长,字段 user_id, event_time, dwell_ms:
SELECT
user_id,
event_time,
AVG(dwell_ms) OVER (
PARTITION BY user_id
ORDER BY event_time
ROWS BETWEEN 4 PRECEDING AND CURRENT ROW
) AS avg_dwell_5
FROM clicks;
关键点:
- 必须
PARTITION BY user_id,否则跨用户混算 - 若某用户只有2条记录,则第1行窗口为
ROWS BETWEEN -3 PRECEDING AND CURRENT ROW→ 实际取UNBOUNDED PRECEDING到当前行,即前2行;第2行同理,不会报错 - 如果
dwell_ms为NULL,AVG()自动跳过(标准 SQL 行为),但COUNT(*)仍会计数——要算“非空样本数”得用COUNT(dwell_ms)
性能陷阱:ORDER BY + ROWS 在大数据量下的真实开销
ROWS 窗口本身不增加复杂度阶数,但 ORDER BY 是瓶颈。当分区大(如单个 user_id 有百万行)、且 ORDER BY 字段无索引时,MySQL 8.0+ 和 PostgreSQL 都会触发临时文件排序,I/O 暴涨。
可优化点:
- 确保
PARTITION BY和ORDER BY字段联合有索引,例如(user_id, event_time) - 避免在
ORDER BY中用函数,如ORDER BY DATE(event_time)会让索引失效 - SQLite 不支持窗口函数的索引加速,大数据量慎用
ROWS窗口 - 如果只是求固定长度滑动平均,且数据已按时间排序入库,考虑用应用层游标分页 + 环形缓冲区,比纯 SQL 更快
最易被忽略的一点:ROWS 定义的是“物理行偏移”,不是“业务时间偏移”。想按“过去7天”统计,不能靠 ROWS BETWEEN 6 PRECEDING AND CURRENT ROW —— 数据稀疏时会漏,密集时会超,必须用 RANGE BETWEEN INTERVAL '6 days' PRECEDING AND CURRENT ROW(PostgreSQL/MySQL 8.0.2+ 支持),或手动关联时间条件。










