用 LAG/LEAD 做行间比较、用 SUM(...) OVER 算累计值,并解释 ROWS 与 RANGE 帧的差异
用 CTE 把多层逻辑写成命名步骤,用 WITH RECURSIVE 生成序列
为什么要学
「每人最快的一次 AC」「按 AC 数排名」「相邻两次提交间隔了多久」——这些需求 GROUP BY 做不到(它会把明细行折叠掉),子查询写得又臭又长。窗口函数是 MySQL 8 最重磅的新能力:保留每一行明细,同时在旁边挂上组内计算结果。它也是现在面试的分水岭题:「找出每个部门工资前三名」没有窗口函数基本写不出优雅解。
人话:GROUP BY 是「把 12 行压成 4 行」,窗口函数是「12 行还在,每行多挂一个数」。想看明细+排行,只能靠它。
SELECT
user_id,
runtime_ms,
submitted_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY submitted_at) AS rn
FROM cf_submission;
预期:每人组内 1、2、3 顺次编号(lin 3 行、zhao 3 行、wang 2 行、zhou 3 行、sun 1 行)。PARTITION BY 是「分组但不合并行」,ORDER BY 决定组内序号怎么排。
步骤 2:RANK vs DENSE_RANK——按 AC 数排名
WITH ac AS (
SELECT user_id, SUM(status = 'accepted') AS ac_cnt
FROM cf_submission GROUP BY user_id
)
SELECT
u.username,
a.ac_cnt,
ROW_NUMBER() OVER (ORDER BY a.ac_cnt DESC, u.id) AS rn,
RANK() OVER (ORDER BY a.ac_cnt DESC) AS rk,
DENSE_RANK() OVER (ORDER BY a.ac_cnt DESC) AS drk
FROM ac a JOIN cf_user u ON u.id = a.user_id
ORDER BY rk, u.id;
预期结果(lin/zhao/zhou 各 2 AC 并列,wang 1,sun 0):
username
ac_cnt
rn
rk
drk
lin
2
1
1
1
zhao
2
2
1
1
zhou
2
3
1
1
wang
1
4
4
2
sun
0
5
5
3
三人并列第 1 时:RANK 跳到 4(并列占位),DENSE_RANK 只进到 2(并列不占位),ROW_NUMBER 强行分出 1/2/3。取「第 N 名」用 RANK,取「前 N 行」用 ROW_NUMBER。
步骤 3:每人最快的一次 AC(Top-1 模板)
SELECT username, title, runtime_ms
FROM (
SELECT
u.username, p.title, s.runtime_ms,
ROW_NUMBER() OVER (PARTITION BY s.user_id ORDER BY s.runtime_ms) AS rn
FROM cf_submission s
JOIN cf_user u ON u.id = s.user_id
JOIN cf_problem p ON p.id = s.problem_id
WHERE s.status = 'accepted'
) t
WHERE rn = 1
ORDER BY runtime_ms;
SELECT
user_id,
submitted_at,
runtime_ms,
LAG(submitted_at) OVER (PARTITION BY user_id ORDER BY submitted_at) AS prev_time,
TIMESTAMPDIFF(MINUTE,
LAG(submitted_at) OVER (PARTITION BY user_id ORDER BY submitted_at),
submitted_at) AS gap_minutes
FROM cf_submission
WHERE user_id = 1;
-- 预期(lin):第 1 行 prev_time 为 NULL;第 2 行间隔 35 分钟;第 3 行跨天约 875 分钟
SELECT
submitted_at,
runtime_ms,
SUM(runtime_ms) OVER (ORDER BY DATE(submitted_at)) AS s_range,
SUM(runtime_ms) OVER (ORDER BY DATE(submitted_at)
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS s_rows
FROM cf_submission
ORDER BY submitted_at
LIMIT 5;
预期前五行:
submitted_at
runtime_ms
s_range
s_rows
06-01 09:05
12
42
12
06-01 09:40
30
42
42
06-02 10:00
25
67
67
06-03 20:15
48
2115
115
06-03 21:00
2000
2115
2115
关键差异在第一天:RANGE 默认帧把「同一天」(ORDER BY 值相同)的行全部并入当前行,所以第一行就看到 42;ROWS 帧严格逐行累加。做累计值必须显式写 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,否则同值并列时数字会「跳变」。
步骤 6:CTE 与递归 CTE
-- 普通 CTE:AC 率达标名单(逻辑与第 4 课相同,但分步命名)
WITH ac AS (
SELECT user_id,
COUNT(*) AS total,
SUM(status = 'accepted') AS ac_cnt
FROM cf_submission
GROUP BY user_id
)
SELECT u.username, a.total, a.ac_cnt
FROM ac a JOIN cf_user u ON u.id = a.user_id
WHERE a.ac_cnt / a.total >= 0.6;
-- 递归 CTE:生成 1..5
WITH RECURSIVE seq AS (
SELECT 1 AS n -- 锚点:起点
UNION ALL
SELECT n + 1 FROM seq WHERE n < 5 -- 递归:每次 +1,WHERE 是终止条件
)
SELECT * FROM seq;
-- 预期 1 2 3 4 5
递归 CTE 三段式:锚点 + UNION ALL + 带终止条件的递归体。忘了终止条件会撞上默认 1000 层上限报 ERROR 3636。
讲清楚要点
OVER 三要素:PARTITION BY(分组窗口)、ORDER BY(组内排序,决定编号/累计顺序)、帧(ROWS/RANGE,决定累计看哪些行)。只有 OVER () 空窗口时是对全表计算。