LeetCode 1280. 学生们参加各科测试的次数
题目描述
题意分析
手上有三张表:学生表只有编号和姓名,科目表只有科目名,考试表是一条条「谁考了哪科」的流水,同一个人同一科可以出现多次。要交出的东西是一张固定形状的报表:每个学生对每门科目各占一行,附带这个人考这门课的次数。
关键信号藏在「即使一次都没考也要出现」这句话里。结果的行数由学生表和科目表共同决定,和考试流水里出现过哪些组合无关;考试流水只负责往这张已经定好形状的报表里填数字。这两件事必须分开做,否则很容易写成「流水里有什么就输出什么」。
边界主要有三处:某个学生一门课都没考过,他仍要出现三行且全为 0;某门科目谁都没考过,这一列也不能消失;同一个学生同一科的多条流水要累加而不是去重。输出还要求先按学生编号、再按科目名升序。
解法:笛卡尔积 + 左连接聚合
核心思路
结果必须包含每个「学生 × 科目」组合,即使该组合在考试流水中从未出现。因此不能从
Examinations出发聚合;不存在的组合无法凭空补回。先用
Students CROSS JOIN Subjects建立完整结果骨架,再把Examinations左连接进来:
CROSS JOIN保证每个学生都有每门科目;LEFT JOIN保证没有考试记录的组合仍保留;COUNT(e.subject_name)只统计右表的非空匹配,未参加时结果自然为 0。连接条件必须同时包含学生编号和科目名,并写在
ON中。若把右表条件放进WHERE,补出的空行会被过滤,左连接就退化成内连接。
解题步骤
- 对
Students与Subjects做笛卡尔积,得到报表的全部行。- 按学生编号、科目名左连接考试流水。
- 按学生编号、姓名、科目名分组,用
COUNT(e.subject_name)统计匹配记录数。- 按学生编号、科目名升序排列结果。
若有 4 名学生、3 门科目,骨架固定为 12 行。某个组合没有考试记录时,左连接仍保留一行,只是
e.subject_name为NULL,所以COUNT(e.subject_name)返回 0。
代码实现
SELECT
s.student_id,
s.student_name,
sub.subject_name,
COUNT(e.subject_name) AS attended_exams
FROM Students AS s
CROSS JOIN Subjects AS sub
LEFT JOIN Examinations AS e
ON e.student_id = s.student_id
AND e.subject_name = sub.subject_name
GROUP BY s.student_id, s.student_name, sub.subject_name
ORDER BY s.student_id, sub.subject_name;
复杂度分析
- 设学生数为 $S$、科目数为 $C$、考试记录数为 $E$。结果本身就有 $SC$ 行,因此至少需要 $O(SC)$ 的处理与输出成本。
- 在哈希连接或连接列有合适索引时,连接与聚合通常可在 $O(SC + E)$ 量级完成;最终排序约为 $O(SC log(SC))$。具体执行代价取决于数据库优化器和索引。
- 中间结果与分组结构需要 $O(SC + E)$ 量级空间,具体同样由执行计划决定。
关键点总结
- 「零次也要展示」意味着先构造维度组合,再左连接事实表。
COUNT(列)忽略NULL,而COUNT(*)会把左连接补出的空行也计为 1。- 外连接右表的匹配条件应留在
ON中;放进WHERE会删除未匹配行。- 分组粒度必须与结果粒度一致,即一个学生的一门科目一行。
- 面试追问若考试表很大,可先按
student_id, subject_name预聚合,再与骨架连接,减少连接阶段的行膨胀。
易错点总结
- 从考试表直接分组:从未参加考试的学生或科目组合不会出现。
- 使用内连接:零次记录被整体丢弃。
- 写成
COUNT(*):无匹配组合也会被计为 1。- 只按学生编号连接:同一学生不同科目的记录会混在一起。
- 把右表过滤条件写入
WHERE:左连接退化成内连接。- 遗漏科目名排序:同一学生内的输出顺序不满足题目要求。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 175. 组合两个表 | 简单 | 单次左连接保留主表全部行,无需笛卡尔积骨架 |
| 183. 从不订购的客户 | 简单 | 反向利用左连接后的 NULL 做差集筛选 |
| 570. 至少有5名直接下属的经理 | 中等 | 自连接后用 HAVING 对聚合结果加阈值 |
| 1341. 电影评分 | 中等 | 多表连接后分组取极值并用 UNION 合并两问 |