首页 / MySQL 入门教程 / 窗口函数

MySQL 入门教程

窗口函数

本教程共 46 篇 · 第 29 篇 · 更新于 2026-07-30 · 约 14 分钟阅读

MySQLMySQL 入门教程窗口函数Window FunctionsROW_NUMBERRANKDENSE_RANKLAGLEAD

29. 窗口函数

本节目标:理解窗口函数的概念和原理,掌握 OVER() 子句的用法(PARTITION BY 分区、ORDER BY 排序、ROWS/RANGE 帧定义),学会 ROW_NUMBER、RANK、DENSE_RANK、LAG、LEAD、聚合函数 OVER 等常用窗口函数,能解决排名、累计求和、环比同比等实际问题。

Warning

窗口函数是 MySQL 8.0+ 才支持的功能。5.7 及更早版本不支持,会报语法错误。本章所有示例基于 MySQL 26.7.0,在 8.0+ 环境下均可运行。
参考文档:MySQL 8.0 Window Functions

29.1 什么是窗口函数

前面学的聚合函数(COUNT、SUM、AVG 等)会把多行”压缩”成一行。比如 GROUP BY status 后,每种状态只有一行结果。

窗口函数不同。它也在多行上做计算,但不压缩行数—每一行都有自己的计算结果。

说白了,窗口函数就是”在不减少行数的前提下,给每行附加一个计算值”。这个计算值是基于一个”窗口”(一组相关行)算出来的。

一个直观的例子:给每个订单加上”这个用户所有订单的平均金额”:

-- 聚合函数 + GROUP BY:每个用户一行,丢失了订单明细
SELECT user_id, AVG(amount) FROM orders GROUP BY user_id;

-- 窗口函数:每行订单都保留,同时附带该用户的平均金额
SELECT
    id,
    user_id,
    amount,
    AVG(amount) OVER (PARTITION BY user_id) AS 用户平均金额
FROM orders;

结果每行都保留了原始数据,同时多了一列”用户平均金额”。这就是窗口函数的价值。

29.2 OVER() 子句

OVER() 是窗口函数的核心,它定义了”窗口”—在哪些行上做计算。

空 OVER():全局窗口

SELECT
    id,
    amount,
    AVG(amount) OVER() AS 全局平均金额
FROM orders;

空的 OVER() 表示窗口是整张表。每行的”全局平均金额”都是所有订单的平均值。

PARTITION BY:分区

PARTITION BY 把数据分成多个区(类似 GROUP BY),每个区独立计算:

SELECT
    id,
    user_id,
    amount,
    AVG(amount) OVER(PARTITION BY user_id) AS 用户平均金额
FROM orders;

按 user_id 分区,每个用户的订单各自计算平均值。张三的行显示张三的平均金额,李四的行显示李四的平均金额。

ORDER BY:区内排序

ORDER BY 在分区内排序,通常配合序号函数和累计计算使用:

SELECT
    id,
    user_id,
    amount,
    ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY amount DESC) AS 金额排名
FROM orders;

按 user_id 分区,区内按 amount 降序排,给每行编号。结果就是每个用户的订单按金额从大到小排名。

Note

PARTITION BY 和 GROUP BY 的区别:GROUP BY 把每组压缩成一行,PARTITION BY 不压缩行数。你可以理解为 PARTITION BY 是”分组但保留所有行”。

29.3 序号函数:ROW_NUMBER / RANK / DENSE_RANK

这三个是最常用的窗口函数,都用于排名,但处理并列的方式不同。

ROW_NUMBER:行号

给每行分配一个连续的序号,不管有没有并列:

SELECT
    user_id,
    amount,
    ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY amount DESC) AS rn
FROM orders;

每个用户的订单按金额降序排,从 1 开始编号。即使两行金额相同,序号也不同(1、2、3…)。

RANK:排名(跳号)

有并列时,相同值给相同排名,下一个排名跳过:

SELECT
    user_id,
    amount,
    RANK() OVER(PARTITION BY user_id ORDER BY amount DESC) AS rank_num
FROM orders;

如果第一名有两个(金额相同),都是 rank 1,下一个是 rank 3(跳过 2)。

DENSE_RANK:密集排名(不跳号)

