MySQL 窗口函数入门到精通

目录

常用窗口函数速查表

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

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

[那么,SQL 中的"窗口"是什么?](#那么,SQL 中的"窗口"是什么?)

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

[2. 窗口函数解决了什么问题](#2. 窗口函数解决了什么问题)

场景:学生成绩单

窗口函数的主要优势

[3. 窗口函数基础语法详解](#3. 窗口函数基础语法详解)

[3.1 OVER 子句:窗口函数的核心](#3.1 OVER 子句:窗口函数的核心)

[3.2 PARTITION BY:分组但不合并](#3.2 PARTITION BY:分组但不合并)

[3.3 ORDER BY:窗口内的排序](#3.3 ORDER BY:窗口内的排序)

[3.4 组合使用 PARTITION BY 和 ORDER BY](#3.4 组合使用 PARTITION BY 和 ORDER BY)

实际演示:创建示例数据

[4. 排名函数详解与实例](#4. 排名函数详解与实例)

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

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

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

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

[4.5 排名函数实际应用案例](#4.5 排名函数实际应用案例)

[5. 聚合窗口函数实例解析](#5. 聚合窗口函数实例解析)

[5.1 SUM()、AVG()、COUNT()、MIN()、MAX() 函数](#5.1 SUM()、AVG()、COUNT()、MIN()、MAX() 函数)

[5.2 窗口聚合 vs. GROUP BY 聚合](#5.2 窗口聚合 vs. GROUP BY 聚合)

[5.3 累计聚合计算](#5.3 累计聚合计算)

[5.4 分组内的累计聚合](#5.4 分组内的累计聚合)

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

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

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

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

[6.5 偏移函数分组应用](#6.5 偏移函数分组应用)

[7. 分组与排序的结合使用](#7. 分组与排序的结合使用)

[7.1 找出每组最值](#7.1 找出每组最值)

[7.2 组内比较和排名](#7.2 组内比较和排名)

[7.3 组内统计分析](#7.3 组内统计分析)

[8. 常见应用场景与实例](#8. 常见应用场景与实例)

[8.1 计算增长率和环比](#8.1 计算增长率和环比)

[8.2 识别连续模式](#8.2 识别连续模式)

[8.3 计算移动平均值](#8.3 计算移动平均值)

[8.4 累计总和和占比分析](#8.4 累计总和和占比分析)

[9. 新手常见疑问解答](#9. 新手常见疑问解答)

[9.1 窗口函数 vs. GROUP BY 的选择](#9.1 窗口函数 vs. GROUP BY 的选择)

[9.2 性能优化建议](#9.2 性能优化建议)

[9.3 OVER() 的必要性](#9.3 OVER() 的必要性)

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

[9.5 窗口函数在 WHERE 中的限制](#9.5 窗口函数在 WHERE 中的限制)

[9.6 窗口函数中 ORDER BY 的特殊作用](#9.6 窗口函数中 ORDER BY 的特殊作用)

[10. 学习路径建议](#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 窗口函数中的"窗口"就是定义了当前行可以"看到"哪些其他行的规则。

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

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

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

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

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

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

  2. 按班级分组的窗口

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

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

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

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

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

场景:学生成绩单

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

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

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

sql 复制代码
-- 查询学生信息和班级平均分(复杂且性能差)
SELECT 
    s.name, 
    s.class, 
    s.score,
    (SELECT AVG(score) FROM students WHERE class = s.class) AS class_avg
FROM students s;

-- 单独查询排名还需要另一个复杂查询...

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

sql 复制代码
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 子句:窗口函数的核心

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

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

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

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

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

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

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

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

3.2 PARTITION BY:分组但不合并

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

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

sql 复制代码
SELECT 
    name, 
    class, 
    score,
    AVG(score) OVER(PARTITION BY class) AS class_avg
FROM students;

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

表格形式表示:

sql 复制代码
分区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 不仅决定显示顺序,更重要的是定义了"累计"计算的顺序。

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

sql 复制代码
SELECT 
    name, 
    score,
    SUM(score) OVER(ORDER BY score) AS running_total
FROM students;

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

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

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

3.4 组合使用 PARTITION BY 和 ORDER BY

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

sql 复制代码
SELECT 
    name, 
    class, 
    score,
    SUM(score) OVER(PARTITION BY class ORDER BY score) AS class_running_total
FROM students;

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

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

sql 复制代码
分区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                 | 计算孙八+赵六+钱七的分数

实际演示:创建示例数据

sql 复制代码
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() 函数:每行唯一序号

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

语法

sql 复制代码
ROW_NUMBER() OVER ([PARTITION BY 列名] ORDER BY 列名)

示例

sql 复制代码
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;

结果

sql 复制代码
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() 函数:相同值相同排名,会跳过排名

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

语法

sql 复制代码
RANK() OVER ([PARTITION BY 列名] ORDER BY 列名)

示例

sql 复制代码
-- 添加一些重复分数的学生
INSERT INTO students VALUES
(7, '周九', '一班', 85),  -- 和张三分数相同
(8, '吴十', '二班', 88);  -- 和赵六分数相同

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

结果

sql 复制代码
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() 函数:相同值相同排名,不跳过排名

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

语法

sql 复制代码
DENSE_RANK() OVER ([PARTITION BY 列名] ORDER BY 列名)

示例

sql 复制代码
SELECT 
    name, 
    class, 
    score,
    DENSE_RANK() OVER(ORDER BY score DESC) AS dense_rank
FROM students;

结果

sql 复制代码
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)。

语法

sql 复制代码
NTILE(n) OVER ([PARTITION BY 列名] ORDER BY 列名)

示例

sql 复制代码
SELECT 
    name, 
    class, 
    score,
    NTILE(4) OVER(ORDER BY score DESC) AS quartile
FROM students;

结果

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

应用场景

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

4.5 排名函数实际应用案例

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

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

结果

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

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

sql 复制代码
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() 函数

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

语法

sql 复制代码
聚合函数(列名) OVER ([PARTITION BY 列名] [ORDER BY 列名])

示例

sql 复制代码
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;

结果

sql 复制代码
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 示例

sql 复制代码
-- 使用 GROUP BY
SELECT 
    class, 
    AVG(score) AS avg_score
FROM students
GROUP BY class;

结果(只有2行):

sql 复制代码
class | avg_score
------|----------
一班  | 85.00
二班  | 87.75

窗口函数示例

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

结果(保留所有8行):

sql 复制代码
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 时,会计算"累计"或"截止到当前行"的聚合值。

示例

sql 复制代码
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;

结果

sql 复制代码
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,可以在每个组内部进行累计计算。

示例

sql 复制代码
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;

结果

sql 复制代码
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() 函数:获取前几行的值

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

语法

sql 复制代码
LAG(列名, 偏移量, 默认值) OVER ([PARTITION BY 列名] ORDER BY 列名)

参数

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

示例

sql 复制代码
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;

结果

sql 复制代码
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() 函数:获取后几行的值

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

语法

sql 复制代码
LEAD(列名, 偏移量, 默认值) OVER ([PARTITION BY 列名] ORDER BY 列名)

参数

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

示例

sql 复制代码
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;

结果

sql 复制代码
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() 函数:窗口中第一行的值

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

语法

sql 复制代码
FIRST_VALUE(列名) OVER ([PARTITION BY 列名] ORDER BY 列名 [窗口框架])

示例

sql 复制代码
SELECT 
    name, 
    class, 
    score,
    FIRST_VALUE(score) OVER(PARTITION BY class ORDER BY score) AS min_score_in_class
FROM students;

结果

sql 复制代码
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() 函数:窗口中最后一行的值

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

语法

sql 复制代码
LAST_VALUE(列名) OVER ([PARTITION BY 列名] ORDER BY 列名 [窗口框架])

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

示例

sql 复制代码
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;

结果

sql 复制代码
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 结果不符合预期。

sql 复制代码
-- 错误示例(不要这样用)
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,可以在组内使用偏移函数。

示例

sql 复制代码
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;

结果

sql 复制代码
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 找出每组最值

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

准备数据

sql 复制代码
-- 创建考试成绩表
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);

实现

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

结果

sql 复制代码
student_name | best_subject | best_score
-------------|-------------|-----------
张三         | 英语        | 92
李四         | 数学        | 90
王五         | 英语        | 88

7.2 组内比较和排名

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

实现

sql 复制代码
SELECT 
    student_name,
    subject,
    score,
    RANK() OVER(PARTITION BY subject ORDER BY score DESC) AS subject_rank
FROM exam_scores;

结果

sql 复制代码
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 组内统计分析

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

实现

sql 复制代码
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;

结果

sql 复制代码
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 计算增长率和环比

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

准备数据

sql 复制代码
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);

实现

sql 复制代码
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;

结果

sql 复制代码
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天的用户。

准备数据

sql 复制代码
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');

实现

sql 复制代码
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_group
    FROM user_logins
),
ConsecutiveLogins AS (
    SELECT 
        user_id,
        date_group,
        COUNT(*) AS consecutive_days
    FROM LoginData
    GROUP 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;

结果

sql 复制代码
user_id | max_consecutive_days
--------|---------------------
101     | 3
103     | 4

8.3 计算移动平均值

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

实现

sql 复制代码
SELECT 
    month_date,
    sales_amount,
    AVG(sales_amount) OVER(
        ORDER BY month_date
        ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
    ) AS moving_avg_3months
FROM monthly_sales;

结果

sql 复制代码
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 累计总和和占比分析

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

实现

sql 复制代码
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;

结果

sql 复制代码
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 的场景:
    • 需要聚合数据,减少结果行数
    • 只需要每组的聚合结果,不需要原始行
  • 使用窗口函数的场景:
    • 需要保留原始记录同时添加计算值
    • 需要计算排名或累计值
    • 需要比较每行与组内其他行的关系

选择指南

sql 复制代码
需要聚合,减少行数 → 使用 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(),函数将作为普通聚合函数处理,会压缩结果

对比

sql 复制代码
-- 返回一行(普通聚合)
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"

正确用法

sql 复制代码
-- 正确使用 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

错误示例

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

正确示例

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

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

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

答案

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

区别示例

sql 复制代码
-- 窗口函数中的 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 技能更上一层楼,能够更优雅地解决复杂的分析问题!

相关推荐
文牧之14 分钟前
PostgreSQL 查询历史最大进程数方法
运维·数据库·postgresql
楠木s22 分钟前
常见汇编代码及其指定
java·汇编·数据库·安全·网络攻击模型·二进制·栈溢出
A旧城以西1 小时前
MySQL----数据库的操作
java·开发语言·数据库·sql·学习·mysql
帅次2 小时前
Flutter TabBar / TabBarView 详解
android·flutter·ios·小程序·iphone·taro·reactnative
极小狐2 小时前
极狐Gitlab 如何创建并使用子群组?
数据库·人工智能·git·机器学习·gitlab
可喜~可乐5 小时前
SQLite数据类型
数据库·sql·sqlite·c#
学也不会5 小时前
d202552-sql
数据库·sql
zizisuo7 小时前
2.Redis高阶实战
数据库·redis·缓存
苹果酱05677 小时前
【Azure Redis】Redis导入备份文件(RDB)失败的原因
java·vue.js·spring boot·mysql·课程设计
每次的天空7 小时前
Android第六次面试总结之Java设计模式(二)
android·java·面试