在数据分析里,滑动窗口聚合是个高频操作,但也是最容易踩坑的地方之一。很多人写出来的窗口函数,乍一看语法都对,跑出来的结果却和业务直觉对不上。今天,我们就来聊聊其中最核心也最微妙的一个概念:ROWS BETWEEN。

ROWS BETWEEN 是什么,它和 RANGE BETWEEN 有什么本质区别
这二者的区别,是理解窗口函数的关键。简单来说,ROWS BETWEEN 数的是“行”,它严格按照物理行位置来划定窗口边界。而 RANGE BETWEEN 看的是“值”,它根据排序键的值来分组,相同值的行会被视为一个整体。
举个例子就明白了。假设你有一张销售表,按 order_date 排序,结果集里可能有十几行记录的日期都是同一天。这时,如果你用 RANGE BETWEEN 1 PRECEDING AND CURRENT ROW 来计算“最近两天均值”,那么所有与当前行日期相同或为前一天的记录,都会被纳入计算,窗口大小可能瞬间膨胀到几十行。但如果你用 ROWS BETWEEN 1 PRECEDING AND CURRENT ROW,它就只认物理上紧挨着的前一行,窗口大小严格可控,永远是两行。
所以,一个常见的错误现象就是:当你用 A VG(sales) OVER (ORDER BY order_date RANGE BETWEEN 1 PRECEDING AND CURRENT ROW) 想算“最近两天”的均值时,在日期密集的区域,结果会剧烈跳变。因为你算的其实是“和当前日期相同或前一天的所有订单均值”,这和你想要的“时间序列上的滑动平均”根本不是一回事。
怎么写一个可靠的“过去7天销售额滚动平均”
说到滑动平均,最经典的需求莫过于“过去7天销售额的滚动平均”。这里有个关键点:时间维度不能直接用 ORDER BY date_col 配合 ROWS。为什么?因为你的原始数据里很可能有日期缺失(比如周末没销售),直接用 ROWS BETWEEN 6 PRECEDING 会错位,导致你取到的根本不是“过去7天”,而是“过去7行”。
正确的做法,得先“补全”日期。你需要一个连续的日期序列作为骨架:
- 在 PostgreSQL 里,可以用
GENERATE_SERIES函数轻松生成。 - 在 MySQL 8.0+ 或 SQL Server 里,可以用递归 CTE 来实现。
然后,用这个连续日期序列去左连接(LEFT JOIN)你的原始销售表,把缺失日期的销售额用 COALESCE(sales, 0) 补为0。最后,在这个“日期连续、数据完整”的结果集上,套用窗口函数:A VG(sales) OVER (ORDER BY dt ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)。
下面是一个 PostgreSQL 的示例,思路很清晰:
SELECT dt,
sales,
ROUND(A VG(sales) OVER (ORDER BY dt ROWS BETWEEN 6 PRECEDING AND CURRENT ROW), 2) AS a vg_7d
FROM (
SELECT d.dt, COALESCE(t.sales, 0) AS sales
FROM GENERATE_SERIES('2024-01-01'::DATE, '2024-01-31'::DATE, '1 day') AS d(dt)
LEFT JOIN sales_table t ON d.dt = t.sale_date
) AS filled;
ROWS BETWEEN 的边界参数哪些能省略,哪些绝对不能省
写窗口帧的时候,边界参数怎么省是个学问。CURRENT ROW 在某些情况下可以省略,比如 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 可以简写成 ROWS UNBOUNDED PRECEDING。但核心原则是:起始和结束边界必须成对出现,语义才完整。
有几个容易踩的坑:
ROWS BETWEEN 2 PRECEDING—— 缺少AND ...来指定结束边界,语法直接报错。ROWS BETWEEN 1 FOLLOWING AND 1 PRECEDING—— 起始边界在结束边界之后,逻辑矛盾。PostgreSQL 会报错,而 MySQL 可能会静默地返回空值,更隐蔽。ROWS BETWEEN 5 PRECEDING AND 5 FOLLOWING—— 这种“前后各N行”的写法在首尾几行会自动截断,实际参与计算的行数会少于11行,这是符合预期的,但心里要有数。
性能影响:ROWS 越宽,执行计划越容易崩
最后,我们来谈谈性能。窗口帧的宽度,直接决定了内存占用和排序开销。当你写下 ROWS BETWEEN 10000 PRECEDING AND CURRENT ROW 这样的语句,面对百万级的结果集时,数据库引擎的压力是巨大的。PostgreSQL 可能会因为 work_mem 不足而被迫降级到磁盘排序,速度骤降;SQL Server 甚至可能直接拒绝执行,抛出一个“查询处理器无法生成查询计划”的错误。
这里有几个优化建议:
- 避免无意义的大数字:如果你本意是想取“所有前面的行”,那就直接用
UNBOUNDED PRECEDING,别写一个巨大的具体数字,这会让优化器很难做。 - 利用索引:在
ORDER BY的字段上建立索引,能让窗口计算复用排序结果,大幅降低 CPU 和内存的压力。 - 考虑替代方案:如果只是求一个简单的累计值(比如 running sum),并且数据本身就是按时间顺序入库的,有时候用应用层的流式累加来代替数据库的窗口函数,可能是更轻量、更高效的选择。
说到底,ROWS BETWEEN 的语法本身并不复杂。真正的挑战,是当它遇上稀疏的时间序列、超高基数的分组、或者超长的滑动跨度时,如何确保结果既正确无误,又能高效执行。这时候,与其盲目调大数据库的内存参数,不如回过头,好好审视一下你的数据分布和物化策略。这才是解决问题的根本。
