MySQL 窗口函数完全指南

窗口函数(Window Function)是 MySQL 8.0+ 引入的强大功能,它可以在不改变行数的情况下,对每一行执行跨行计算(如排名、累计、移动平均等)。

一、窗口函数核心语法

复制代码
函数名([参数]) OVER (
    [PARTITION BY 分组列1, 分组列2, ...]   -- 分区:将数据分组
    [ORDER BY 排序列 [ASC|DESC]]          -- 排序:定义组内顺序
    [窗口帧子句]                          -- 指定计算的行范围
)

窗口帧子句(Frame)

指定当前行参与计算的行范围:

写法 含义
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 从分区第一行到当前行(默认,累计)
ROWS BETWEEN 3 PRECEDING AND CURRENT ROW 前3行 + 当前行
ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING 当前行 + 后3行
ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING 前3行 + 当前 + 后3行
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING 整个分区

注意 :没有 ORDER BY 时,窗口帧默认为 RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING;有 ORDER BY 时默认为 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW


二、排名类窗口函数

函数 功能 特点
ROW_NUMBER() 为每一行分配唯一、连续的序号 同分不同号,1,2,3,4
RANK() 排名,相同值同排名,后续跳过 1,2,2,4
DENSE_RANK() 密集排名,相同值同排名,后续连续 1,2,2,3
NTILE(n) 将分区均匀分成 n 个桶,返回桶编号 数据倾斜时桶大小差异 ≤1

案例1:学生成绩排名

复制代码
-- 准备数据
CREATE TABLE scores (
    student VARCHAR(20),
    subject VARCHAR(20),
    score INT
);
​
INSERT INTO scores VALUES
('张三', '数学', 95),
('李四', '数学', 90),
('王五', '数学', 90),
('赵六', '数学', 85),
('小明', '数学', 95);
​
-- 三种排名对比
SELECT 
    student,
    score,
    ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num,
    RANK()       OVER (ORDER BY score DESC) AS rank_num,
    DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank_num
FROM scores
WHERE subject = '数学';

结果:

复制代码
student | score | row_num | rank_num | dense_rank_num
--------|-------|---------|----------|----------------
张三    | 95    | 1       | 1        | 1
小明    | 95    | 2       | 1        | 1
李四    | 90    | 3       | 3        | 2
王五    | 90    | 4       | 3        | 2
赵六    | 85    | 5       | 5        | 3

案例2:每个学科内部分组排名

复制代码
-- 每个学科内按分数排名
SELECT 
    subject,
    student,
    score,
    RANK() OVER (PARTITION BY subject ORDER BY score DESC) AS subject_rank
FROM scores;

案例3:NTILE 分桶(四分位数)

复制代码
-- 将所有学生按分数分成4个等级
SELECT 
    student,
    score,
    NTILE(4) OVER (ORDER BY score DESC) AS quartile
FROM scores;

三、偏移类窗口函数(访问前后行)

函数 语法 功能
LAG(expr, offset, default) LAG(列, 偏移量, 默认值) 访问当前行之前第 offset 行的值
LEAD(expr, offset, default) LEAD(列, 偏移量, 默认值) 访问当前行之后第 offset 行的值

案例4:环比增长率计算

复制代码
-- 准备销售月报数据
CREATE TABLE monthly_sales (
    sale_month DATE,
    amount DECIMAL(10,2)
);
​
INSERT INTO monthly_sales VALUES
('2026-01-01', 10000),
('2026-02-01', 12000),
('2026-03-01', 11000),
('2026-04-01', 13000);
​
-- 计算环比增长
SELECT 
    DATE_FORMAT(sale_month, '%Y-%m') AS month,
    amount,
    LAG(amount, 1) OVER (ORDER BY sale_month) AS prev_month_amount,
    amount - LAG(amount, 1) OVER (ORDER BY sale_month) AS increase,
    ROUND(
        (amount - LAG(amount, 1) OVER (ORDER BY sale_month)) / 
        LAG(amount, 1) OVER (ORDER BY sale_month) * 100, 2
    ) AS growth_rate_pct
FROM monthly_sales;

结果:

复制代码
month   | amount | prev_month_amount | increase | growth_rate_pct
--------|--------|-------------------|----------|----------------
2026-01 | 10000  | NULL              | NULL     | NULL
2026-02 | 12000  | 10000             | 2000     | 20.00
2026-03 | 11000  | 12000             | -1000    | -8.33
2026-04 | 13000  | 11000             | 2000     | 18.18

案例5:计算每日销售额与昨日对比(带默认值)

