场景引入
前面两天我们学了窗口函数的基础语法和排名类窗口函数。但窗口函数真正的威力,在于它能把聚合函数 (SUM、AVG、COUNT、MAX、MIN)和窗口的"滑动"能力结合起来------保留每一行明细的同时,还能看到累计、移动平均、分组占比。
普通聚合函数会让多行变一行(GROUP BY),而聚合窗口函数让每一行都保留,同时看到聚合结果。
一、聚合窗口函数基础语法
sql
<聚合函数>(<表达式>) OVER (
[PARTITION BY <分组列>]
[ORDER BY <排序列>]
[<窗口帧子句>]
)
与普通聚合函数唯一的区别就是多了 OVER (...)。
常用聚合函数
| 函数 | 含义 |
|---|---|
| SUM() | 求和 |
| AVG() | 平均值 |
| COUNT() | 计数 |
| MAX() | 最大值 |
| MIN() | 最小值 |
二、不加 ORDER BY:每组总览
当 OVER() 中只有 PARTITION BY 而没有 ORDER BY 时,窗口就是整个分区,相当于把 GROUP BY 的结果贴回每一行。
示例:各部门总薪资
sql
SELECT
name,
department,
salary,
SUM(salary) OVER (PARTITION BY department) AS dept_total,
AVG(salary) OVER (PARTITION BY department) AS dept_avg
FROM employees;
执行结果:
| name | department | salary | dept_total | dept_avg |
|---|---|---|---|---|
| 张三 | 技术部 | 25000 | 75000 | 25000.0 |
| 李四 | 技术部 | 28000 | 75000 | 25000.0 |
| 王五 | 技术部 | 22000 | 75000 | 25000.0 |
| 赵六 | 市场部 | 18000 | 35000 | 17500.0 |
| 钱七 | 市场部 | 17000 | 35000 | 17500.0 |
关键点:
- 每一行都显示所在部门的薪资总和与平均值
- 可以用
dept_total / dept_avg计算该员工薪资在部门中的相对位置 - ✅ 这种写法比
GROUP BY后再JOIN回原表优雅得多
三、加 ORDER BY:累计与滚动
当加上 ORDER BY 后,窗口默认从分区第一行到当前行 ,这就实现了累计(Running Total)。
默认窗口帧:RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
示例:按日期累计销售额
sql
SELECT
sale_date,
amount,
SUM(amount) OVER (ORDER BY sale_date) AS running_total
FROM sales
ORDER BY sale_date;
执行结果:
| sale_date | amount | running_total |
|---|---|---|
| 2026-01-01 | 1200 | 1200 |
| 2026-01-02 | 800 | 2000 |
| 2026-01-03 | 1500 | 3500 |
| 2026-01-05 | 900 | 4400 |
| 2026-01-06 | 2000 | 6400 |
注意事项:
- 2026-01-03 到 2026-01-05 之间有间隔(1月4日无数据)
- 默认帧是
RANGE BETWEEN,当日期不连续时,会把相同日期的行算在一起 - 如果要精确控制行数,需要显式指定
ROWS BETWEEN
四、窗口帧(Frame)详解
窗口帧定义了「窗口有多大」,三种模式:
| 模式 | 含义 | 适用场景 |
|---|---|---|
ROWS |
按物理行数 | 移动平均、精确行数控制 |
RANGE |
按逻辑值范围(ORDER BY 列的值) | 日期连续累计 |
GROUPS |
按 ORDER BY 值相同的组 | 较少使用 |
常用帧边界
sql
-- 从分区开始到当前行(累计)
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
-- 当前行前后各2行(共5行)
ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING
-- 从第1行到第N行
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING
-- 仅当前行
ROWS BETWEEN CURRENT ROW AND CURRENT ROW
五、实战:3 日移动平均
sql
SELECT
sale_date,
amount,
AVG(amount) OVER (
ORDER BY sale_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_3d
FROM sales
ORDER BY sale_date;
执行结果:
| sale_date | amount | moving_avg_3d |
|---|---|---|
| 2026-01-01 | 1200 | 1200.0 |
| 2026-01-02 | 800 | 1000.0 |
| 2026-01-03 | 1500 | 1166.7 |
| 2026-01-05 | 900 | 1066.7 |
| 2026-01-06 | 2000 | 1466.7 |
解释:
- 第1行不够3行 → 只算1行平均值
- 第2行不够3行 → 算前2行平均值
- 第3行之后 → 算前3行平均值
六、按部门计算移动平均
sql
SELECT
name,
department,
salary,
AVG(salary) OVER (
PARTITION BY department
ORDER BY salary
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
) AS local_avg
FROM employees
ORDER BY department, salary;
这个查询会在每个部门内部 ,对每行取其前一行 + 当前行 + 后一行的薪资平均值。
应用场景:
- 薪资异常检测:如果某人的薪资远低于
local_avg或远高于local_avg - 趋势平滑:去除极端值的影响
七、COUNT + 窗口:分组内的行号之外的统计
sql
SELECT
department,
name,
salary,
COUNT(*) OVER (PARTITION BY department) AS dept_headcount,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rank_in_dept
FROM employees;
用途: 同时看到部门人数 + 部门内排名,一个查询搞定!
八、MAX / MIN 窗口:找出组内极值并做差值
sql
SELECT
department,
name,
salary,
MAX(salary) OVER (PARTITION BY department) AS dept_max,
MAX(salary) OVER (PARTITION BY department) - salary AS gap_to_max
FROM employees;
结果:
| department | name | salary | dept_max | gap_to_max |
|---|---|---|---|---|
| 技术部 | 李四 | 28000 | 28000 | 0 |
| 技术部 | 张三 | 25000 | 28000 | 3000 |
| 技术部 | 王五 | 22000 | 28000 | 6000 |
| 市场部 | 赵六 | 18000 | 18000 | 0 |
| 市场部 | 钱七 | 17000 | 18000 | 1000 |
九、综合实战:销售月报分析
sql
SELECT
DATE_FORMAT(sale_date, '%Y-%m') AS month,
SUM(amount) AS monthly_total,
AVG(SUM(amount)) OVER (
ORDER BY DATE_FORMAT(sale_date, '%Y-%m')
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS quarterly_avg,
SUM(SUM(amount)) OVER (
ORDER BY DATE_FORMAT(sale_date, '%Y-%m')
) AS ytd_total
FROM sales
GROUP BY DATE_FORMAT(sale_date, '%Y-%m')
ORDER BY month;
这里用了嵌套聚合:先 GROUP BY 算出每月总额,再用窗口函数计算季度平均和年度累计。
⚠️ 注意事项
-
ROWS vs RANGE 默认行为不同
ORDER BY x不加帧子句 → 默认RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWRANGE会把 ORDER BY 值相同的行全部纳入窗口,可能导致意外结果
-
ORDER BY 列有重复值时注意 RANGE
sql-- 如果 sale_date 有重复,RANGE 会把所有相同日期的行都算上 SUM(amount) OVER (ORDER BY sale_date)改成
ROWS可精确控制行数 -
性能考虑
- 窗口函数通常比同功能的子查询快,因为只需扫描一次表
- 但
PARTITION BY列有无索引会影响性能
-
窗口帧不能反向指定
ROWS BETWEEN 1 FOLLOWING AND 2 PRECEDING❌ 会报错- 起始边界必须在终止边界之前
-
所有的聚合窗口函数都可以搭配 FILTER
sqlSUM(salary) FILTER (WHERE department = '技术部') OVER (ORDER BY hire_date)但注意:FILTER 不是所有数据库都支持(PostgreSQL ✅,MySQL ❌)
总结
| 场景 | 写法 | 用途 |
|---|---|---|
| 分组总览 | SUM(x) OVER (PARTITION BY dept) |
保留明细+分组统计 |
| 累计求和 | SUM(x) OVER (ORDER BY date) |
运行总量 |
| 移动平均 | AVG(x) OVER (ORDER BY d ROWS BETWEEN N PRECEDING AND CURRENT ROW) |
平滑趋势 |
| 组内比较 | MAX(x) OVER (PARTITION BY dept) - x |
找出差值 |
| 占比计算 | x / SUM(x) OVER (PARTITION BY dept) |
分组占比 |
Day 14 的核心心法是:聚合窗口函数 = 普通聚合函数 + OVER(),让聚合结果和每一行共存。这是数据分析中最常用的 SQL 技巧之一,掌握它,你的 SQL 水平会上一个台阶。