SQL187 每份试卷每月作答数和截止当月的作答总数。
一、题目描述
现有试卷作答记录表 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_cntFROM exam_recordGROUP 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)→ 每场考试内,按时间顺序编号。