复制代码
-- 使用默认值,避免NULL
SELECT 
    sale_date,
    amount,
    LAG(amount, 1, 0) OVER (ORDER BY sale_date) AS prev_amount
FROM daily_sales;

四、首尾类窗口函数

函数 功能 注意
FIRST_VALUE(expr) 分区内第一行的值 需要明确的窗口帧
LAST_VALUE(expr) 分区内最后一行的值 默认帧只到当前行,需指定 UNBOUNDED FOLLOWING
NTH_VALUE(expr, n) 分区内第 n 行的值 同样需要指定帧

案例6:获取每组首个和末个值

复制代码
-- 按学科分组,获取最高分和最低分(按分数排序)
SELECT DISTINCT
    subject,
    FIRST_VALUE(score) OVER (PARTITION BY subject ORDER BY score DESC) AS highest_score,
    LAST_VALUE(score) OVER (PARTITION BY subject ORDER BY score DESC 
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS lowest_score
FROM scores;

⚠️ LAST_VALUE 陷阱 :默认窗口帧是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,所以 LAST_VALUE 只会返回当前行,而非分区最后一行。必须显式指定 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING

案例7:获取第二名分数(NTH_VALUE)

复制代码
SELECT DISTINCT
    subject,
    NTH_VALUE(score, 2) OVER (PARTITION BY subject ORDER BY score DESC 
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS second_score
FROM scores;

五、聚合窗口函数(累计/移动计算)

函数 作为窗口函数时的作用
SUM() 累计和、移动和
AVG() 移动平均
COUNT() 累计计数
MIN() / MAX() 累计极值

案例8:累计和(从第一行到当前行)

复制代码
SELECT 
    sale_date,
    amount,
    SUM(amount) OVER (ORDER BY sale_date) AS cumulative_sum
FROM monthly_sales;

案例9:3日移动平均(前1天+当天+后1天)

复制代码
SELECT 
    sale_date,
    amount,
    AVG(amount) OVER (ORDER BY sale_date 
        ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS moving_avg_3
FROM monthly_sales;

案例10:分组内累计占比

复制代码
-- 每个学科内,计算当前学生成绩占学科总分的百分比
SELECT 
    subject,
    student,
    score,
    SUM(score) OVER (PARTITION BY subject) AS subject_total,
    ROUND(score * 100.0 / SUM(score) OVER (PARTITION BY subject), 2) AS pct
FROM scores;

六、完整实战案例集合

案例11:用户消费分析(累计、排名、环比)

复制代码
-- 假设有订单表 orders (user_id, amount, create_time)
-- 按用户分组,计算每个用户每笔订单的:
-- 1. 累计消费金额
-- 2. 订单金额排名
-- 3. 环比上一笔订单增长率
​
SELECT 
    user_id,
    order_id,
    amount,
    create_time,
    SUM(amount) OVER (PARTITION BY user_id ORDER BY create_time) AS cumulative_amount,
    RANK() OVER (PARTITION BY user_id ORDER BY amount DESC) AS amount_rank,
    LAG(amount) OVER (PARTITION BY user_id ORDER BY create_time) AS prev_amount,
    ROUND((amount - LAG(amount) OVER (PARTITION BY user_id ORDER BY create_time)) / 
          NULLIF(LAG(amount) OVER (PARTITION BY user_id ORDER BY create_time), 0) * 100, 2) AS mom_growth
FROM orders;

案例12:电商7日移动平均销售额

复制代码
WITH daily AS (
    SELECT DATE(create_time) AS dt, SUM(amount) AS daily_amount
    FROM orders
    GROUP BY DATE(create_time)
)
SELECT 
    dt,
    daily_amount,
    AVG(daily_amount) OVER (ORDER BY dt ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma7,
    SUM(daily_amount) OVER (ORDER BY dt ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS ytd
FROM daily;

案例13:删除重复数据(保留每组最小ID)

复制代码
-- 假设 users 表中 email 有重复,保留每个 email 的最小 id
DELETE FROM users
WHERE id NOT IN (
    SELECT * FROM (
        SELECT MIN(id) 
        FROM users 
        GROUP BY email
    ) AS tmp
);
​
-- 使用窗口函数更优雅:先标记再删除
WITH dedup AS (
    SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
    FROM users
)
DELETE FROM users WHERE id IN (SELECT id FROM dedup WHERE rn > 1);

案例14:查询每个分类销量前3的商品

复制代码
SELECT * FROM (
    SELECT 
        p.category_id,
        p.name,
        SUM(oi.quantity) AS total_sold,
        ROW_NUMBER() OVER (PARTITION BY p.category_id ORDER BY SUM(oi.quantity) DESC) AS rn
    FROM products p
    JOIN order_items oi ON p.id = oi.product_id
    GROUP BY p.category_id, p.id
) t WHERE rn <= 3;

案例15:同比环比完整报表

复制代码
WITH monthly AS (
    SELECT 
        DATE_FORMAT(create_time, '%Y-%m') AS month,
        YEAR(create_time) AS year,
        MONTH(create_time) AS month_num,
        SUM(amount) AS amount
    FROM orders
    GROUP BY YEAR(create_time), MONTH(create_time)
)
SELECT 
    month,
    amount,
    LAG(amount) OVER (ORDER BY month) AS prev_month,           -- 上月
    ROUND((amount - LAG(amount) OVER (ORDER BY month)) / 
          LAG(amount) OVER (ORDER BY month) * 100, 2) AS mom,   -- 环比
    LAG(amount, 12) OVER (ORDER BY month) AS last_year,        -- 去年同期
    ROUND((amount - LAG(amount, 12) OVER (ORDER BY month)) / 
          LAG(amount, 12) OVER (ORDER BY month) * 100, 2) AS yoy  -- 同比
FROM monthly;

七、快速参考表

函数分类 函数名 典型用途 必须 ORDER BY
排名 ROW_NUMBER() 唯一序号
排名 RANK() 跳跃排名
排名 DENSE_RANK() 连续排名
排名 NTILE(n) 分桶
偏移 LAG() 前一行
偏移 LEAD() 后一行
首尾 FIRST_VALUE() 首行值 是(配合帧)
首尾 LAST_VALUE() 末行值 是(必须指定帧)
聚合 SUM() 累计和 可选
聚合 AVG() 移动平均 可选
聚合 COUNT() 累计计数 可选

八、常见错误与注意事项

1. 窗口函数不能直接用在 WHERE 子句中

复制代码
-- ❌ 错误
SELECT * FROM orders WHERE ROW_NUMBER() OVER (ORDER BY amount) = 1;
​
-- ✅ 正确:使用子查询
SELECT * FROM (
    SELECT *, ROW_NUMBER() OVER (ORDER BY amount) AS rn FROM orders
) t WHERE rn = 1;

2. ORDER BY 缺失导致的问题

复制代码
-- 没有 ORDER BY,SUM 累计范围是整个分区(每行都一样)
SELECT id, amount, SUM(amount) OVER () FROM orders;  -- 全部是总和
​
-- 加上 ORDER BY,变成累计和
SELECT id, amount, SUM(amount) OVER (ORDER BY id) FROM orders;

3. LAST_VALUE 需要显式窗口帧

复制代码
-- ❌ 错误示例:返回当前行
SELECT student, score, LAST_VALUE(score) OVER (ORDER BY score) FROM scores;
​
-- ✅ 正确
SELECT student, score, LAST_VALUE(score) OVER (
    ORDER BY score ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) FROM scores;

4. 不同窗口函数可以共用 OVER 子句

复制代码
SELECT 
    name, score,
    ROW_NUMBER() OVER w AS rn,
    RANK() OVER w AS rk,
    SUM(score) OVER w AS total
FROM scores
WINDOW w AS (PARTITION BY subject ORDER BY score DESC);

相关推荐
betazhou5 小时前
电科金仓数据库V9 MySQL兼容版本搭建一主一从体验
数据库·mysql·oracle·主从·高可用·kingbase·v9 mysql兼容版本
元宝骑士5 小时前
MySQL 8.0 递归 CTE:树形结构一键生成层级 Path 并更新回表
后端·mysql
python在学ing6 小时前
Django框架学习笔记:从零基础到项目实战
数据库·python·django·sqlite
duoduo_sing6 小时前
数据库备份终极方案:从脚本手动到自动化热备+异地同步实战
运维·数据库·自动化·用友
Lao A(zhou liang)的菜园6 小时前
Oracle 增量检查点 & FAST_START_MTTR_TARGET 核心总结
数据库·oracle
wbs_scy6 小时前
MySQL 多表连接查询实战:内连接 + 外连接
数据库·mysql
杨云龙UP7 小时前
ODA/Oracle RAC 节点 Load 100+ 排查:一个 lsof 残留进程引发的负载虚高问题 2026-05-27
linux·数据库·oracle·centos·误操作
凯瑟琳.奥古斯特7 小时前
选择题专练数据库原理精选30题
开发语言·数据库·职场和发展·数据库开发
BD_Marathon7 小时前
SQL学习指南——事务
数据库·sql·oracle