SQL 进阶指南:窗口函数的神奇用法

发布于:2025-09-06 ⋅ 阅读:(20) ⋅ 点赞:(0)

学完基础查询后,你可能会遇到这样的难题:“想给每个部门的员工按业绩排名,同时显示员工的原始信息”“想计算每个员工的业绩占部门总业绩的百分比”。用普通的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;


网站公告

今日签到

点亮在社区的每一天
去签到