SQL中的窗口函数能够在保留原表所有行的基础上,按照指定的规则对数据进行分组统计计算,完美适配复杂分组统计需求,避免了多次关联子查询带来的性能损耗和代码复杂度。它和普通的聚合函数不同,不会将多行数据合并成一行,而是为每一行数据返回对应的统计结果。

窗口函数基本语法
窗口函数的标准语法结构如下,其中OVER()子句是核心,用于定义统计的窗口范围:
-- 基本语法结构
函数名([参数]) OVER (
[PARTITION BY 分组列1, 分组列2...]
[ORDER BY 排序列1 [ASC|DESC], 排序列2 [ASC|DESC]...]
[窗口帧定义]
)
语法的几个核心部分说明:
- PARTITION BY:可选,用于指定分组的列,作用类似GROUP BY,但是不会合并行,只是把相同值的行划分到同一个窗口内
- ORDER BY:可选,用于指定窗口内数据的排序规则,很多窗口函数的计算结果依赖排序
- 窗口帧定义:可选,用于指定窗口内的数据范围,比如从第一行到当前行,默认是分区内的所有行
常见窗口函数分类
窗口函数主要分为排名函数、聚合窗口函数、取值窗口函数三类,适配不同的统计场景:
| 分类 | 常见函数 | 功能说明 |
|---|---|---|
| 排名函数 | ROW_NUMBER、RANK、DENSE_RANK | 计算数据在分组内的排名 |
| 聚合窗口函数 | SUM、AVG、COUNT、MAX、MIN | 在窗口内完成聚合计算,返回每行对应的聚合结果 |
| 取值窗口函数 | LAG、LEAD、FIRST_VALUE、LAST_VALUE | 获取窗口内前后行或者首尾行的数据 |
全场景示例解析
场景1:分组排名统计
需求:统计每个部门内员工的薪资排名,相同薪资排名规则为并列排名且后续排名不跳跃。
这里使用DENSE_RANK函数,相同薪资会得到相同排名,下一个排名紧接着上一个排名,不会跳过序号:
-- 员工表结构示例:employee(id, name, dept, salary)
SELECT
id,
name,
dept,
salary,
-- 按部门分区,按薪资降序排名
DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS dept_salary_rank
FROM employee;
场景2:分组累计求和
需求:统计每个部门内员工薪资从高到低的累计薪资,方便查看薪资分布。
使用SUM作为窗口函数,配合窗口帧定义ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,表示从分区第一行到当前行的范围做求和:
SELECT
id,
name,
dept,
salary,
-- 按部门分区,按薪资降序排序,累计求和到当前行
SUM(salary) OVER (
PARTITION BY dept
ORDER BY salary DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cum_sum_salary
FROM employee;
场景3:组内占比统计
需求:计算每个员工的薪资占所在部门总薪资的比例。
可以用两次窗口函数,一次计算部门总薪资,一次计算当前行薪资的占比:
SELECT
id,
name,
dept,
salary,
-- 计算部门总薪资
SUM(salary) OVER (PARTITION BY dept) AS dept_total_salary,
-- 计算占比,保留两位小数
ROUND(salary * 1.0 / SUM(salary) OVER (PARTITION BY dept), 2) AS salary_ratio
FROM employee;
场景4:移动平均计算
需求:统计每个员工前2行到后2行的平均薪资,用于平滑薪资波动分析。
通过窗口帧定义ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING指定前后各2行的范围:
SELECT
id,
name,
dept,
salary,
-- 计算前2行到后2行的移动平均薪资
AVG(salary) OVER (
PARTITION BY dept
ORDER BY id
ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING
) AS moving_avg_salary
FROM employee;
场景5:分组内前后值取值
需求:获取每个员工的上一个入职员工的姓名,假设员工表有hire_date入职日期字段。
使用LAG函数获取当前行之前指定偏移量的数据:
SELECT
id,
name,
dept,
hire_date,
-- 获取同部门内上一个入职的员工姓名,没有则返回无
LAG(name, 1, '无') OVER (PARTITION BY dept ORDER BY hire_date) AS prev_emp_name
FROM employee;
使用注意事项
- 窗口函数只能出现在SELECT子句和ORDER BY子句中,不能用于WHERE、GROUP BY、HAVING子句
- 如果OVER子句中没有PARTITION BY,那么整个表会作为一个大的窗口进行统计
- 不同的数据库对窗口函数的支持略有差异,比如MySQL从8.0版本开始支持窗口函数,使用前需要确认数据库版本
- 窗口帧定义如果不指定,默认是
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,和ROWS模式的区别在于处理相同排序值的行时范围不同