有并列时,相同值给相同排名,下一个排名不跳:

SELECT
    user_id,
    amount,
    DENSE_RANK() OVER(PARTITION BY user_id ORDER BY amount DESC) AS dense_rank_num
FROM orders;

如果第一名有两个(金额相同),都是 dense_rank 1,下一个是 dense_rank 2(不跳)。

三者对比

假设金额从大到小是:500, 500, 300, 200

金额ROW_NUMBERRANKDENSE_RANK
500111
500211
300332
200443
Tip

选择哪个取决于需求:要唯一序号用 ROW_NUMBER,要奥运奖牌式排名(并列跳号)用 RANK,要连续排名用 DENSE_RANK。实际开发中 DENSE_RANK 用得最多。

29.4 取值函数:LAG / LEAD

LAGLEAD 用于在分区内取”前一行”或”后一行”的值,常用于计算环比、同比。

LAG:取前一行

SELECT
    user_id,
    created_at,
    amount,
    LAG(amount) OVER(PARTITION BY user_id ORDER BY created_at) AS 上一笔金额
FROM orders;

按用户分区,按时间排序,每行显示上一笔订单的金额。第一行没有”上一行”,显示 NULL。

可以指定偏移量和默认值:

-- 取前 2 行的值,没有时默认显示 0
LAG(amount, 2, 0) OVER(PARTITION BY user_id ORDER BY created_at)

LEAD:取后一行

SELECT
    user_id,
    created_at,
    amount,
    LEAD(amount) OVER(PARTITION BY user_id ORDER BY created_at) AS 下一笔金额
FROM orders;

取后一行的值。最后一行没有”后一行”,显示 NULL。

实际应用:计算环比增长

SELECT
    user_id,
    created_at,
    amount AS 当前金额,
    LAG(amount) OVER(PARTITION BY user_id ORDER BY created_at) AS 上次金额,
    amount - LAG(amount) OVER(PARTITION BY user_id ORDER BY created_at) AS 增减,
    ROUND(
        (amount - LAG(amount) OVER(PARTITION BY user_id ORDER BY created_at))
        / LAG(amount) OVER(PARTITION BY user_id ORDER BY created_at) * 100,
        2
    ) AS 环比增长率
FROM orders
WHERE status = 'paid';
Note

在窗口函数出现之前,做这种”跟上一行比较”的查询要用自连接或用户变量,写法复杂且性能差。窗口函数让这类需求变得简洁。参考:MySQL Window Function Descriptions

29.5 聚合函数作为窗口函数

前面学的聚合函数(SUM、AVG、COUNT、MAX、MIN)都可以作为窗口函数使用,加 OVER() 即可:

SELECT
    id,
    user_id,
    amount,
    -- 每个用户的订单总数
    COUNT(*) OVER(PARTITION BY user_id) AS 用户订单数,
    -- 每个用户的总消费
    SUM(amount) OVER(PARTITION BY user_id) AS 用户总消费,
    -- 每个用户的最大单笔金额
    MAX(amount) OVER(PARTITION BY user_id) AS 用户最大金额
FROM orders;

每行都保留原始数据,同时附带该用户的统计值。

累计求和

配合 ORDER BY,聚合窗口函数可以做累计计算

