当前位置: 首页 > news >正文

MySQL 窗口函数入门到精通

目录

常用窗口函数速查表

1. 什么是"窗口"(不是你想的那种窗口)

"窗口"≠电脑界面的窗口

那么,SQL 中的"窗口"是什么?

用表格形式理解"窗口"概念

2. 窗口函数解决了什么问题

场景:学生成绩单

窗口函数的主要优势

3. 窗口函数基础语法详解

3.1 OVER 子句:窗口函数的核心

3.2 PARTITION BY:分组但不合并

3.3 ORDER BY:窗口内的排序

3.4 组合使用 PARTITION BY 和 ORDER BY

实际演示:创建示例数据

4. 排名函数详解与实例

4.1 ROW_NUMBER() 函数:每行唯一序号

4.2 RANK() 函数:相同值相同排名,会跳过排名

4.3 DENSE_RANK() 函数:相同值相同排名,不跳过排名

4.4 NTILE(n) 函数:将数据分成n个组

4.5 排名函数实际应用案例

5. 聚合窗口函数实例解析

5.1 SUM()、AVG()、COUNT()、MIN()、MAX() 函数

5.2 窗口聚合 vs. GROUP BY 聚合

5.3 累计聚合计算

5.4 分组内的累计聚合

6.1 LAG() 函数:获取前几行的值

6.2 LEAD() 函数:获取后几行的值

6.3 FIRST_VALUE() 函数:窗口中第一行的值

6.4 LAST_VALUE() 函数:窗口中最后一行的值

6.5 偏移函数分组应用

7. 分组与排序的结合使用

7.1 找出每组最值

7.2 组内比较和排名

7.3 组内统计分析

8. 常见应用场景与实例

8.1 计算增长率和环比

8.2 识别连续模式

8.3 计算移动平均值

8.4 累计总和和占比分析

9. 新手常见疑问解答

9.1 窗口函数 vs. GROUP BY 的选择

9.2 性能优化建议

9.3 OVER() 的必要性

9.4 LAST_VALUE() 函数使用注意事项

9.5 窗口函数在 WHERE 中的限制

9.6 窗口函数中 ORDER BY 的特殊作用

10. 学习路径建议

10.1学习路径建议

总结


常用窗口函数速查表

函数类型函数名功能简述详细章节
排名函数ROW_NUMBER()分配唯一序号,不会重复4.1
RANK()相同值相同排名,会跳过排名4.2
DENSE_RANK()相同值相同排名,不跳过排名4.3
NTILE(n)将数据分成n个等大小的组4.4
聚合函数SUM()计算总和5.1
AVG()计算平均值5.1
COUNT()计算行数5.1
MIN()找出最小值5.1
MAX()找出最大值5.1
偏移函数LAG()获取前几行的值6.1
LEAD()获取后几行的值6.2
FIRST_VALUE()返回窗口中第一行的值6.3
LAST_VALUE()返回窗口中最后一行的值6.4
分布函数PERCENT_RANK()计算百分比排名8.4
CUME_DIST()计算累积分布值8.4

1. 什么是"窗口"(不是你想的那种窗口)

"窗口"≠电脑界面的窗口

首先,SQL 中的"窗口"不是像 Windows 操作系统上那种可以拖动的界面框!这是很多初学者的误解。

那么,SQL 中的"窗口"是什么?

在 SQL 中,"窗口"是指数据的可视范围。想象你站在一个队列中,通过一个"窗口",你能看到:

  1. 只看自己(普通查询)
  2. 看到整个队列(全局窗口)
  3. 看到同组的人(分组窗口)
  4. 看到排在你前面的人(排序窗口)

通俗解释:想象你站在一列人当中,一般情况下你只能看到自己的信息。但如果给你一个"窗口",你就能:

  • 看到整队人的信息(比如队伍的平均身高)
  • 看到你前面所有人的信息(比如你前面有多少人)
  • 看到你所在小组的信息(比如你所在小组的平均身高)

SQL 窗口函数中的"窗口"就是定义了当前行可以"看到"哪些其他行的规则。

用表格形式理解"窗口"概念

假设我们有一张学生成绩表:

ID | 姓名 | 班级 | 分数
---|------|------|-----
1  | 张三 | 一班 | 85
2  | 李四 | 一班 | 92
3  | 王五 | 一班 | 78
4  | 赵六 | 二班 | 88
5  | 钱七 | 二班 | 95
6  | 孙八 | 二班 | 80

普通查询:每行只能"看到"自己的信息

窗口函数:定义"窗口"后,每行可以看到窗口内其他行的信息:

  1. 全局窗口(所有行):每个学生都能看到所有人的数据

  2. 按班级分组的窗口

    • 一班的学生只能看到一班的信息(张三、李四、王五)
    • 二班的学生只能看到二班的信息(赵六、钱七、孙八)
  3. 按分数排序的累计窗口

    • 分数78的王五只能看到自己
    • 分数80的孙八能看到自己和王五
    • 分数85的张三能看到自己、孙八和王五
    • 以此类推...

