学完基础查询后,你可能会遇到这样的难题:“想给每个部门的员工按业绩排名,同时显示员工的原始信息”“想计算每个员工的业绩占部门总业绩的百分比”。用普通的GROUP BY很难实现 —— 要么只能看分组统计结果,要么只能看原始数据。而窗口函数,就是专门解决 “既想保留原始数据,又想做分组统计 / 排名” 的神器。今天我们用 “员工业绩表” 为案例,手把手教你学会窗口函数。
我整理好了超全的学习资料,蕴含SQL、Python、Excel、数据库、数据分析等内容
学习资料合集https://www.kdocs.cn/l/cjchDXwklk1B
一、先搞懂:什么是窗口函数?
窗口函数可以理解为 “在数据的‘窗口’上做计算”:
- “窗口”:不是指电脑窗口,而是指 “一组与当前行相关的数据行”(比如当前员工所在部门的所有员工);
- “函数”:对这组数据做统计或排名(比如算部门内排名、部门业绩平均值)。
核心特点:不改变原始数据的行数,能在每行数据后额外增加一列 “统计 / 排名结果”,这是它和GROUP BY最大的区别(GROUP BY会压缩行数)。
基本语法:
窗口函数() OVER (PARTITION BY 分组字段 ORDER BY 排序字段)
- PARTITION BY:按哪个字段分组(比如按 “部门” 分组,每个部门是一个独立窗口),可选;
- ORDER BY:在分组内按哪个字段排序(比如按 “业绩” 降序排,用于排名),可选。
准备案例数据:员工业绩表
我们创建全新的 “员工业绩表”(表名:staff_performance),包含员工姓名、部门、业绩等信息,代码可直接复制运行:
-- 创建员工业绩表
CREATE TABLE staff_performance (
staff_id INT, -- 员工ID
staff_name VARCHAR(20), -- 员工姓名
department VARCHAR(10), -- 部门
performance INT, -- 月度业绩(万元)
hire_date DATE -- 入职日期
);
-- 插入测试数据
INSERT INTO staff_performance VALUES
(101, '张明', '技术部', 28, '2023-01-15'),
(102, '李娜', '技术部', 35, '2022-09-20'),
(103, '王强', '销售部', 52, '2023-03-10'),
(104, '赵晓', '销售部', 48, '2022-11-05'),
(105, '刘芳', '销售部', 60, '2021-07-12'),
(106, '陈杰', '人事部', 18, '2023-02-28'),
(107, '黄丽', '人事部', 22, '2022-08-30');
-- 查看表数据
SELECT * FROM staff_performance;
表中数据如下:
staff_id |
staff_name |
department |
performance |
hire_date |
101 |
张明 |
技术部 |
28 |
2023-01-15 |
102 |
李娜 |
技术部 |
35 |
2022-09-20 |
103 |
王强 |
销售部 |
52 |
2023-03-10 |
104 |
赵晓 |
销售部 |
48 |
2022-11-05 |
105 |
刘芳 |
销售部 |
60 |
2021-07-12 |
106 |
陈杰 |
人事部 |
18 |
2023-02-28 |
107 |
黄丽 |
人事部 |
22 |
2022-08-30 |
二、常用窗口函数:3 类核心场景
窗口函数分很多种,但对新手来说,掌握 “排名函数”“聚合窗口函数”“分布函数” 这 3 类就够应对 80% 的需求,我们逐个讲。
场景 1:排名函数(最常用)
解决 “按某个字段分组排名” 的问题,比如 “每个部门按业绩排名”“全公司按入职时间排名”。常用的有 3 个:ROW_NUMBER()、RANK()、DENSE_RANK()。
例子 1:每个部门按业绩降序排名(对比 3 个排名函数)
需求:给每个部门的员工按业绩从高到低排名,同时显示 3 种排名结果,看区别。
代码:
SELECT
staff_name,
department,
performance,
-- 1. ROW_NUMBER():不重复排名,即使业绩相同也按顺序排1、2、3
ROW_NUMBER() OVER (PARTITION BY department ORDER BY performance DESC) AS rn,
-- 2. RANK():相同业绩并列排名,下一名跳过重复名次(如1、1、3)
RANK() OVER (PARTITION BY department ORDER BY performance DESC) AS rk,
-- 3. DENSE_RANK():相同业绩并列排名,下一名不跳过重复名次(如1、1、2)
DENSE_RANK() OVER (PARTITION BY department ORDER BY performance DESC) AS dr
FROM staff_performance;
运行结果(重点看销售部,假设新增 1 个业绩 48 的员工 108):
staff_name |
department |
performance |
rn |
rk |
dr |
李娜 |
技术部 |
35 |
1 |
1 |
1 |
张明 |
技术部 |
28 |
2 |
2 |
2 |
刘芳 |
销售部 |
60 |
1 |
1 |
1 |
王强 |
销售部 |
52 |
2 |
2 |
2 |
赵晓 |
销售部 |
48 |
3 |
3 |
3 |
陈杰 |
人事部 |
18 |
2 |
2 |
2 |
黄丽 |
人事部 |
22 |
1 |
1 |
1 |
如果销售部新增员工 108(业绩 48),结果会变成:
staff_name |
department |
performance |
rn |
rk |
dr |
刘芳 |
销售部 |
60 |
1 |
1 |
1 |
王强 |
销售部 |
52 |
2 |
2 |
2 |
赵晓 |
销售部 |
48 |
3 |
3 |
3 |
员工 108 |
销售部 |
48 |
4 |
3 |
3 |
总结 3 个排名函数的区别:
- 要 “无重复排名” 用ROW_NUMBER()(比如给员工发唯一编号);
- 要 “并列且跳过名次” 用RANK()(比如比赛排名);
- 要 “并列且不跳过名次” 用DENSE_RANK()(比如部门内评级)。
场景 2:聚合窗口函数(分组统计 + 保留原始数据)
把SUM()、AVG()、MAX()等聚合函数放到OVER()里,就能实现 “在每行显示分组后的统计结果”,比如 “显示每个员工的业绩,同时显示其部门的业绩总和、平均值”。
例子 2:显示每个员工的业绩及部门统计信息
需求:查看员工业绩,同时显示该员工所在部门的 “业绩总和”“业绩平均值”“最高业绩”。
代码:
SELECT
staff_name,
department,
performance,
-- 部门内业绩总和(按部门分组,不排序)
SUM(performance) OVER (PARTITION BY department) AS dept_total,
-- 部门内业绩平均值(保留1位小数)
ROUND(AVG(performance) OVER (PARTITION BY department), 1) AS dept_avg,
-- 部门内最高业绩
MAX(performance) OVER (PARTITION BY department) AS dept_max
FROM staff_performance;
运行结果:
staff_name |
department |
performance |
dept_total |
dept_avg |
dept_max |
张明 |
技术部 |
28 |
63 |
31.5 |
35 |
李娜 |
技术部 |
35 |
63 |
31.5 |
35 |
王强 |
销售部 |
52 |
160 |
53.3 |
60 |
赵晓 |
销售部 |
48 |
160 |
53.3 |
60 |
刘芳 |
销售部 |
60 |
160 |
53.3 |
60 |
陈杰 |
人事部 |
18 |
40 |
20.0 |
22 |
黄丽 |
人事部 |
22 |
40 |
20.0 |
22 |
可以看到:技术部 2 个员工的 “dept_total” 都是 63(28+35),销售部 3 个员工的 “dept_total” 都是 160(52+48+60)—— 这就是聚合窗口函数的作用:同一分组内的统计结果相同,且保留所有原始行。
场景 3:分布函数(占比 / 累计统计)
常用PERCENT_RANK()(百分比排名)和CUME_DIST()(累计分布),解决 “看某个值在分组内的占比或位置” 的问题,比如 “某员工业绩占部门总业绩的百分比”“业绩累计到当前员工的占比”。
例子 3:计算员工业绩的部门占比和累计占比
需求:查看每个员工的业绩,以及 “业绩占部门总业绩的百分比”“累计业绩占部门总业绩的百分比”。
代码:
SELECT
staff_name,
department,
performance,
-- 业绩占部门总业绩的百分比(保留2位小数)
ROUND(performance / SUM(performance) OVER (PARTITION BY department) * 100, 2) AS perf_pct,
-- 累计业绩占部门总业绩的百分比(按业绩降序累计)
ROUND(SUM(performance) OVER (
PARTITION BY department
ORDER BY performance DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW -- 从分组第一行到当前行
) / SUM(performance) OVER (PARTITION BY department) * 100, 2) AS cumul_pct
FROM staff_performance;
运行结果(以销售部为例):
staff_name |
department |
performance |
perf_pct |
cumul_pct |
刘芳 |
销售部 |
60 |
37.50 |
37.50 |
王强 |
销售部 |
52 |
32.50 |
70.00 |
赵晓 |
销售部 |
48 |
30.00 |
100.00 |
李娜 |
技术部 |
35 |
55.56 |
55.56 |
张明 |
技术部 |
28 |
44.44 |
100.00 |
黄丽 |
人事部 |
22 |
55.00 |
55.00 |
陈杰 |
人事部 |
18 |
45.00 |
100.00 |
这个结果很实用:比如销售部的刘芳,业绩占部门 37.5%,累计到她时占比 37.5%;累计到王强时,两人业绩占部门 70%,能快速看出核心员工的贡献。
三、窗口函数的 3 个实用技巧
1.省略PARTITION BY:全表作为一个窗口
如果不加PARTITION BY,整个表就是一个窗口。比如 “全公司按业绩排名”:
SELECT staff_name, performance,
ROW_NUMBER() OVER (ORDER BY performance DESC) AS company_rank
FROM staff_performance;
2.结合过滤条件:先筛选再开窗
可以用WHERE先筛选数据,再对筛选后的结果开窗。比如 “只看 2023 年入职的员工,按部门排名”:
SELECT staff_name, department, performance,
RANK() OVER (PARTITION BY department ORDER BY performance DESC) AS dept_rank
FROM staff_performance
WHERE hire_date >= '2023-01-01'; -- 先筛选2023年入职的员工
3.避免常见误区:窗口函数不能用在 WHERE 里
窗口函数的结果是 “每行的额外列”,WHERE是在开窗前筛选行,所以不能直接写WHERE rn = 1(rn 是窗口函数的结果)。如果要筛选排名第一的员工,需要用子查询或 CTE:
-- 子查询筛选每个部门业绩第一的员工
SELECT * FROM (
SELECT staff_name, department, performance,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY performance DESC) AS rn
FROM staff_performance
) AS temp
WHERE temp.rn = 1; -- 这里的rn是子查询的结果,能用于WHERE
四、课后小练习(巩固一下)
1.给每个部门的员工按 “入职时间最早” 排名(入职时间越早,排名越靠前),用ROW_NUMBER();
2.计算每个员工的业绩,以及 “全公司的业绩总和”“全公司业绩平均值”;
3.找出每个部门业绩排名前 2 的员工(用ROW_NUMBER())。
练习答案
1.按入职时间排名:
SELECT staff_name, department, hire_date,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY hire_date ASC) AS hire_rank
FROM staff_performance;
2.全公司业绩统计:
SELECT staff_name, performance,
SUM(performance) OVER () AS company_total, -- 全公司总和(无PARTITION BY)
ROUND(AVG(performance) OVER (), 1) AS company_avg
FROM staff_performance;
3.每个部门业绩前 2 的员工:
SELECT * FROM (
SELECT staff_name, department, performance,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY performance DESC) AS rn
FROM staff_performance
) AS temp
WHERE temp.rn <= 2;