SELECT
    user_id,
    created_at,
    amount,
    SUM(amount) OVER(
        PARTITION BY user_id
        ORDER BY created_at
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS 累计消费
FROM orders;

ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 表示窗口范围是”从分区的第一行到当前行”。每行的”累计消费”是当前行及之前所有行的金额之和。

+---------+-----------+--------+----------+
| user_id | created_at| amount | 累计消费 |
+---------+-----------+--------+----------+
|    1    | 07-01     |  100   |   100    |  -- 100
|    1    | 07-05     |  200   |   300    |  -- 100 + 200
|    1    | 07-10     |  150   |   450    |  -- 100 + 200 + 150
+---------+-----------+--------+----------+
Tip

累计求和是窗口函数最实用的功能之一。以前要靠用户变量(@变量名:=)实现,写法复杂且容易出错。窗口函数一行搞定。

29.6 帧框架(Frame Clause)

帧(Frame)定义了窗口函数的计算范围。在 ORDER BY 后面用 ROWSRANGE 指定。

语法

ROWS BETWEEN 起点 AND 终点
RANGE BETWEEN 起点 AND 终点

起点和终点可以是:

关键字含义
UNBOUNDED PRECEDING分区的第一行
N PRECEDING前面第 N 行
CURRENT ROW当前行
N FOLLOWING后面第 N 行
UNBOUNDED FOLLOWING分区的最后一行

ROWS vs RANGE

  • ROWS:按物理行数计算
  • RANGE:按逻辑值范围计算
-- 前一行到当前行的移动平均
AVG(amount) OVER(
    PARTITION BY user_id
    ORDER BY created_at
    ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
)

-- 前一行到后一行的移动平均(3行窗口)
AVG(amount) OVER(
    PARTITION BY user_id
    ORDER BY created_at
    ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
)

默认帧

如果指定了 ORDER BY 但没写帧:

  • 大多数函数的默认帧是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  • 等价于”从分区开头到当前行(含同值行)”

如果没指定 ORDER BY

  • 默认帧是整个分区(ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
Note

默认帧是 RANGE 不是 ROWS,这可能导致意外结果。当排序列有相同值时,RANGE 会把所有相同值的行都包含进窗口。如果不确定,显式写 ROWS BETWEEN 更安全。参考:Window Function Frame Specification

29.7 其他窗口函数

NTILE:分桶

把数据分成 N 等份,返回每行属于第几桶:

-- 把用户按年龄分成 4 组
SELECT
    username,
    age,
    NTILE(4) OVER(ORDER BY age) AS 年龄分组
FROM users;

常用于数据分析中的四分位数、十分位数。

FIRST_VALUE / LAST_VALUE

取分区内第一个/最后一个值:

SELECT
    user_id,
    created_at,
    amount,
    FIRST_VALUE(amount) OVER(PARTITION BY user_id ORDER BY created_at) AS 第一笔金额,
    LAST_VALUE(amount) OVER(
        PARTITION BY user_id ORDER BY created_at
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    ) AS 最后一笔金额
FROM orders;
Warning

LAST_VALUE 的默认帧是”到当前行为止”,所以不加 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING 的话,每行的 LAST_VALUE 就是当前行自己。这是最常见的窗口函数坑之一。

NTH_VALUE

取分区内第 N 个值:

-- 每个用户第二笔订单的金额
SELECT
    user_id,
    created_at,
    amount,
    NTH_VALUE(amount, 2) OVER(PARTITION BY user_id ORDER BY created_at) AS 第二笔金额
FROM orders;

CUME_DIST 和 PERCENT_RANK

-- 累积分布和百分比排名
SELECT
    username,
    age,
    CUME_DIST() OVER(ORDER BY age) AS 累积分布,
    PERCENT_RANK() OVER(ORDER BY age) AS 百分比排名
FROM users;

完整窗口函数列表

函数作用
ROW_NUMBER()行号
RANK()排名(跳号)
DENSE_RANK()密集排名(不跳号)
LAG(列, n, 默认值)前第 n 行的值
LEAD(列, n, 默认值)后第 n 行的值
FIRST_VALUE(列)分区第一个值
LAST_VALUE(列)分区最后一个值
NTH_VALUE(列, n)分区第 n 个值
NTILE(n)分成 n 桶,返回桶号
CUME_DIST()累积分布值
PERCENT_RANK()百分比排名
SUM/AVG/COUNT/MIN/MAX(列) OVER()聚合窗口函数

29.8 命名窗口(WINDOW 子句)

如果多个窗口函数用相同的 OVER() 定义,可以用 WINDOW 子句命名,避免重复:

SELECT
    user_id,
    amount,
    ROW_NUMBER() OVER w AS rn,
    RANK() OVER w AS rnk,
    SUM(amount) OVER w AS total
FROM orders
WINDOW w AS (PARTITION BY user_id ORDER BY amount DESC);

WINDOW w AS (...) 定义了一个名为 w 的窗口,后面直接用 OVER w 引用。当 SQL 里有多个相同 OVER 的窗口函数时,这能让代码简洁很多。

Tip

命名窗口在复杂查询中特别有用。我之前写过一个 200 行的报表 SQL,十几个窗口函数共享相同的 PARTITION BY,用了 WINDOW 子句后代码量少了三分之一。

29.9 窗口函数的执行顺序

窗口函数在 SQL 执行流程中的位置:

FROM -> WHERE -> GROUP BY -> HAVING -> SELECT(窗口函数) -> ORDER BY -> LIMIT

窗口函数在 SELECT 阶段执行,在 GROUP BY 和 HAVING 之后。这意味着:

  1. 窗口函数能用到 WHERE 和 GROUP BY 的结果
  2. 窗口函数不能用 WHERE 来过滤(它在 WHERE 之后执行)
  3. 窗口函数的结果可以在 ORDER BY 中使用
-- 窗口函数结果不能用在 WHERE 里(会报错)
SELECT *, ROW_NUMBER() OVER(ORDER BY amount DESC) AS rn
FROM orders
WHERE rn <= 3;
-- ERROR: Unknown column 'rn'

-- 正确做法:用子查询或 CTE 包一层
SELECT * FROM (
    SELECT *, ROW_NUMBER() OVER(ORDER BY amount DESC) AS rn
    FROM orders
) AS t
WHERE rn <= 3;
Note

窗口函数的结果不能直接在 WHERE 中过滤,这是因为它在 WHERE 之后执行。要过滤窗口函数结果,需要把整个查询包成派生表或 CTE,外层再 WHERE。CTE 在下一章讲。

29.10 实战示例

示例一:查每个用户金额最高的 3 笔订单

SELECT * FROM (
    SELECT
        id,
        user_id,
        amount,
        status,
        created_at,
        ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY amount DESC) AS rn
    FROM orders
) AS t
WHERE rn <= 3;

这是经典的”分组取 Top N”问题,窗口函数是最佳解法。

示例二:查金额排名前 3 的所有用户(含并列)

SELECT * FROM (
    SELECT
        id,
        user_id,
        amount,
        DENSE_RANK() OVER(ORDER BY amount DESC) AS dr
    FROM orders
) AS t
WHERE dr <= 3;

用 DENSE_RANK 不会跳号,并列的都能查出来。

示例三:计算每个用户连续 3 笔订单的移动平均

SELECT
    user_id,
    created_at,
    amount,
    ROUND(
        AVG(amount) OVER(
            PARTITION BY user_id
            ORDER BY created_at
            ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
        ),
        2
    ) AS 三笔移动平均
FROM orders;

示例四:查每个用户第一笔和最后一笔订单

SELECT DISTINCT
    user_id,
    FIRST_VALUE(amount) OVER w AS 第一笔金额,
    FIRST_VALUE(created_at) OVER w AS 第一笔时间,
    LAST_VALUE(amount) OVER(
        PARTITION BY user_id ORDER BY created_at
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    ) AS 最后一笔金额
FROM orders
WINDOW w AS (PARTITION BY user_id ORDER BY created_at);

29.11 性能注意事项

窗口函数虽然强大,但也有性能考量:

  1. 需要排序:大多数窗口函数需要排序(PARTITION BY + ORDER BY),大数据量时消耗内存和 CPU。
  2. 无法走索引优化:窗口函数在结果集上计算,不直接利用索引。
  3. 内存消耗:窗口函数需要缓存分区数据,分区越大内存消耗越多。
Warning

| 大表上用窗口函数要谨慎。百万级数据跑复杂窗口函数可能要几秒。优化思路:先用 WHERE 缩小数据范围,再做窗口计算。别在全表上直接跑窗口函数。

29.12 小结

窗口函数是 MySQL 8.0+ 最重要的新特性之一。它在不减少行数的前提下给每行附加计算值,解决了排名、累计、环比等复杂查询需求。核心记住:OVER() 定义窗口、PARTITION BY 分区、ORDER BY 排序、ROWS BETWEEN 定义帧范围。ROW_NUMBER/RANK/DENSE_RANK 做排名,LAG/LEAD 做前后行比较,聚合函数 OVER 做分组统计。窗口函数结果不能直接在 WHERE 过滤,要包一层子查询。下一章学 CTE(公用表表达式),跟窗口函数是好搭档。