通过这个比喻,你应该能够理解 SQL 中"窗口"的含义了——它定义了当前行可以"看到"并计算的数据范围。

2. 窗口函数解决了什么问题

在学习新概念前,理解它能解决什么问题很重要。让我们看看没有窗口函数时的痛点:

场景:学生成绩单

假设我们需要生成包含以下信息的成绩单:

  • 学生姓名和分数
  • 班级平均分
  • 学生在班级中的排名
  • 学生与班级平均分的差距

没有窗口函数时:需要多个查询和复杂的子查询/连接

-- 查询学生信息和班级平均分(复杂且性能差)
SELECT s.name, s.class, s.score,(SELECT AVG(score) FROM students WHERE class = s.class) AS class_avg
FROM students s;-- 单独查询排名还需要另一个复杂查询...

使用窗口函数:一个查询搞定所有

SELECT name, class, score,AVG(score) OVER(PARTITION BY class) AS class_avg,RANK() OVER(PARTITION BY class ORDER BY score DESC) AS class_rank,score - AVG(score) OVER(PARTITION BY class) AS diff_from_avg
FROM students;

一次查询就能获得所有需要的信息,代码更简洁,性能更好!

窗口函数的主要优势

  1. 保留原始数据的同时添加计算值(不像 GROUP BY 会减少行数)
  2. 简化复杂查询(避免自连接和子查询)
  3. 提高查询性能(通常比等效的多表查询更高效)
  4. 解决"看到相关行"的问题(如排名、累计、与平均值比较等)

3. 窗口函数基础语法详解

3.1 OVER 子句:窗口函数的核心

窗口函数的语法乍看有点复杂,让我们一步步拆解它:

函数名(<参数>) OVER ([PARTITION BY <分组列>][ORDER BY <排序列>][窗口框架子句]
)

OVER 是窗口函数的核心标志,告诉 MySQL:"这是一个窗口函数"。

类比:想象 OVER 就像眼镜,戴上它后你就能"看到"其他行的数据。

没有 OVER,普通聚合函数会将多行合并为一行:

SELECT AVG(score) FROM students; 
-- 只返回一个值:所有学生的平均分

加上 OVER,窗口函数会为每行计算一个结果:

SELECT name, score, AVG(score) OVER() FROM students; 
-- 返回每个学生的名字、分数,以及全部学生的平均分

3.2 PARTITION BY:分组但不合并

PARTITION BY 将数据分成多个独立的组(窗口),每组内分别计算。

类比:想象一个大教室被隔成小教室,每个小教室的学生只能看到自己教室内的情况。

SELECT name, class, score,AVG(score) OVER(PARTITION BY class) AS class_avg
FROM students;

这个查询分别计算每个班级的平均分,每个学生行都显示自己班级的平均分。

表格形式表示:

分区1(一班):
name  | class | score | class_avg
------|-------|-------|----------
张三  | 一班  | 85    | 85.00 ← 一班平均分
李四  | 一班  | 92    | 85.00 ← 一班平均分
王五  | 一班  | 78    | 85.00 ← 一班平均分分区2(二班):
name  | class | score | class_avg
------|-------|-------|----------
赵六  | 二班  | 88    | 87.67 ← 二班平均分
钱七  | 二班  | 95    | 87.67 ← 二班平均分
孙八  | 二班  | 80    | 87.67 ← 二班平均分

3.3 ORDER BY:窗口内的排序

窗口函数中的 ORDER BY 不仅决定显示顺序,更重要的是定义了"累计"计算的顺序。

类比:想象学生按成绩从低到高排队,站在队伍中间的学生可以看到自己前面的所有人。

SELECT name, score,SUM(score) OVER(ORDER BY score) AS running_total
FROM students;

这个查询计算"截至当前分数"的累计总分。对于第n个学生,running_total 包含前n个学生(按分数排序)的总分。

表格形式解析(按分数排序):

name  | score | running_total | 说明
------|-------|---------------|---------------
王五  | 78    | 78            | 只计算王五的分数
孙八  | 80    | 158           | 计算王五+孙八的分数
张三  | 85    | 243           | 计算王五+孙八+张三的分数
赵六  | 88    | 331           | 计算王五+孙八+张三+赵六的分数
李四  | 92    | 423           | 计算王五+孙八+张三+赵六+李四的分数
钱七  | 95    | 518           | 计算全部学生的分数

3.4 组合使用 PARTITION BY 和 ORDER BY

同时使用这两个子句,你可以实现"分组内的累计计算":

SELECT name, class, score,SUM(score) OVER(PARTITION BY class ORDER BY score) AS class_running_total
FROM students;

这会先按班级分组,然后在每个班级内部按分数排序计算累计总分。

表格形式解析(按班级分组,然后按分数排序):

分区1(一班):
name  | class | score | class_running_total | 说明
------|-------|-------|---------------------|---------------
王五  | 一班  | 78    | 78                  | 只计算王五的分数
张三  | 一班  | 85    | 163                 | 计算王五+张三的分数
李四  | 一班  | 92    | 255                 | 计算王五+张三+李四的分数分区2(二班):
name  | class | score | class_running_total | 说明
------|-------|-------|---------------------|---------------
孙八  | 二班  | 80    | 80                  | 只计算孙八的分数
赵六  | 二班  | 88    | 168                 | 计算孙八+赵六的分数
钱七  | 二班  | 95    | 263                 | 计算孙八+赵六+钱七的分数

