SQL186 对试卷得分做min-max归一化
一、题目描述
表结构
examination_info:试卷信息表(exam_id,tag,difficulty,duration,release_time)exam_record:作答记录表(uid,exam_id,start_time,submit_time,score)
要求
- 只处理 高难度(difficulty = 'hard')的试卷
- 对每份高难度试卷的所有作答记录,进行 Min-Max 归一化,公式:
new_score=score−min(score)max(score)−min(score)×100new_score=max(score)−min(score)score−min(score)×100
- 特殊情况:如果某试卷只有一条有效得分记录,则归一化后分数 = 原分数
- 输出:
uid,exam_id,avg_new_score(每个用户在每份试卷上的平均新分数,只保留整数部分) - 排序:按
exam_id升序,avg_new_score降序
二、正确 SQL 解法
SELECT uid, exam_id, ROUND(SUM(max_min) / COUNT(max_min), 0) AS avg_new_score
FROM (SELECT exam_id, uid, score,IF(min_x = max_x, score, (score - min_x) * 100 / (max_x - min_x)) AS max_minFROM (SELECT uid, a.exam_id, score,MIN(score) OVER (PARTITION BY a.exam_id) AS min_x,MAX(score) OVER (PARTITION BY a.exam_id) AS max_xFROM exam_record aLEFT JOIN examination_info b ON a.exam_id = b.exam_idWHERE b.difficulty = 'hard'AND a.score IS NOT NULL) t1
) t2
GROUP BY exam_id, uid
ORDER BY exam_id, avg_new_score DESC;三、分步解析(核心!)
我们像“搭积木”一样,从内到外一步步构建。
第一步:筛选高难度试卷的有效作答记录
SELECT uid, a.exam_id, score,MIN(score) OVER (PARTITION BY a.exam_id) AS min_x,MAX(score) OVER (PARTITION BY a.exam_id) AS max_x
FROM exam_record a
LEFT JOIN examination_info b ON a.exam_id = b.exam_id
WHERE b.difficulty = 'hard'AND a.score IS NOT NULL做了什么?
LEFT JOIN关联两张表,获取试卷难度WHERE b.difficulty = 'hard':只保留高难度试卷AND a.score IS NOT NULL:排除未交卷记录(如submit_time为NULL)- 使用窗口函数:
MIN(score) OVER (PARTITION BY exam_id)→ 每份试卷的最低分MAX(score) OVER (PARTITION BY exam_id)→ 每份试卷的最高分
输出示例(t1):
| uid | exam_id | score | min_x | max_x |
|---|---|---|---|---|
| 1001 | 9001 | 90 | 68 | 90 |
| 1003 | 9001 | 68 | 68 | 90 |
| 1001 | 9001 | 89 | 68 | 90 |
| 1003 | 9002 | 75 | 60 | 90 |
| 1004 | 9002 | 60 | 60 | 90 |
| ... | ... | ... | ... | ... |
这是“第一层中间结果”,我们拿到了每条记录对应的
min和max。
第二步:计算归一化后的分数(关键!)
IF(min_x = max_x, score, (score - min_x) * 100 / (max_x - min_x)) AS max_min为什么用 IF?
因为题目说:
“如果某个试卷作答记录中只有一个得分,那么无需使用公式,归一化并缩放后分数仍为原分数”
这意味着:
- 如果
min_x == max_x(即所有分都一样,或只有一条记录),就直接用原分 - 否则,使用归一化公式
计算过程(以 9001 为例):
| score | min_x | max_x | new_score |
|---|---|---|---|
| 90 | 68 | 90 | (90-68)/(90-68)*100 = 100 |
| 68 | 68 | 90 | (68-68)/22*100 = 0 |
| 89 | 68 | 90 | (89-68)/22*100 ≈ 95.45 → 但先保留,后面再处理 |
注意:这里我们先不四舍五入,因为后面还要平均。
第三步:对每个用户在每份试卷上的新分数求平均
SELECT uid, exam_id, ROUND(SUM(max_min) / COUNT(max_min), 0) AS avg_new_score
FROM ( ... ) t2
GROUP BY exam_id, uid做了什么?
GROUP BY exam_id, uid:按“试卷+用户”分组SUM(max_min) / COUNT(max_min):计算平均值(等价于AVG(max_min))ROUND(..., 0):四舍五入到整数位
示例(9001):
- 用户
1001:有两条记录,100和95.45→ 平均 ≈ 97.73 →ROUND(97.73, 0)= 98 - 用户
1003:只有一条0→ 平均 = 0 → 0
完全符合题目要求。
第四步:排序输出
ORDER BY exam_id, avg_new_score DESC- 先按试卷 ID 升序
- 再按归一化后平均分降序
四、常见错误与避坑指南
| 错误 | 后果 | 正确做法 |
|---|---|---|
忘记 score IS NOT NULL | 包含未交卷记录,影响 min/max | 必须过滤 score 为 NULL 的行 |
忘记 difficulty = 'hard' | 包含中等难度试卷 | 必须关联 examination_info 并筛选 |
直接用 AVG() 而不 ROUND(..., 0) | 小数部分保留,不符合“只保留整数” | 必须 ROUND(..., 0) |
不处理 min_x = max_x 的情况 | 分母为 0 报错 | 用 IF 判断边界情况 |
在窗口函数中用 GROUP BY | 逻辑混乱 | 窗口函数用于“每行计算”,不替代 GROUP BY |
五、核心知识点总结
1. Min-Max 归一化公式(牢记!)
new_value=old_value−minmax−min×(new_max−new_min)+new_minnew_value=max−minold_value−min×(new_max−new_min)+new_min
本题中:
new_min = 0new_max = 100- 所以简化为:
(score - min) * 100 / (max - min)
2. 窗口函数 vs 聚合函数
| 类型 | 是否减少行数 | 用途 |
|---|---|---|
MIN()/MAX() 聚合 | 是 | 配合 GROUP BY,返回一行 |
MIN() OVER() 窗口 | 否 | 每行都带上 min 值,用于后续计算 |
本题必须用窗口函数,因为我们要保留每一行记录,同时知道全局 min/max。
3. IF 条件判断(MySQL)
IF(条件, 真值, 假值)等价于其他数据库的 CASE WHEN。
🎯 六、举一反三
想要 Z-score 标准化?
(score - AVG(score) OVER (PARTITION BY exam_id)) / STDDEV(score) OVER (...)想要缩放到 [10, 90]?
IF(min_x = max_x, 50, (score - min_x) * 80 / (max - min) + 10)想要排除异常值?
先用 PERCENT_RANK() 或 Z-score 过滤离群值,再归一化。
