SQL187 每份试卷每月作答数和截止当月的作答总数。

一、题目描述

现有试卷作答记录表 exam_record,包含字段:

  • uid:用户 ID
  • exam_id:试卷 ID
  • start_time:开始作答时间
  • submit_time:交卷时间
  • score:得分

要求输出:

  1. 每份试卷(exam_id
  2. 每月(格式为 YYYYMM,如 202001
  3. 当月作答次数(month_cnt
  4. 截止当月的累计作答总数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

📌 解释:

试卷 9001202001 有 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_idstart_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_cntGROUP 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)

→ 每场考试内,按时间顺序编号。


相关推荐
ximy13354 小时前
Mysql基础知识之SQL语句——库表管理操作
sql·mysql·oracle
MrZhangBaby7 小时前
SQL-leetcode—3475. DNA 模式识别
数据库·sql·leetcode
码力引擎8 小时前
【零基础学MySQL】第二章:SQL类型
数据库·sql·mysql·oracle
折翼的恶魔11 小时前
SQL 189 统计有未完成状态的试卷的未完成数和未完成率
数据库·sql
梦里不知身是客1113 小时前
hive的SQL语句练习2
hive·hadoop·sql
梦里不知身是客1113 小时前
hive的SQL练习3
hive·hadoop·sql
姚远Oracle ACE14 小时前
Oracle AWR 报告中的SQL来自哪儿?
数据库·sql·oracle
艾菜籽16 小时前
MyBatis动态sql与留言墙联系
java·数据库·sql·spring·mybatis
青~16 小时前
sql 双游标循环
数据库·sql