SQL185 试卷完成数同比2020年的增长率及排名变化

描述

现有试卷信息表examination_info(exam_id试卷ID, tag试卷类别, difficulty试卷难度, duration考试时长, release_time发布时间):

试卷作答记录表exam_record(uid用户ID, exam_id试卷ID, start_time开始作答时间, submit_time交卷时间, score得分):

请计算2021年上半年各类试卷的做完次数相比2020年上半年同期的增长率(百分比格式,保留1位小数),以及做完次数排名变化,按增长率和21年排名降序输出。

由示例数据结果输出如下:

解释:2020年上半年有3个tag有作答完成的记录,分别是C++、SQL、PYTHON,它们被做完的次数分别是3、3、2,做完次数排名为1、1(并列)、3;

2021年上半年有2个tag有作答完成的记录,分别是算法、SQL,它们被做完的次数分别是3、2,做完次数排名为1、2;具体如下:

因此能输出同比结果的tag只有SQL,从2020到2021年,做完次数3=>2,减少33.3%(保留1位小数);排名1=>2,后退1名。

复制代码
WITH
    t2 AS (
        SELECT
            exam_id,
            IF(start_year = '2020', exam_cnt, NULL) exam_cnt_20, -- 2020年的完成次数
            LEAD(exam_cnt, 1) OVER (
                PARTITION BY
                    exam_id
                ORDER BY
                    start_year
            ) exam_cnt_21, -- 2021年的完成次数
            IF(start_year = '2020', rk, NULL) exam_cnt_rank_20, -- 2020年的排名
            LEAD(rk, 1) OVER (
                PARTITION BY
                    exam_id
                ORDER BY
                    start_year
            ) exam_cnt_rank_21 -- 2021年的排名
        FROM
            (
                SELECT
                    exam_id,
                    YEAR(submit_time) start_year,
                    COUNT(score) exam_cnt,
                    RANK() OVER (
                        PARTITION BY
                            YEAR(submit_time)
                        ORDER BY
                            COUNT(score) DESC
                    ) rk
                    /*分别对2021和2020的做完情况进行排名*/
                FROM
                    exam_record
                WHERE
                    MONTH(submit_time) BETWEEN 1 AND 6 -- 选取上半年数据
                    AND submit_time BETWEEN '2020-01-00 00:00:00' AND '2022-01-01 00:00:00' -- 选取2020和2021年的数据
                GROUP BY
                    YEAR(submit_time),
                    exam_id
                    /*对年份和类别进行聚类*/
            ) t1
    )
SELECT
    a.tag,
    exam_cnt_20,
    exam_cnt_21,
    CONCAT(
        ROUND(
            (exam_cnt_21 - exam_cnt_20) * 100 / exam_cnt_20,
            1
        ),
        '%'
    ) AS growth_rate,
    exam_cnt_rank_20,
    exam_cnt_rank_21,
    CAST(exam_cnt_rank_21 AS SIGNED) - CAST(exam_cnt_rank_20 AS SIGNED) AS rank_delta -- 需要转换格式,否则会报错
FROM
    t2
    LEFT JOIN examination_info AS a ON a.exam_id = t2.exam_id
WHERE
    exam_cnt_21 IS NOT NULL
ORDER BY
    growth_rate DESC,
    exam_cnt_rank_21 DESC;

🔍 代码逐层解析

🧱 1. 内层查询 t1 ------ 按年份+试卷分组并排名

复制代码
SELECT
    exam_id,
    YEAR(submit_time) AS start_year,
    COUNT(score) AS exam_cnt,
    RANK() OVER (
        PARTITION BY YEAR(submit_time)
        ORDER BY COUNT(score) DESC
    ) AS rk
FROM exam_record
WHERE
    MONTH(submit_time) BETWEEN 1 AND 6
    AND submit_time >= '2020-01-01' AND submit_time < '2022-01-01'
GROUP BY YEAR(submit_time), exam_id
✅ 做了什么?
  • 筛选 2020 和 2021 年上半年 的数据
  • 年份 + 试卷 ID 分组
  • 统计每类试卷每年的完成次数COUNT(score)
  • 使用 RANK() 计算每年内的完成次数排名(降序)
⚠️ 注意:
  • BETWEEN 1 AND 6:精确筛选上半年
  • submit_time < '2022-01-01':避免包含 2022 年数据
  • RANK() 处理并列情况(如 3,3 → 排名 1,1,下一名为 3)

🧱 2. 中层查询 t2 ------ 使用 LEAD() 对齐两年数据

复制代码
SELECT
    exam_id,
    IF(start_year = '2020', exam_cnt, NULL) AS exam_cnt_20,
    LEAD(exam_cnt, 1) OVER (PARTITION BY exam_id ORDER BY start_year) AS exam_cnt_21,
    IF(start_year = '2020', rk, NULL) AS exam_cnt_rank_20,
    LEAD(rk, 1) OVER (PARTITION BY exam_id ORDER BY start_year) AS exam_cnt_rank_21
FROM t1
✅ 核心技巧:LEAD() 窗口函数
函数 作用
LEAD(col, 1) 获取当前行之后第 1 行的值
PARTITION BY exam_id 按试卷分组,确保只在同 exam_id 内查找
ORDER BY start_year 按年份升序排列(2020 → 2021)
💡 举个例子:

原始 t1 数据:

exam_id start_year exam_cnt rk
9001 2020 100 2
9001 2021 150 1

经过 LEAD() 后:

exam_id exam_cnt_20 exam_cnt_21 rk_20 rk_21
9001 100 150 2 1

✅ 实现了"将两年数据对齐到同一行"

📌 IF(start_year = '2020', ..., NULL) 的作用:
  • 将 2020 年的数据保留在 exam_cnt_20 字段
  • 2021 年该字段为 NULL
  • 配合 LEAD(),确保 exam_cnt_21 是下一年的值

🧱 3. 主查询 ------ 计算增长率与排名变化

复制代码
SELECT
    a.tag,
    exam_cnt_20,
    exam_cnt_21,
    CONCAT(ROUND((exam_cnt_21 - exam_cnt_20) * 100.0 / exam_cnt_20, 1), '%') AS growth_rate,
    exam_cnt_rank_20,
    exam_cnt_rank_21,
    CAST(exam_cnt_rank_21 AS SIGNED) - CAST(exam_cnt_rank_20 AS SIGNED) AS rank_delta
FROM t2
LEFT JOIN examination_info AS a ON a.exam_id = t2.exam_id
WHERE exam_cnt_21 IS NOT NULL
ORDER BY growth_rate DESC, exam_cnt_rank_21 DESC;
✅ 关键计算:
指标 公式 说明
增长率 (2021 - 2020) / 2020 * 100% * 100.0 保证浮点运算
格式化输出 CONCAT(..., '%') 添加百分号
排名变化 rank_21 - rank_20 正数表示排名下降,负数表示上升
类型转换 CAST(... AS SIGNED) 避免字符串减法报错
✅ 过滤与排序:
  • WHERE exam_cnt_21 IS NOT NULL:确保该试卷在 2020 和 2021 都存在
  • ORDER BY growth_rate DESC:增长率从高到低
  • exam_cnt_rank_21 DESC:2021 年排名靠后的优先(同增长率时)

📝 核心知识点总结

技术点 说明 应用场景
LEAD()/LAG() 获取下一行/上一行的值 同比、环比分析
RANK() 处理并列排名 排行榜、绩效排名
PARTITION BY 窗口函数分组 分组内排序、对比
CONCAT + ROUND 格式化数值输出 百分比、金额显示
CAST(... AS SIGNED) 类型转换 字符串转整数计算
WITH ... AS () CTE 公共表表达式 分步处理复杂逻辑

✅ 最佳实践建议

  1. 时间范围写法

    • BETWEEN '2020-01-00'(非法日期)
    • submit_time >= '2020-01-01' AND submit_time < '2022-01-01'
  2. 增长率计算注意除零

    • 可加 WHERE exam_cnt_20 > 0
  3. 排名函数选择

    • RANK():允许并列,下一名跳过(1,1,3)
    • DENSE_RANK():允许并列,下一名不跳过(1,1,2)
    • ROW_NUMBER():强制唯一,无并列
  4. LEAD() 的适用场景

    • 跨行对比(如:今年 vs 去年)
    • 避免自连接,提升性能

🎯 一句话总结

"用 LEAD() 实现跨年数据对齐,RANK() 计算年度排名,CONCAT+ROUND 格式化增长率,完成试卷类别的同比分析"

这套模式适用于:

  • 年度/季度/月度对比分析
  • 排名变化监控
  • 增长率计算与展示