实际演示:创建示例数据

CREATE TABLE students (id INT PRIMARY KEY,name VARCHAR(50),class VARCHAR(10),score INT
);INSERT INTO students VALUES
(1, '张三', '一班', 85),
(2, '李四', '一班', 92),
(3, '王五', '一班', 78),
(4, '赵六', '二班', 88),
(5, '钱七', '二班', 95),
(6, '孙八', '二班', 80);

4. 排名函数详解与实例

4.1 ROW_NUMBER() 函数:每行唯一序号

功能:为每一行分配一个唯一的序号,即使数值相同也会分配不同序号。

语法

ROW_NUMBER() OVER ([PARTITION BY 列名] ORDER BY 列名)

示例

SELECT name, class, score,ROW_NUMBER() OVER(ORDER BY score DESC) AS overall_rank,ROW_NUMBER() OVER(PARTITION BY class ORDER BY score DESC) AS class_rank
FROM students;

结果

name  | class | score | overall_rank | class_rank
------|-------|-------|--------------|----------
钱七  | 二班  | 95    | 1            | 1
李四  | 一班  | 92    | 2            | 1
赵六  | 二班  | 88    | 3            | 2
张三  | 一班  | 85    | 4            | 2
孙八  | 二班  | 80    | 5            | 3
王五  | 一班  | 78    | 6            | 3

应用场景

  • 需要唯一标识每一行
  • 分页查询(如每页显示10条记录)
  • 选取每个组中的第N行

4.2 RANK() 函数:相同值相同排名,会跳过排名

功能:为每行分配排名,相同值获得相同排名,但会跳过重复的排名数。

语法

RANK() OVER ([PARTITION BY 列名] ORDER BY 列名)

示例

-- 添加一些重复分数的学生
INSERT INTO students VALUES
(7, '周九', '一班', 85),  -- 和张三分数相同
(8, '吴十', '二班', 88);  -- 和赵六分数相同SELECT name, class, score,RANK() OVER(ORDER BY score DESC) AS overall_rank
FROM students;

结果

name  | class | score | overall_rank
------|-------|-------|-------------
钱七  | 二班  | 95    | 1
李四  | 一班  | 92    | 2
赵六  | 二班  | 88    | 3
吴十  | 二班  | 88    | 3
张三  | 一班  | 85    | 5
周九  | 一班  | 85    | 5
孙八  | 二班  | 80    | 7
王五  | 一班  | 78    | 8

注意:上面结果中,排名从3直接跳到5,因为第3名有两人。

应用场景

  • 体育比赛排名
  • 成绩排名,相同分数同名次,下一个名次顺延

4.3 DENSE_RANK() 函数:相同值相同排名,不跳过排名

功能:为每行分配排名,相同值获得相同排名,但不会跳过排名号。

语法

DENSE_RANK() OVER ([PARTITION BY 列名] ORDER BY 列名)

示例

SELECT name, class, score,DENSE_RANK() OVER(ORDER BY score DESC) AS dense_rank
FROM students;

结果

name  | class | score | dense_rank
------|-------|-------|----------
钱七  | 二班  | 95    | 1
李四  | 一班  | 92    | 2
赵六  | 二班  | 88    | 3
吴十  | 二班  | 88    | 3
张三  | 一班  | 85    | 4
周九  | 一班  | 85    | 4
孙八  | 二班  | 80    | 5
王五  | 一班  | 78    | 6

注意:排名是连续的,没有跳过的编号。

应用场景

  • 考试等级划分
  • 需要连续排名的场景

4.4 NTILE(n) 函数:将数据分成n个组

功能:将有序数据分为n个等大的组,并为每行分配其所在的组号(1到n)。

语法

NTILE(n) OVER ([PARTITION BY 列名] ORDER BY 列名)

示例

SELECT name, class, score,NTILE(4) OVER(ORDER BY score DESC) AS quartile
FROM students;

结果

name  | class | score | quartile
------|-------|-------|----------
钱七  | 二班  | 95    | 1
李四  | 一班  | 92    | 1
赵六  | 二班  | 88    | 2
吴十  | 二班  | 88    | 2
张三  | 一班  | 85    | 3
周九  | 一班  | 85    | 3
孙八  | 二班  | 80    | 4
王五  | 一班  | 78    | 4

应用场景

  • 将学生分为不同等级(四分位数、十分位数等)
  • 数据分桶
  • 将客户分为不同价值层级

4.5 排名函数实际应用案例

场景1:找出每个班级前两名

WITH RankedStudents AS (SELECT name, class, score,RANK() OVER(PARTITION BY class ORDER BY score DESC) AS class_rankFROM students
)
SELECT * FROM RankedStudents WHERE class_rank <= 2;

结果

