SQL基础-开窗函数
最近在复习 SQL 的开窗函数(Window Functions),这是 SQL 中非常强大的高级功能,能在保留每一行原始数据的同时进行分组计算,在数据分析场景中几乎无处不在。排名、累计求和、环比计算等需求都离不开它。内容不少,整理成一份速查手册方便日后回顾。
本文使用以下示例表来演示所有查询:
1 | -- 员工表 |
💡 表说明:
employee表:8 名员工分布在 3 个部门,用于演示排名、分组计算等monthly_sales表:3 位销售人员 4 个月的业绩记录,用于演示累计求和、环比分析等
一、开窗函数概述
1. 什么是开窗函数
开窗函数(Window Functions)可以对结果集的一个子集(称为”窗口“)进行计算,不需要 GROUP BY 就能做聚合计算,而且不会减少行数。每一行都能”看到”自己所在窗口内的其他行,并基于它们进行计算。
⚠️ 版本要求:开窗函数需要 MySQL 8.0+ 才支持。
2. ⭐ 与 GROUP BY 聚合函数的区别
这是理解开窗函数最关键的一点:
| 对比维度 | GROUP BY + 聚合函数 | 开窗函数 |
|---|---|---|
| 行数变化 | 多行合并为一行 | ✅ 保留所有原始行 |
| 原始数据 | 聚合后看不到明细 | ✅ 聚合结果和明细并存 |
| 使用位置 | 需要 GROUP BY 子句 | 使用 OVER() 子句 |
1 | -- GROUP BY:按部门求平均工资 → 只有 3 行 |
| department | avg_salary |
|---|---|
| 技术部 | 18333.33 |
| 销售部 | 12000.00 |
| 人事部 | 12000.00 |
1 | -- 开窗函数:每个员工都保留,同时附上部门平均工资 → 仍然 8 行 |
| name | department | salary | dept_avg |
|---|---|---|---|
| 张三 | 技术部 | 15000 | 18333.33 |
| 李四 | 技术部 | 18000 | 18333.33 |
| 周八 | 技术部 | 22000 | 18333.33 |
| 王五 | 销售部 | 12000 | 12000.00 |
| 赵六 | 销售部 | 9000 | 12000.00 |
| 吴九 | 销售部 | 15000 | 12000.00 |
| 孙七 | 人事部 | 11000 | 12000.00 |
| 郑十 | 人事部 | 13000 | 12000.00 |
⭐ 核心区别:GROUP BY 把多行”压缩”成一行,开窗函数在每行”旁边”附加聚合结果。
3. 基本语法
1 | 窗口函数() OVER ( |
常见的开窗函数类型:
| 类型 | 函数 | 说明 |
|---|---|---|
| 聚合函数 | SUM、AVG、COUNT、MAX、MIN |
配合 OVER() 使用 |
| 排名函数 | ROW_NUMBER、RANK、DENSE_RANK、NTILE |
为行编号或排名 |
| 偏移函数 | LAG、LEAD |
访问前/后行的数据 |
| 取值函数 | FIRST_VALUE、LAST_VALUE、NTH_VALUE |
取窗口中特定位置的值 |
二、PARTITION BY 与 ORDER BY
PARTITION BY 和 ORDER BY 是开窗函数中最重要的两个子句,它们决定了”窗口”的形状。
1. PARTITION BY(分区)
将数据按指定列分成多个分区,开窗函数在每个分区内独立计算。类似 GROUP BY 的分组,但不合并行。
1 | -- 计算每个员工的工资占其部门总工资的百分比 |
| name | department | salary | dept_total | pct |
|---|---|---|---|---|
| 张三 | 技术部 | 15000 | 55000 | 27.27 |
| 李四 | 技术部 | 18000 | 55000 | 32.73 |
| 周八 | 技术部 | 22000 | 55000 | 40.00 |
| 王五 | 销售部 | 12000 | 36000 | 33.33 |
| 赵六 | 销售部 | 9000 | 36000 | 25.00 |
| 吴九 | 销售部 | 15000 | 36000 | 41.67 |
| 孙七 | 人事部 | 11000 | 24000 | 45.83 |
| 郑十 | 人事部 | 13000 | 24000 | 54.17 |
💡 省略 PARTITION BY:如果不写
PARTITION BY,整个结果集被视为一个分区。
1 | -- 不分区:计算每个人工资占全公司总工资的百分比 |
2. ORDER BY(排序)
指定分区内的行排列顺序,这对排名函数和累计计算至关重要。
1 | -- 按月份排序,计算累计销售额 |
| salesperson | month | amount | running_total |
|---|---|---|---|
| 吴九 | 2025-01 | 35000 | 35000 |
| 吴九 | 2025-02 | 40000 | 75000 |
| 吴九 | 2025-03 | 38000 | 113000 |
| 吴九 | 2025-04 | 45000 | 158000 |
| 王五 | 2025-01 | 30000 | 30000 |
| 王五 | 2025-02 | 28000 | 58000 |
| 王五 | 2025-03 | 35000 | 93000 |
| 王五 | 2025-04 | 42000 | 135000 |
| 赵六 | 2025-01 | 25000 | 25000 |
| 赵六 | 2025-02 | 32000 | 57000 |
| 赵六 | 2025-03 | 28000 | 85000 |
| 赵六 | 2025-04 | 30000 | 115000 |
⚠️ 重要:在聚合开窗函数中加了
ORDER BY后,默认的计算范围会变成从分区开头到当前行(即累计计算),而不是整个分区!这一点很容易忽略,后面”窗口范围”章节会详细说明。
3. 组合使用
1 | -- PARTITION BY + ORDER BY:每个销售人员按月排序的累计业绩 |
💡 对比理解:
- 只有
PARTITION BY(无 ORDER BY)→ 整个分区的汇总值(每行相同)PARTITION BY+ORDER BY→ 分区内按顺序的累计值(逐行递增)
三、聚合开窗函数
常见的聚合函数 SUM、AVG、COUNT、MAX、MIN 都可以搭配 OVER() 使用。
1. SUM() OVER()
1 | -- 1. 全局总和(每行都显示相同的总数) |
2. AVG() OVER()
1 | -- 每个员工的工资与部门平均工资的差值 |
| name | department | salary | dept_avg | diff |
|---|---|---|---|---|
| 张三 | 技术部 | 15000 | 18333.33 | -3333.33 |
| 李四 | 技术部 | 18000 | 18333.33 | -333.33 |
| 周八 | 技术部 | 22000 | 18333.33 | 3666.67 |
| 王五 | 销售部 | 12000 | 12000.00 | 0.00 |
| 赵六 | 销售部 | 9000 | 12000.00 | -3000.00 |
| 吴九 | 销售部 | 15000 | 12000.00 | 3000.00 |
| 孙七 | 人事部 | 11000 | 12000.00 | -1000.00 |
| 郑十 | 人事部 | 13000 | 12000.00 | 1000.00 |
3. COUNT() / MAX() / MIN() OVER()
1 | -- 每个部门的人数、最高工资、最低工资 |
四、窗口范围(Frame)详解
窗口范围(Frame)定义了当前行参与计算的”可见范围”。这是开窗函数中最容易被忽略但非常重要的概念。
1. 基本语法
1 | ROWS BETWEEN 起点 AND 终点 |
常用的起点/终点关键字:
| 关键字 | 说明 |
|---|---|
UNBOUNDED PRECEDING |
分区的第一行 |
N PRECEDING |
当前行的前 N 行 |
CURRENT ROW |
当前行 |
N FOLLOWING |
当前行的后 N 行 |
UNBOUNDED FOLLOWING |
分区的最后一行 |
2. 默认窗口范围
⚠️ 易错点:有无
ORDER BY会导致默认窗口范围不同!
| 情况 | 默认窗口范围 | 效果 |
|---|---|---|
| 无 ORDER BY | ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING |
整个分区 |
| 有 ORDER BY | RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW |
分区开头到当前行 |
这就是为什么加了 ORDER BY 后,SUM() OVER() 会变成累计求和的原因。
1 | -- 无 ORDER BY → 整个分区的总和(每行一样) |
3. 常用窗口范围模式
1 | -- 移动平均(前1行 + 当前行 + 后1行 = 3行滑动窗口) |
| salesperson | month | amount | moving_avg_3 |
|---|---|---|---|
| 王五 | 2025-01 | 30000 | 29000.00 |
| 王五 | 2025-02 | 28000 | 31000.00 |
| 王五 | 2025-03 | 35000 | 35000.00 |
| 王五 | 2025-04 | 42000 | 38500.00 |
💡 解析:
- 2025-01:无前一行 → AVG(30000, 28000) = 29000
- 2025-02:AVG(30000, 28000, 35000) = 31000
- 2025-03:AVG(28000, 35000, 42000) = 35000
- 2025-04:无后一行 → AVG(35000, 42000) = 38500
其他常用模式:
1 | -- 从分区开头到当前行(默认,累计计算) |
4. ROWS vs RANGE
⚠️ 注意:
ROWS按物理行数计算,RANGE按值的范围计算。当存在相同排序值时,两者结果可能不同。
1 | -- ROWS:严格按行数,相同值的行也分开处理 |
💡 建议:大多数情况下使用
ROWS更直观、可预测。RANGE在处理相同排序值时有特殊行为,使用时要注意。
五、⭐ 排名函数
排名函数是开窗函数中使用频率最高的一类,面对”Top N”、”排名第几”等需求时必不可少。
1. ROW_NUMBER()
为分区内的每一行分配一个唯一的连续序号(1, 2, 3, …),即使值相同,序号也不同。
1 | SELECT |
| name | department | salary | rn |
|---|---|---|---|
| 周八 | 技术部 | 22000 | 1 |
| 李四 | 技术部 | 18000 | 2 |
| 张三 | 技术部 | 15000 | 3 |
| 吴九 | 销售部 | 15000 | 1 |
| 王五 | 销售部 | 12000 | 2 |
| 赵六 | 销售部 | 9000 | 3 |
| 郑十 | 人事部 | 13000 | 1 |
| 孙七 | 人事部 | 11000 | 2 |
⭐ 经典用法:取每组的 Top N
1 | -- 取每个部门工资最高的员工 |
| name | department | salary | rn |
|---|---|---|---|
| 周八 | 技术部 | 22000 | 1 |
| 吴九 | 销售部 | 15000 | 1 |
| 郑十 | 人事部 | 13000 | 1 |
⚠️ 注意:
ROW_NUMBER()在值相同时的排序是不确定的(相同值的行谁排前面取决于数据库实现)。如果需要确定性排序,应在ORDER BY中加入额外的列(如主键)。
2. RANK()
相同值获得相同排名,但后续排名会跳跃(如 1, 2, 2, 4)。
1 | SELECT |
| name | salary | rnk |
|---|---|---|
| 周八 | 22000 | 1 |
| 李四 | 18000 | 2 |
| 张三 | 15000 | 3 |
| 吴九 | 15000 | 3 |
| 郑十 | 13000 | 5 |
| 王五 | 12000 | 6 |
| 孙七 | 11000 | 7 |
| 赵六 | 9000 | 8 |
注意:张三和吴九并列第 3,下一名直接跳到第 5(跳过了 4)。
3. DENSE_RANK()
相同值获得相同排名,后续排名不跳跃(如 1, 2, 2, 3)。
1 | SELECT |
| name | salary | drnk |
|---|---|---|
| 周八 | 22000 | 1 |
| 李四 | 18000 | 2 |
| 张三 | 15000 | 3 |
| 吴九 | 15000 | 3 |
| 郑十 | 13000 | 4 |
| 王五 | 12000 | 5 |
| 孙七 | 11000 | 6 |
| 赵六 | 9000 | 7 |
4. ⭐ ROW_NUMBER vs RANK vs DENSE_RANK 对比
将三个函数放在一起对比,差异一目了然:
1 | SELECT |
| name | salary | row_num | rnk | dense_rnk |
|---|---|---|---|---|
| 周八 | 22000 | 1 | 1 | 1 |
| 李四 | 18000 | 2 | 2 | 2 |
| 张三 | 15000 | 3 | 3 | 3 |
| 吴九 | 15000 | 4 | 3 | 3 |
| 郑十 | 13000 | 5 | 5 | 4 |
| 王五 | 12000 | 6 | 6 | 5 |
| 孙七 | 11000 | 7 | 7 | 6 |
| 赵六 | 9000 | 8 | 8 | 7 |
| 函数 | 并列处理 | 排名跳跃 | 典型场景 |
|---|---|---|---|
ROW_NUMBER |
❌ 不并列(每行唯一) | 不涉及 | 分页、Top N、去重 |
RANK |
✅ 并列 | ✅ 会跳跃 | 竞赛排名(第3名两人,下一个是第5名) |
DENSE_RANK |
✅ 并列 | ❌ 不跳跃 | 连续排名(第3名两人,下一个是第4名) |
💡 选择建议:
- 需要唯一编号 →
ROW_NUMBER(分页、去重取一)- 需要真实名次(如比赛排名)→
RANK- 需要连续等级(如工资等级)→
DENSE_RANK
5. NTILE(n)
将分区内的行均分为 n 组,返回组号(1 到 n)。常用于”前 25%”、”四分位”等场景。
1 | -- 将员工按工资分成 4 组(四分位) |
| name | salary | quartile |
|---|---|---|
| 周八 | 22000 | 1 |
| 李四 | 18000 | 1 |
| 张三 | 15000 | 2 |
| 吴九 | 15000 | 2 |
| 郑十 | 13000 | 3 |
| 王五 | 12000 | 3 |
| 孙七 | 11000 | 4 |
| 赵六 | 9000 | 4 |
💡 8 行分 4 组,每组 2 行。如果不能整除,前面的组会多分 1 行(如 7 行分 3 组 → 3, 2, 2)。
六、⭐ 偏移函数与取值函数
1. LAG(expr, offset, default)
获取当前行前面第 N 行的值。
expr:要获取的列offset:偏移量,默认 1(前一行)default:没有前一行时的默认值,默认 NULL
1 | -- 计算每个销售人员的月度环比增长 |
| salesperson | month | amount | prev_amount | growth |
|---|---|---|---|---|
| 王五 | 2025-01 | 30000 | NULL | NULL |
| 王五 | 2025-02 | 28000 | 30000 | -2000 |
| 王五 | 2025-03 | 35000 | 28000 | 7000 |
| 王五 | 2025-04 | 42000 | 35000 | 7000 |
| 赵六 | 2025-01 | 25000 | NULL | NULL |
| 赵六 | 2025-02 | 32000 | 25000 | 7000 |
| 赵六 | 2025-03 | 28000 | 32000 | -4000 |
| 赵六 | 2025-04 | 30000 | 28000 | 2000 |
| 吴九 | 2025-01 | 35000 | NULL | NULL |
| 吴九 | 2025-02 | 40000 | 35000 | 5000 |
| 吴九 | 2025-03 | 38000 | 40000 | -2000 |
| 吴九 | 2025-04 | 45000 | 38000 | 7000 |
1 | -- 指定偏移量和默认值 |
2. LEAD(expr, offset, default)
获取当前行后面第 N 行的值,参数与 LAG 相同,方向相反。
1 | -- 查看每个销售人员下个月的业绩 |
| salesperson | month | amount | next_amount |
|---|---|---|---|
| 王五 | 2025-01 | 30000 | 28000 |
| 王五 | 2025-02 | 28000 | 35000 |
| 王五 | 2025-03 | 35000 | 42000 |
| 王五 | 2025-04 | 42000 | NULL |
⭐ 实用场景:计算环比增长率
1 | SELECT |
3. FIRST_VALUE() / LAST_VALUE()
获取窗口中第一行 / 最后一行的值。
1 | -- 每个部门工资最高的人的工资(第一行 = 最大值) |
⚠️ LAST_VALUE 的陷阱:
LAST_VALUE搭配ORDER BY使用时,默认窗口范围是”到当前行为止”,所以LAST_VALUE实际返回的是当前行的值,而不是分区最后一行!
1 | -- ❌ 结果不符合预期:每行的 last_val 都是自己 |
💡 建议:使用
LAST_VALUE时必须显式指定窗口范围,否则很容易得到错误结果。或者直接用FIRST_VALUE配合反向排序来替代。
七、⚠️ 开窗函数不能在 WHERE 中使用
这是一个非常重要的限制:开窗函数的结果不能直接在 WHERE、GROUP BY、HAVING 中使用。
原因:SQL 的执行顺序决定了这一点:
1 | FROM → WHERE → GROUP BY → HAVING → SELECT(开窗函数在此计算) → ORDER BY → LIMIT |
WHERE 在 SELECT 之前执行,而开窗函数是在 SELECT 阶段才计算的,所以 WHERE 根本”看不到”开窗函数产生的列。
1 | -- ❌ 错误:WHERE 中不能直接引用开窗函数 |
| name | salary | rnk |
|---|---|---|
| 周八 | 22000 | 1 |
| 李四 | 18000 | 2 |
| 张三 | 15000 | 3 |
| 吴九 | 15000 | 3 |
💡 实用建议:需要对开窗函数的结果做过滤时,套一层子查询或 CTE 是标准做法。推荐用 CTE(
WITH语法),可读性更好。这也是前一篇文章中 CTE 的典型应用场景之一。
⭐ 记忆口诀:开窗函数只能出现在
SELECT和ORDER BY中,不能出现在WHERE、GROUP BY、HAVING中。要过滤?先套一层!
八、综合速查表
| 需求 | 函数 / 写法 | 要点 |
|---|---|---|
| 分区内汇总(保留明细) | SUM/AVG/COUNT() OVER(PARTITION BY ...) |
不加 ORDER BY = 整个分区 |
| 累计求和 | SUM() OVER(PARTITION BY ... ORDER BY ...) |
加 ORDER BY 默认累计 |
| 移动平均 / 滑动窗口 | AVG() OVER(... ROWS BETWEEN N PRECEDING AND N FOLLOWING) |
手动指定 Frame |
| 唯一行号 | ROW_NUMBER() OVER(ORDER BY ...) |
不并列,常用于 Top N |
| 排名(跳跃) | RANK() OVER(ORDER BY ...) |
并列后跳号 |
| 排名(不跳跃) | DENSE_RANK() OVER(ORDER BY ...) |
并列后连续 |
| 均分为 N 组 | NTILE(N) OVER(ORDER BY ...) |
四分位、百分位 |
| 前一行的值 | LAG(col, 1) OVER(ORDER BY ...) |
环比计算 |
| 后一行的值 | LEAD(col, 1) OVER(ORDER BY ...) |
预测 / 对比 |
| 窗口内第一个值 | FIRST_VALUE(col) OVER(...) |
分区最大/最小 |
| 窗口内最后一个值 | LAST_VALUE(col) OVER(... ROWS BETWEEN ... AND UNBOUNDED FOLLOWING) |
⚠️ 必须显式指定 Frame |
九、LeetCode 相关练习
🔗 后续待补充..
参考资料
📚 本文内容参考以下资料整理: