在SQL数据处理的实际工作中,分组内数据排序聚合是非常常见的需求,比如统计每个部门内员工的薪资排名、计算每个班级内学生的成绩累计和等。传统实现方式往往需要多次嵌套子查询,逻辑繁琐且容易出错,而窗口函数的出现很好地解决了这个问题。

传统分组排序聚合的实现痛点
在没有窗口函数的情况下,要实现分组内排序聚合,通常需要先对数据进行分组排序,再嵌套一层查询做聚合计算。比如要统计每个部门员工薪资从高到低的排名,传统写法如下:
-- 传统写法,先排序再嵌套计算排名
SELECT
dept_id,
emp_name,
salary,
rank_num
FROM (
SELECT
dept_id,
emp_name,
salary,
@rank := IF(@prev_dept = dept_id, @rank + 1, 1) AS rank_num,
@prev_dept := dept_id
FROM emp_salary,
(SELECT @rank := 0, @prev_dept := NULL) t
ORDER BY dept_id, salary DESC
) t1;
这种写法需要借助用户变量来记录分组和排名状态,逻辑晦涩,而且如果还要同时计算分组内的累计薪资、平均薪资等聚合值,嵌套层级会更多,不仅可读性差,执行时也会产生更多的临时表,影响查询效率。
窗口函数的基本用法
窗口函数可以在不改变原表行数的情况下,对指定的窗口范围进行计算,基本语法为:函数名() OVER (PARTITION BY 分组字段 ORDER BY 排序字段 [窗口范围])。常用的排序类窗口函数有ROW_NUMBER()、RANK()、DENSE_RANK(),聚合类窗口函数有SUM()、AVG()、COUNT()等。
比如用ROW_NUMBER()实现上述部门员工薪资排名,写法会非常简洁:
-- 窗口函数实现分组内排序排名
SELECT
dept_id,
emp_name,
salary,
ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rank_num
FROM emp_salary;
这里PARTITION BY dept_id表示按部门分组,ORDER BY salary DESC表示每个分组内按薪资降序排序,ROW_NUMBER()会为每个分组内的行生成连续的序号,完全不需要额外的变量和嵌套子查询。
窗口函数实现分组内排序聚合
除了排序排名,窗口函数还能很方便地实现分组内的聚合计算,比如计算每个部门内员工的薪资累计和、分组内最高薪资等,不需要GROUP BY子句,也不会减少原表的行数。
示例:统计每个部门员工的薪资,同时计算部门内薪资累计和、部门最高薪资:
SELECT
dept_id,
emp_name,
salary,
-- 分组内按薪资降序排序后的累计薪资
SUM(salary) OVER (
PARTITION BY dept_id
ORDER BY salary DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cum_salary,
-- 部门内最高薪资
MAX(salary) OVER (PARTITION BY dept_id) AS dept_max_salary
FROM emp_salary
ORDER BY dept_id, salary DESC;
上述代码中,SUM()作为窗口函数使用,指定了窗口范围是每个分组内从第一行到当前行,就能直接得到累计和;MAX()窗口函数只按部门分组不排序,就能得到每个部门的薪资最大值,逻辑清晰且执行效率高。
窗口函数的逻辑优化思路
使用窗口函数优化分组内排序聚合逻辑时,有几个点可以进一步提升效率:
- 尽量减少不必要的排序:如果只需要聚合值不需要排序,可以省略ORDER BY子句,避免额外的排序开销。
- 合理指定窗口范围:如果只需要计算分组内的总和,不需要累计值,就不要指定ROWS子句,默认窗口是整个分组,减少计算范围。
- 避免重复开窗:如果多个计算需要相同的分组和排序规则,可以复用同一个窗口定义,比如先定义WINDOW w AS (PARTITION BY dept_id ORDER BY salary DESC),后续函数直接OVER w即可。
示例:复用窗口定义简化代码:
SELECT
dept_id,
emp_name,
salary,
ROW_NUMBER() OVER w AS rank_num,
SUM(salary) OVER w AS cum_salary
FROM emp_salary
WINDOW w AS (PARTITION BY dept_id ORDER BY salary DESC)
ORDER BY dept_id, salary DESC;
注意事项
需要注意的是,窗口函数只能出现在SELECT子句和ORDER BY子句中,不能用于WHERE、GROUP BY、HAVING子句,因为窗口函数的执行顺序在这些子句之后。另外,不同数据库对窗口函数的支持略有差异,MySQL 8.0+、PostgreSQL、SQL Server、Oracle等主流数据库都已经完整支持窗口函数,使用时可以确认对应数据库的版本兼容性。