name  | class | score | class_rank
------|-------|-------|----------
李四  | 一班  | 92    | 1
张三  | 一班  | 85    | 2
周九  | 一班  | 85    | 2
钱七  | 二班  | 95    | 1
赵六  | 二班  | 88    | 2
吴十  | 二班  | 88    | 2

场景2:根据分数划分等级

SELECT name, class, score,CASE WHEN NTILE(4) OVER(ORDER BY score DESC) = 1 THEN 'A'WHEN NTILE(4) OVER(ORDER BY score DESC) = 2 THEN 'B'WHEN NTILE(4) OVER(ORDER BY score DESC) = 3 THEN 'C'ELSE 'D'END AS grade
FROM students;

5. 聚合窗口函数实例解析

5.1 SUM()、AVG()、COUNT()、MIN()、MAX() 函数

功能:这些常用聚合函数在窗口函数中保留原始行,不会减少结果集。

语法

聚合函数(列名) OVER ([PARTITION BY 列名] [ORDER BY 列名])

示例

SELECT name,class, score,SUM(score) OVER(PARTITION BY class) AS class_total,AVG(score) OVER(PARTITION BY class) AS class_avg,COUNT(*) OVER(PARTITION BY class) AS class_count,MIN(score) OVER(PARTITION BY class) AS class_min,MAX(score) OVER(PARTITION BY class) AS class_max
FROM students;

结果

name  | class | score | class_total | class_avg | class_count | class_min | class_max
------|-------|-------|-------------|-----------|-------------|-----------|----------
张三  | 一班  | 85    | 340        | 85.00     | 4           | 78        | 92
李四  | 一班  | 92    | 340        | 85.00     | 4           | 78        | 92
王五  | 一班  | 78    | 340        | 85.00     | 4           | 78        | 92
周九  | 一班  | 85    | 340        | 85.00     | 4           | 78        | 92
赵六  | 二班  | 88    | 351        | 87.75     | 4           | 80        | 95
钱七  | 二班  | 95    | 351        | 87.75     | 4           | 80        | 95
孙八  | 二班  | 80    | 351        | 87.75     | 4           | 80        | 95
吴十  | 二班  | 88    | 351        | 87.75     | 4           | 80        | 95

5.2 窗口聚合 vs. GROUP BY 聚合

关键区别:窗口聚合保留所有原始行,而 GROUP BY 聚合会将多行合并为一行。

GROUP BY 示例

-- 使用 GROUP BY
SELECT class, AVG(score) AS avg_score
FROM students
GROUP BY class;

结果(只有2行):

class | avg_score
------|----------
一班  | 85.00
二班  | 87.75

窗口函数示例

-- 使用窗口函数
SELECT name,class, score,AVG(score) OVER(PARTITION BY class) AS avg_score
FROM students;

结果(保留所有8行):

name  | class | score | avg_score
------|-------|-------|----------
张三  | 一班  | 85    | 85.00
李四  | 一班  | 92    | 85.00
王五  | 一班  | 78    | 85.00
周九  | 一班  | 85    | 85.00
赵六  | 二班  | 88    | 87.75
钱七  | 二班  | 95    | 87.75
孙八  | 二班  | 80    | 87.75
吴十  | 二班  | 88    | 87.75

5.3 累计聚合计算

功能:当在聚合窗口函数中添加 ORDER BY 时,会计算"累计"或"截止到当前行"的聚合值。

示例

SELECT name, class, score,SUM(score) OVER(ORDER BY score) AS running_total,AVG(score) OVER(ORDER BY score) AS running_avg,COUNT(score) OVER(ORDER BY score) AS running_count
FROM students;

结果

name  | class | score | running_total | running_avg | running_count
------|-------|-------|---------------|-------------|-------------
王五  | 一班  | 78    | 78            | 78.00       | 1
孙八  | 二班  | 80    | 158           | 79.00       | 2
张三  | 一班  | 85    | 243           | 81.00       | 3
周九  | 一班  | 85    | 328           | 82.00       | 4
赵六  | 二班  | 88    | 416           | 83.20       | 5
吴十  | 二班  | 88    | 504           | 84.00       | 6
李四  | 一班  | 92    | 596           | 85.14       | 7
钱七  | 二班  | 95    | 691           | 86.38       | 8

5.4 分组内的累计聚合

功能:结合 PARTITION BY 和 ORDER BY,可以在每个组内部进行累计计算。

示例

SELECT name, class, score,SUM(score) OVER(PARTITION BY class ORDER BY score) AS class_running_total,COUNT(score) OVER(PARTITION BY class ORDER BY score) AS class_running_count
FROM students;

结果

name  | class | score | class_running_total | class_running_count
------|-------|-------|---------------------|-------------------
王五  | 一班  | 78    | 78                  | 1
张三  | 一班  | 85    | 163                 | 2
周九  | 一班  | 85    | 248                 | 3
李四  | 一班  | 92    | 340                 | 4
孙八  | 二班  | 80    | 80                  | 1
赵六  | 二班  | 88    | 168                 | 2
吴十  | 二班  | 88    | 256                 | 3
钱七  | 二班  | 95    | 351                 | 4

6.1 LAG() 函数:获取前几行的值

功能:返回当前行之前的行的值。

语法

LAG(列名, 偏移量, 默认值) OVER ([PARTITION BY 列名] ORDER BY 列名)

参数

  • 列名:要获取的列
  • 偏移量:前面第几行(默认为1)
  • 默认值:找不到前面行时返回的值(默认为NULL)

示例

SELECT name, class, score,LAG(score, 1, 0) OVER(ORDER BY score) AS prev_score,  -- 前一个学生的分数score - LAG(score, 1, 0) OVER(ORDER BY score) AS diff_from_prev  -- 与前一个学生的分数差
FROM students;

结果

name  | class | score | prev_score | diff_from_prev
------|-------|-------|------------|---------------
王五  | 一班  | 78    | 0          | 78
孙八  | 二班  | 80    | 78         | 2
张三  | 一班  | 85    | 80         | 5
周九  | 一班  | 85    | 85         | 0
赵六  | 二班  | 88    | 85         | 3
吴十  | 二班  | 88    | 88         | 0
李四  | 一班  | 92    | 88         | 4
钱七  | 二班  | 95    | 92         | 3

应用场景

  • 计算环比增长率
  • 比较相邻记录的差异
  • 识别连续变化模式

6.2 LEAD() 函数:获取后几行的值

功能:返回当前行之后的行的值。

语法

LEAD(列名, 偏移量, 默认值) OVER ([PARTITION BY 列名] ORDER BY 列名)

参数

  • 列名:要获取的列
  • 偏移量:后面第几行(默认为1)
  • 默认值:找不到后面行时返回的值(默认为NULL)

示例

SELECT name, class, score,LEAD(score, 1, 0) OVER(ORDER BY score) AS next_score,  -- 后一个学生的分数LEAD(score, 1, 0) OVER(ORDER BY score) - score AS diff_to_next  -- 与后一个学生的分数差
FROM students;

结果

name  | class | score | next_score | diff_to_next
------|-------|-------|------------|-------------
王五  | 一班  | 78    | 80         | 2
孙八  | 二班  | 80    | 85         | 5
张三  | 一班  | 85    | 85         | 0
周九  | 一班  | 85    | 88         | 3
赵六  | 二班  | 88    | 88         | 0
吴十  | 二班  | 88    | 92         | 4
李四  | 一班  | 92    | 95         | 3
钱七  | 二班  | 95    | 0          | -95

应用场景

  • 预测下一期值
  • 计算未来趋势
  • 分析序列间隔

6.3 FIRST_VALUE() 函数:窗口中第一行的值

功能:返回窗口框架中第一行的值。

语法

FIRST_VALUE(列名) OVER ([PARTITION BY 列名] ORDER BY 列名 [窗口框架])

示例

SELECT name, class, score,FIRST_VALUE(score) OVER(PARTITION BY class ORDER BY score) AS min_score_in_class
FROM students;

结果

name  | class | score | min_score_in_class
------|-------|-------|------------------
王五  | 一班  | 78    | 78
张三  | 一班  | 85    | 78
周九  | 一班  | 85    | 78
李四  | 一班  | 92    | 78
孙八  | 二班  | 80    | 80
赵六  | 二班  | 88    | 80
吴十  | 二班  | 88    | 80
钱七  | 二班  | 95    | 80

应用场景

  • 获取组内最小值
  • 获取序列第一个值
  • 计算与基准值的差异

6.4 LAST_VALUE() 函数:窗口中最后一行的值

功能:返回窗口框架中最后一行的值。

语法

LAST_VALUE(列名) OVER ([PARTITION BY 列名] ORDER BY 列名 [窗口框架])

重要提示:LAST_VALUE 默认窗口框架是"从头至当前行",要获取真正的最后值,需要显式设置窗口框架为整个分区。

示例

