一、题目描述
现有试卷作答记录表 exam_record,包含字段:
uid:用户 IDexam_id:试卷 IDstart_time:开始作答时间submit_time:交卷时间score:得分
要求输出:
- 每份试卷(
exam_id) - 每月(格式为
YYYYMM,如202001) - 当月作答次数(
month_cnt) - 截止当月的累计作答总数 (
cum_exam_cnt)
示例输出:
9001|202001|2|2
9001|202002|1|3
9001|202003|3|6
9001|202005|1|7
9002|202001|1|1
9002|202002|3|4
9002|202003|1|5
📌 解释:
试卷
9001在202001有 2 次作答,累计 = 2在
202002有 1 次,累计 = 2+1=3在
202003有 3 次,累计 = 3+3=6......以此类推。
二、正确 SQL 解法
SELECT
exam_id,
start_month,
month_cnt,
SUM(month_cnt) OVER (
PARTITION BY exam_id
ORDER BY start_month
) AS cum_exam_cnt
FROM (
SELECT
exam_id,
DATE_FORMAT(start_time, '%Y%m') AS start_month,
COUNT(*) AS month_cnt
FROM exam_record
GROUP BY exam_id, DATE_FORMAT(start_time, '%Y%m')
) t
ORDER BY exam_id, start_month;
三、分步解析(核心!)
我们把整个查询拆成 4 个步骤,像"做菜"一样一步步来。
第一步:提取"年月"并按月统计
SELECT
exam_id,
DATE_FORMAT(start_time, '%Y%m') AS start_month,
COUNT(*) AS month_cnt
FROM exam_record
GROUP BY exam_id, DATE_FORMAT(start_time, '%Y%m')
📌 做了什么?
- 使用
DATE_FORMAT(start_time, '%Y%m')将时间转为202001格式。 - 按
exam_id和start_month分组。 - 统计每月每卷的作答次数(
COUNT(*))。
输出结果(中间表):
| exam_id | start_month | month_cnt |
|---|---|---|
| 9001 | 202001 | 2 |
| 9001 | 202002 | 1 |
| 9001 | 202003 | 3 |
| 9001 | 202005 | 1 |
| 9002 | 202001 | 1 |
| 9002 | 202002 | 3 |
| 9002 | 202003 | 1 |
这就是"中间值"!它是后续计算的基础。
第二步:使用子查询"显式化中间结果"
FROM (
-- 上面的查询作为子查询
) t
为什么需要子查询?
- 因为
month_cnt是GROUP BY后的聚合结果。 - 如果直接在
SELECT中写SUM(month_cnt),某些数据库不支持引用别名。 - 所以必须用子查询把它"固化"成一个临时表
t,外层才能安全使用。
类比:你不能一边切菜一边炒,必须先切好(子查询),再炒(外层计算)。
第三步:窗口函数计算"累计值"
SUM(month_cnt) OVER (
PARTITION BY exam_id
ORDER BY start_month
)
拆解窗口函数三要素:
| 部分 | 作用 | 类比(切蛋糕) |
|---|---|---|
PARTITION BY exam_id |
把数据按试卷分组 | 把大蛋糕切成几块,每块代表一场试卷 |
ORDER BY start_month |
在每块内按时间排序 | 把每块蛋糕的"夹心"按时间排好 |
SUM(month_cnt) |
从第一行到当前行累加 | 从第一片开始,逐片切,记录累计大小 |
累计过程示例(exam_id = 9001):
| 月份 | 当月次数 | 累计值 |
|---|---|---|
| 202001 | 2 | 2 |
| 202002 | 1 | 2+1=3 |
| 202003 | 3 | 3+3=6 |
| 202005 | 1 | 6+1=7 |
完全符合"截止当月的作答总数"。
第四步:排序输出
ORDER BY exam_id, start_month
确保结果按试卷 ID 和时间顺序排列,便于阅读。
四、常见错误与避坑指南
| 错误写法 | 问题 | 正确做法 |
|---|---|---|
SUM(month_cnt) OVER() |
全局求和,不分组 | 必须 PARTITION BY exam_id |
SUM(COUNT(*)) OVER(...) |
聚合函数嵌套不合法 | 先 GROUP BY,再用窗口函数 |
PARTITION BY month_cnt |
按"次数"分组,无意义 | 应 PARTITION BY exam_id |
不用子查询直接引用 month_cnt |
某些数据库报错 | 用子查询显式构造中间表 |
ORDER BY start_month 缺失 |
累计顺序不确定 | 必须排序,确保时间顺序 |
五、核心知识点总结
1. SQL 执行顺序(逻辑)
FROM → WHERE → GROUP BY → SELECT → ORDER BY
- 窗口函数在
SELECT阶段执行,在GROUP BY之后。 - 所以可以对聚合结果进行窗口计算。
2. 窗口函数公式
FUNCTION(列) OVER (
PARTITION BY 分组列 -- 分块
ORDER BY 排序列 -- 块内排序
ROWS BETWEEN ... -- 窗口范围(默认从头到当前行)
)
3. 什么时候用子查询?
- 当你需要对
GROUP BY后的结果再做复杂计算时。 - 特别是窗口函数要引用聚合结果时,必须用子查询。
六、举一反三
想要"每场考试的总作答次数"?
SUM(month_cnt) OVER (PARTITION BY exam_id)
→ 每行都显示该试卷的总次数(不累计)。
想要"所有试卷的总作答次数"?
SUM(month_cnt) OVER ()
→ 全局总数,每行都一样。
想要"排名"?
ROW_NUMBER() OVER (PARTITION BY exam_id ORDER BY start_month)
→ 每场考试内,按时间顺序编号。