在数据分析场景中,我们往往需要在同一份明细数据上同时得到累计求和、移动平均以及分组内的排名结果。SQL窗口函数提供了一套统一的语法,让我们不必依赖多层子查询或自连接,就能在保持原行粒度的同时算出这些衍生指标。

窗口函数的基础语法结构
窗口函数的核心在于over()子句,它定义了“函数作用在哪些行上”以及“这些行以什么顺序参与计算”。与聚合函数不同,窗口函数不会把多行压缩成一行,而是为每一行返回一个计算结果。最基本的写法是在聚合函数或排名函数后面加上over(partition by 列1 order by 列2)。
其中partition by负责把数据划分成多个独立的组,组与组之间的计算互不影响,相当于先按该列分组再在组内开窗口;order by则决定了窗口内行的排列顺序,累计求和和移动平均都强烈依赖这个顺序。如果省略partition by,则整张表被视为一个组。理解这两点是组合模式的前提。
累计求和的实现模式
累计求和通常使用sum(列) over(partition by 组 order by 排序)。在默认情况下,如果不写窗口范围,数据库会将该组从第一行到当前行作为窗口,这正好符合“累计”的语义。例如,按用户分组、按日期排序,计算每位用户截至当天的消费总额。
下面是一段可运行的 PostgreSQL 风格示例,展示如何在一个查询中同时得到每日金额与累计金额:
select
user_id,
order_date,
amount,
sum(amount) over (
partition by user_id
order by order_date
) as cum_amount
from user_orders
order by user_id, order_date;
这段代码里,cum_amount就是每个用户自己的累计消费。由于窗口定义在user_id分区内,不同用户的累计互不影响。如果需要按月份重置累计,只需把partition by改为user_id, date_trunc('month', order_date)即可,非常灵活。
移动平均的组合写法
移动平均与累计求和的区别在于窗口范围不是“从开头到当前”,而是“当前行附近的一个滑动区间”。这时必须显式使用rows between来约束范围。最常见的三天移动平均写法为avg(列) over(order by 时间 rows between 2 preceding and current row)。
我们将移动平均与累计求和管理解成同一函数的不同窗口边界设置。以下示例计算产品最近三天的平均销量,并附带累计销量:
select
product_id,
sale_date,
qty,
sum(qty) over (
partition by product_id
order by sale_date
) as cum_qty,
avg(qty) over (
partition by product_id
order by sale_date
rows between 2 preceding and current row
) as mov_avg_3
from sales_detail
order by product_id, sale_date;
注意rows between 2 preceding and current row表示包含当前行及之前两行,共三行求平均。若数据不足三行,数据库会自动按实际行数计算,不会报错。这种写法比自连接效率更高,因为排序只需一次。
分组内排名的嵌套使用
排名类函数如row_number()、rank()、dense_rank()同样依托over(partition by ... order by ...)。它们常用于取出每组前 N 条记录,或标记组内位次。排名函数与累计、移动平均可以共存于同一个 select 列表,因为它们只是各自定义自己的窗口。
假设我们想找出每个用户消费最高的三笔订单,并展示其累计金额与三天移动平均,可以这么写:
select *
from (
select
user_id,
order_date,
amount,
sum(amount) over (
partition by user_id
order by order_date
) as cum_amount,
avg(amount) over (
partition by user_id
order by order_date
rows between 2 preceding and current row
) as mov_avg_3,
row_number() over (
partition by user_id
order by amount desc
) as rn
from user_orders
) t
where rn <= 3
order by user_id, rn;
这里内层查询同时计算了三种指标,外层再用rn <= 3过滤出每组前三。可以看到,分组内排名的order by amount desc与累计求和的order by order_date并不冲突,因为它们分属不同窗口定义。这种组合模式在榜单类和时序类报表中极其常见。
性能与注意事项
虽然窗口函数写法简洁,但仍需注意索引配合。如果partition by和order by的列上有复合索引,数据库可以避免额外排序,显著提升性能。另外,移动平均的rows between不要用成range between,后者按值区间算边界,在重复时间点下会得到意料之外的行数。
最后提醒,在 MySQL 8.0 之前版本不支持窗口函数,需改用变量模拟;而主流的 PostgreSQL、SQL Server、Oracle 均已完整支持上述语法。掌握这些组合模式,能让你用一条 SQL 替代原本几十行的过程代码。