由浅入深学习开窗函数
开窗函数(Window Function)是SQL中强大的分析工具,与聚合函数(如SUM、COUNT)不同,它不会将多行数据聚合为一行,而是为每行数据计算一个基于“窗口”(指定范围的行集合)的结果,保留原始行的所有信息。本文从基础概念到高级用法,再结合实战案例,带你全面掌握开窗函数。
一、基础认知:开窗函数 vs 聚合函数
先通过一个简单对比理解核心差异:
1. 聚合函数(GROUP BY)
-- 计算每个部门的平均工资(聚合后仅保留部门维度)
SELECT dept_id, AVG(salary) AS avg_sal
FROM employee
GROUP BY dept_id;
结果会按部门分组,每行只显示部门和平均工资,丢失了员工的个体信息。
2. 开窗函数
-- 计算每个员工所属部门的平均工资(保留员工所有信息)
SELECT
emp_id, emp_name, dept_id, salary,
AVG(salary) OVER (PARTITION BY dept_id) AS avg_dept_sal
FROM employee;
结果中每行保留员工的ID、姓名、部门、工资,同时新增“所属部门平均工资”列——这就是开窗函数的核心价值:保留明细,同时计算聚合维度的指标。
二、开窗函数的基本语法
开窗函数的通用结构:
<函数名>(<参数>) OVER (
[PARTITION BY <分区列>] -- 可选:按哪些列分组(类似GROUP BY)
[ORDER BY <排序列> [ASC/DESC]] -- 可选:窗口内的行排序
[ROWS/RANGE <窗口范围>] -- 可选:定义窗口的物理/逻辑范围
) AS <别名>
核心组件解析
| 组件 | 作用 | 示例 |
|---|---|---|
| PARTITION BY | 分区:将数据划分为多个独立的“窗口”,函数仅在分区内计算 | PARTITION BY dept_id(按部门分区) |
| ORDER BY | 排序:指定分区内行的顺序(仅对需要排序的函数生效,如ROW_NUMBER) | ORDER BY salary DESC(按工资降序) |
| ROWS/RANGE | 窗口范围:限定分区内参与计算的行范围(物理/逻辑范围) | ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING(当前行±1行) |
三、入门:常用开窗函数分类
开窗函数分为三类,从易到难掌握:
1. 聚合类开窗函数
将普通聚合函数(SUM/AVG/COUNT/MIN/MAX)结合OVER子句使用,是最基础的开窗函数。
示例1:无分区、无排序(全局窗口)
-- 计算所有员工的平均工资(每行都显示全局平均)
SELECT
emp_id, salary,
AVG(salary) OVER () AS global_avg_sal
FROM employee;
示例2:按分区聚合
-- 计算每个部门的工资总和、员工数、最高/最低工资
SELECT
emp_id, dept_id, salary,
SUM(salary) OVER (PARTITION BY dept_id) AS dept_sal_sum,
COUNT(*) OVER (PARTITION BY dept_id) AS dept_emp_count,
MAX(salary) OVER (PARTITION BY dept_id) AS dept_max_sal
FROM employee;
2. 排序类开窗函数
用于对分区内的行进行排序并生成序号,是最常用的开窗函数之一。
| 函数 | 作用 | 特点 |
|---|---|---|
| ROW_NUMBER() | 生成连续序号 | 即使值相同,序号也不同(1,2,3...) |
| RANK() | 生成排名 | 相同值并列,后续序号跳跃(1,2,2,4...) |
| DENSE_RANK() | 生成密集排名 | 相同值并列,后续序号不跳跃(1,2,2,3...) |
示例:给每个部门的员工按工资降序排名
SELECT
emp_id, dept_id, salary,
ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk,
DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS drk
FROM employee;
结果示例(部门ID=1):
| emp_id | dept_id | salary | rn | rk | drk |
|---|---|---|---|---|---|
| 101 | 1 | 20000 | 1 | 1 | 1 |
| 102 | 1 | 18000 | 2 | 2 | 2 |
| 103 | 1 | 18000 | 3 | 2 | 2 |
| 104 | 1 | 15000 | 4 | 4 | 3 |
3. 偏移类开窗函数
用于获取分区内指定位置的行数据,解决“取上一行/下一行/第N行”的问题。
| 函数 | 作用 | 示例 |
|---|---|---|
| LAG(col, n, default) | 获取当前行的上n行的col值(n默认1,default为无数据时的默认值) | LAG(salary, 1, 0)(上一行工资,无则0) |
| LEAD(col, n, default) | 获取当前行的下n行的col值 | LEAD(salary, 1, 0)(下一行工资,无则0) |
| FIRST_VALUE(col) | 获取分区内排序后的第一行col值 | FIRST_VALUE(salary)(分区内最高工资) |
| LAST_VALUE(col) | 获取分区内排序后的最后一行col值 | LAST_VALUE(salary)(分区内最低工资) |
示例:获取员工的上/下一位同事的工资
SELECT
emp_id, dept_id, salary,
LAG(salary, 1) OVER (PARTITION BY dept_id ORDER BY emp_id) AS prev_sal,
LEAD(salary, 1) OVER (PARTITION BY dept_id ORDER BY emp_id) AS next_sal
FROM employee;
四、实战案例:开窗函数的典型应用
以下案例基于常见业务场景,使用employee(员工表)和order(订单表),表结构如下:
员工表(employee)
| 字段名 | 类型 | 说明 |
|---|---|---|
| emp_id | INT | 员工ID |
| emp_name | VARCHAR | 员工姓名 |
| dept_id | INT | 部门ID |
| salary | DECIMAL | 工资 |
| hire_date | DATE | 入职日期 |
订单表(order)
| 字段名 | 类型 | 说明 |
|---|---|---|
| order_id | INT | 订单ID |
| user_id | INT | 用户ID |
| order_time | DATETIME | 下单时间 |
| amount | DECIMAL | 订单金额 |
案例1:Top N问题(取每个部门工资最高的2名员工)
-- 步骤1:给每个部门的员工按工资降序编号
WITH emp_rank AS (
SELECT
emp_id, emp_name, dept_id, salary,
ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn
FROM employee
)
-- 步骤2:筛选编号≤2的员工
SELECT * FROM emp_rank WHERE rn <= 2;
案例2:同比/环比分析(计算每月订单金额的环比增长率)
-- 步骤1:按月份汇总订单金额
WITH month_amount AS (
SELECT
DATE_FORMAT(order_time, '%Y-%m') AS month,
SUM(amount) AS total_amount
FROM `order`
GROUP BY DATE_FORMAT(order_time, '%Y-%m')
),
-- 步骤2:获取上月订单金额
month_lag AS (
SELECT
month, total_amount,
LAG(total_amount, 1) OVER (ORDER BY month) AS last_month_amount
FROM month_amount
)
-- 步骤3:计算环比增长率(保留2位小数)
SELECT
month, total_amount, last_month_amount,
ROUND((total_amount - last_month_amount) / last_month_amount * 100, 2) AS mom_growth
FROM month_lag;
案例3:累计求和(计算用户累计下单金额)
SELECT
user_id, order_id, order_time, amount,
-- 按用户分区,按下单时间排序,累计金额
SUM(amount) OVER (PARTITION BY user_id ORDER BY order_time
) AS cum_amount
FROM `order`
ORDER BY user_id, order_time;
案例4:删除重复数据(保留每个用户最早的订单)
-- 步骤1:给每个用户的订单按时间排序
WITH order_rank AS (
SELECT
order_id, user_id, order_time, amount,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time) AS rn
FROM `order`
)
-- 步骤2:删除rn>1的重复订单(保留rn=1的最早订单)
DELETE FROM `order`
WHERE order_id IN (SELECT order_id FROM order_rank WHERE rn > 1);
五、总结
开窗函数的核心是“保留明细+分区计算”,相比传统的GROUP BY和子查询,能大幅简化代码、提升效率。学习路径建议:
-
先掌握聚合类开窗函数(SUM/AVG+PARTITION BY),理解分区的概念;
-
再掌握排序类函数(ROW_NUMBER/RANK),解决Top N、排名问题;
-
接着学习偏移类函数(LAG/LEAD),处理行与行之间的关联;
开窗函数是SQL进阶的必备技能,熟练掌握后能高效解决各类分析型需求,尤其在数据仓库、报表开发、业务分析场景中应用广泛。