SELECT name, class, score,LAST_VALUE(score) OVER(PARTITION BY class ORDER BY score RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS max_score_in_class
FROM students;

结果

name  | class | score | max_score_in_class
------|-------|-------|------------------
王五  | 一班  | 78    | 92
张三  | 一班  | 85    | 92
周九  | 一班  | 85    | 92
李四  | 一班  | 92    | 92
孙八  | 二班  | 80    | 95
赵六  | 二班  | 88    | 95
吴十  | 二班  | 88    | 95
钱七  | 二班  | 95    | 95

常见错误:没有明确指定窗口框架时的 LAST_VALUE 结果不符合预期。

-- 错误示例(不要这样用)
SELECT name, class, score,LAST_VALUE(score) OVER(PARTITION BY class ORDER BY score) AS wrong_last_value
FROM students;

问题:此时 LAST_VALUE 只能"看到"当前行及之前的行,所以每行的"最后值"就是当前行的值。

应用场景

  • 获取组内最大值
  • 获取序列最后一个值
  • 计算与终点值的差异

6.5 偏移函数分组应用

功能:结合 PARTITION BY,可以在组内使用偏移函数。

示例

SELECT name, class, score,LAG(name) OVER(PARTITION BY class ORDER BY score) AS prev_student,LAG(score) OVER(PARTITION BY class ORDER BY score) AS prev_score,score - LAG(score) OVER(PARTITION BY class ORDER BY score) AS diff_in_class
FROM students;

结果

name  | class | score | prev_student | prev_score | diff_in_class
------|-------|-------|--------------|------------|--------------
王五  | 一班  | 78    | NULL         | NULL       | NULL
张三  | 一班  | 85    | 王五         | 78         | 7
周九  | 一班  | 85    | 张三         | 85         | 0
李四  | 一班  | 92    | 周九         | 85         | 7
孙八  | 二班  | 80    | NULL         | NULL       | NULL
赵六  | 二班  | 88    | 孙八         | 80         | 8
吴十  | 二班  | 88    | 赵六         | 88         | 0
钱七  | 二班  | 95    | 吴十         | 88         | 7

应用场景

  • 组内相邻记录比较
  • 分析组内连续变化
  • 计算组内差异

7. 分组与排序的结合使用

7.1 找出每组最值

场景:找出每个学生的最佳科目。

准备数据

-- 创建考试成绩表
CREATE TABLE exam_scores (student_id INT,student_name VARCHAR(50),subject VARCHAR(50),score INT
);INSERT INTO exam_scores VALUES
(1, '张三', '数学', 85),
(1, '张三', '语文', 78),
(1, '张三', '英语', 92),
(2, '李四', '数学', 90),
(2, '李四', '语文', 85),
(2, '李四', '英语', 80),
(3, '王五', '数学', 78),
(3, '王五', '语文', 82),
(3, '王五', '英语', 88);

实现

WITH RankedSubjects AS (SELECT student_name,subject,score,RANK() OVER(PARTITION BY student_id ORDER BY score DESC) AS subject_rankFROM exam_scores
)
SELECT student_name,subject AS best_subject,score AS best_score
FROM RankedSubjects
WHERE subject_rank = 1;

结果

student_name | best_subject | best_score
-------------|-------------|-----------
张三         | 英语        | 92
李四         | 数学        | 90
王五         | 英语        | 88

7.2 组内比较和排名

场景:计算每个学生在各科目中的排名。

实现

SELECT student_name,subject,score,RANK() OVER(PARTITION BY subject ORDER BY score DESC) AS subject_rank
FROM exam_scores;

结果

student_name | subject | score | subject_rank
-------------|---------|-------|-------------
李四         | 数学    | 90    | 1
张三         | 数学    | 85    | 2
王五         | 数学    | 78    | 3
李四         | 语文    | 85    | 1
王五         | 语文    | 82    | 2
张三         | 语文    | 78    | 3
张三         | 英语    | 92    | 1
王五         | 英语    | 88    | 2
李四         | 英语    | 80    | 3

7.3 组内统计分析

场景:计算每个学生的总分、平均分和各科与平均分差异。

实现

SELECT student_name,SUM(score) OVER(PARTITION BY student_id) AS total_score,AVG(score) OVER(PARTITION BY student_id) AS avg_score,subject,score,score - AVG(score) OVER(PARTITION BY student_id) AS diff_from_avg
FROM exam_scores;

结果

student_name | total_score | avg_score | subject | score | diff_from_avg
-------------|------------|-----------|---------|-------|---------------
张三         | 255        | 85.00     | 数学    | 85    | 0.00
张三         | 255        | 85.00     | 语文    | 78    | -7.00
张三         | 255        | 85.00     | 英语    | 92    | 7.00
李四         | 255        | 85.00     | 数学    | 90    | 5.00
李四         | 255        | 85.00     | 语文    | 85    | 0.00
李四         | 255        | 85.00     | 英语    | 80    | -5.00
王五         | 248        | 82.67     | 数学    | 78    | -4.67
王五         | 248        | 82.67     | 语文    | 82    | -0.67
王五         | 248        | 82.67     | 英语    | 88    | 5.33

8. 常见应用场景与实例

8.1 计算增长率和环比

场景:计算月度销售的环比增长率。

准备数据

CREATE TABLE monthly_sales (month_date DATE,sales_amount DECIMAL(10, 2)
);INSERT INTO monthly_sales VALUES
('2023-01-01', 10000),
('2023-02-01', 12000),
('2023-03-01', 11500),
('2023-04-01', 13200),
('2023-05-01', 14500),
('2023-06-01', 14000);

实现

SELECT month_date,sales_amount,LAG(sales_amount) OVER(ORDER BY month_date) AS prev_month_sales,sales_amount - LAG(sales_amount) OVER(ORDER BY month_date) AS amount_change,ROUND((sales_amount - LAG(sales_amount) OVER(ORDER BY month_date)) / LAG(sales_amount) OVER(ORDER BY month_date) * 100, 2) AS growth_pct
FROM monthly_sales;

结果

month_date | sales_amount | prev_month_sales | amount_change | growth_pct
-----------|--------------|------------------|---------------|----------
2023-01-01 | 10000.00     | NULL             | NULL          | NULL
2023-02-01 | 12000.00     | 10000.00         | 2000.00       | 20.00
2023-03-01 | 11500.00     | 12000.00         | -500.00       | -4.17
2023-04-01 | 13200.00     | 11500.00         | 1700.00       | 14.78
2023-05-01 | 14500.00     | 13200.00         | 1300.00       | 9.85
2023-06-01 | 14000.00     | 14500.00         | -500.00       | -3.45

8.2 识别连续模式

场景:找出连续登录至少3天的用户。

准备数据

CREATE TABLE user_logins (user_id INT,login_date DATE
);INSERT INTO user_logins VALUES
(101, '2023-01-01'),
(101, '2023-01-02'),
(101, '2023-01-03'),
(101, '2023-01-05'),
(102, '2023-01-01'),
(102, '2023-01-03'),
(102, '2023-01-04'),
(103, '2023-01-01'),
(103, '2023-01-02'),
(103, '2023-01-03'),
(103, '2023-01-04');

实现

WITH LoginData AS (SELECT user_id,login_date,ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY login_date) AS login_seq,DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY login_date) DAY) AS date_groupFROM user_logins
),
ConsecutiveLogins AS (SELECT user_id,date_group,COUNT(*) AS consecutive_daysFROM LoginDataGROUP BY user_id, date_group
)
SELECT user_id,MAX(consecutive_days) AS max_consecutive_days
FROM ConsecutiveLogins
GROUP BY user_id
HAVING max_consecutive_days >= 3;

