窗口函数(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);