结果

user_id | max_consecutive_days
--------|---------------------
101     | 3
103     | 4

8.3 计算移动平均值

场景:计算销售额的3个月移动平均值。

实现

SELECT month_date,sales_amount,AVG(sales_amount) OVER(ORDER BY month_dateROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS moving_avg_3months
FROM monthly_sales;

结果

month_date | sales_amount | moving_avg_3months
-----------|--------------|--------------------
2023-01-01 | 10000.00     | 11000.00
2023-02-01 | 12000.00     | 11166.67
2023-03-01 | 11500.00     | 12233.33
2023-04-01 | 13200.00     | 13066.67
2023-05-01 | 14500.00     | 13900.00
2023-06-01 | 14000.00     | 14250.00

8.4 累计总和和占比分析

场景:计算累计销售额和每月销售占比。

实现

SELECT month_date,sales_amount,SUM(sales_amount) OVER(ORDER BY month_date) AS cumulative_sales,ROUND(sales_amount / SUM(sales_amount) OVER() * 100,2) AS pct_of_total
FROM monthly_sales;

结果

month_date | sales_amount | cumulative_sales | pct_of_total
-----------|--------------|------------------|-------------
2023-01-01 | 10000.00     | 10000.00         | 13.33
2023-02-01 | 12000.00     | 22000.00         | 16.00
2023-03-01 | 11500.00     | 33500.00         | 15.33
2023-04-01 | 13200.00     | 46700.00         | 17.60
2023-05-01 | 14500.00     | 61200.00         | 19.33
2023-06-01 | 14000.00     | 75200.00         | 18.67

9. 新手常见疑问解答

9.1 窗口函数 vs. GROUP BY 的选择

问题:窗口函数和 GROUP BY 什么时候选择哪个?

答案

  • 使用 GROUP BY 的场景:
    • 需要聚合数据,减少结果行数
    • 只需要每组的聚合结果,不需要原始行
  • 使用窗口函数的场景:
    • 需要保留原始记录同时添加计算值
    • 需要计算排名或累计值
    • 需要比较每行与组内其他行的关系

选择指南

需要聚合,减少行数 → 使用 GROUP BY
需要保留原始行 → 使用窗口函数
需要排名、累计、组内比较 → 使用窗口函数

9.2 性能优化建议

问题:为什么我的窗口函数查询很慢?

答案

  • 窗口函数可能需要对数据进行排序和重复计算,大数据集上可能较慢
  • 优化建议:
    1. 确保 PARTITION BY 和 ORDER BY 列上有适当的索引
    2. 考虑将大型窗口函数查询拆分为多个步骤,使用临时表或 CTE
    3. 尽量避免对大数据集使用"ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING"
    4. 尝试减小窗口框架大小,如使用"ROWS BETWEEN 10 PRECEDING AND CURRENT ROW"
    5. 先过滤数据,减少处理行数

9.3 OVER() 的必要性

问题:为什么 OVER() 是必需的?

答案

  • OVER() 是窗口函数的标识符,告诉 MySQL 这是一个窗口函数
  • 它定义了计算的"窗口"(数据范围)
  • 即使不需要分区或排序,也需要空的 OVER()
  • 没有 OVER(),函数将作为普通聚合函数处理,会压缩结果

对比

-- 返回一行(普通聚合)
SELECT AVG(score) FROM students;-- 返回多行(窗口函数)
SELECT name, score, AVG(score) OVER() FROM students;

9.4 LAST_VALUE() 函数使用注意事项

问题:为什么我的 LAST_VALUE() 结果看起来不正确?

答案

  • LAST_VALUE() 默认窗口框架是"RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW"
  • 这意味着它只看到"截至当前行"的数据,不是整个分区的最后一行
  • 解决方法:明确指定窗口框架为"RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING"

正确用法

-- 正确使用 LAST_VALUE
SELECT name, class, score,LAST_VALUE(score) OVER(PARTITION BY class ORDER BY score RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS max_score_in_class
FROM students;

9.5 窗口函数在 WHERE 中的限制

问题:窗口函数能在 WHERE 子句中使用吗?

答案

  • 不能。窗口函数不能在 WHERE 子句中直接使用
  • 原因:窗口函数在 SQL 处理逻辑中发生在 WHERE 过滤之后
  • 解决方法:使用子查询或 CTE 将窗口函数结果包装起来,然后在外层查询中使用 WHERE

错误示例

-- 错误示例(会报错)
SELECT name, score FROM students
WHERE RANK() OVER(ORDER BY score DESC) <= 3;

正确示例

-- 正确示例
WITH RankedStudents AS (SELECT name, score,RANK() OVER(ORDER BY score DESC) AS score_rankFROM students
)
SELECT name, score FROM RankedStudents
WHERE score_rank <= 3;

9.6 窗口函数中 ORDER BY 的特殊作用

问题:ORDER BY 在窗口函数中是做什么用的?和普通 ORDER BY 有什么区别?

答案

  • 普通的 ORDER BY:只影响结果的显示顺序,不影响计算
  • 窗口函数中的 ORDER BY:定义了累计计算的顺序,直接影响计算结果

区别示例

-- 窗口函数中的 ORDER BY 影响计算结果
SELECT name, score, SUM(score) OVER(ORDER BY score) AS running_total_by_score,SUM(score) OVER(ORDER BY name) AS running_total_by_name
FROM students;

这两个计算结果会完全不同,因为累计顺序不同!

10. 学习路径建议

10.1学习路径建议

基础阶段

  • 掌握基本语法:OVER(), PARTITION BY, ORDER BY
  • 熟悉基本函数:ROW_NUMBER(), RANK(), SUM(), AVG()
  • 练习简单场景:排名、累计、分组平均值

进阶阶段

  • 学习偏移函数:LAG(), LEAD(), FIRST_VALUE(), LAST_VALUE()
  • 理解窗口框架:ROWS BETWEEN...AND...
  • 结合 CTE 和其他 SQL 功能使用窗口函数

高级阶段

  • 解决复杂业务场景:连续值检测、间隙填充等
  • 优化窗口函数性能
  • 将窗口函数与其他高级 SQL 功能结合使用

总结

窗口函数是 MySQL 8.0+ 中的强大功能,可以解决许多传统 SQL 难以处理的问题。通过本教程,你应该能理解:

  1. 窗口是数据的可视范围,定义当前行可以"看到"哪些其他行
  2. 窗口函数保留原始行,同时添加计算值,不同于合并行的 GROUP BY
  3. OVER() 子句是必需的,定义了"窗口"(计算范围)
  4. PARTITION BY 将数据分组,类似于 GROUP BY,但不合并行
  5. ORDER BY 在窗口函数中影响计算,特别是排名和累计函数
  6. 排名函数(ROW_NUMBER, RANK, DENSE_RANK)各有不同用途
  7. 聚合窗口函数可以计算组内总计或累计值
  8. 偏移函数可以访问前后行的值,便于数据比较

掌握窗口函数会让你的 SQL 技能更上一层楼,能够更优雅地解决复杂的分析问题!

相关文章:

  • Coding Practice,48天强训(30)
  • 泰迪杯特等奖案例学习资料:基于卷积神经网络与集成学习的网络问政平台留言文本挖掘与分析
  • 网页截图指南
  • 存储系列知识
  • k8s node 报IPVS no destination available
  • Vue3+ Vite + Element-Plus + TypeScript 从0到1搭建
  • 卡特兰数--
  • 25_05_02Linux架构篇、第1章_03安装部署nginx
  • 【爬虫】码上爬第6题-倚天剑
  • 静态库和动态库的区别
  • SQL Server执行安装python环境
  • 用OMS从MySQL迁移到OceanBase,字符集utf8与utf8mb4的差异
  • Python实例题:高德API+Python解决租房问题
  • 室内烟雾明火检测数据集VOC+YOLO格式2469张2类别
  • 驱动开发系列57 - Linux Graphics QXL显卡驱动代码分析(四)显示区域绘制
  • 【专家库】Kuntal Chowdhury
  • 【挖洞利器】GobyAwvs解放双手
  • 基站综合测试仪核心功能详解:从射频参数到5G协议测试实战指南
  • RabbitMQ-api开发
  • 天文探秘学习小结
  • 外交部回应中美经贸高层会谈:这次会谈是应美方请求举行的
  • 当年的你,现在在哪里?——新民晚报杯40周年寻人启事
  • 过半中国上市公司去年都在“扩编”,哪些公司人效最高
  • 李云泽:支持设立新的金融资产投资公司,今天即将批复一家
  • 多个“网约摩托车”平台上线,工人日报:安全与监管不能掉队
  • 上海市政府党组会议传达学习习近平总书记重要讲话精神,部署抓好学习贯彻落实