LeetCode 1341. 电影评分
题目描述
题意分析
有三张表:
Movies(movie_id, title)、Users(user_id, name)、MovieRating(movie_id, user_id, rating, created_at)。要返回恰好两行,列名固定为results:第一行是评论电影数量最多的用户名,第二行是 2020 年 2 月平均评分最高的电影名。两问都要求并列时取名称字典序最小的那个。最需要先看清的是输出形态:结果是一个单列两行的表,而且两行的顺序是固定的——先用户名后电影名。这意味着两个毫不相干的统计要被拼接到同一个结果集里,而不是各返回各的。
两个子问题的粒度不同。第一问按用户分组,度量是评分记录条数,统计范围是全部记录,不限时间。第二问按电影分组,度量是评分的算术平均,统计范围只限 2020 年 2 月。把「分组键 + 度量 + 过滤范围」这三要素分别写清楚,两个子查询就各自成型了。
并列规则是本题的主要陷阱:两问都明确说了「如果并列,取名字字典序较小的」。这不是可有可无的容错,示例数据里两问都会出现并列,不写第二排序键必然出错。
还要注意「评论数量最多」数的是记录条数而不是去重后的电影数,「平均评分最高」用的是算术平均而不是评分总和——用总和排序会让评分次数多的电影占便宜,结果不同。
边界要留意三点:日期过滤要覆盖 2 月的全部日期且不能漏掉 2 月 29 日(2020 是闰年);两个子查询各自只取一行;
Users和Movies表里可能有从未出现在MovieRating中的记录,它们不该被统计进来。
解法:SQL 查询建模
核心思路
两个结果来自不同的统计粒度,应该分别求出后再合并:第一问按用户统计全部评分记录数;第二问只取 2020 年 2 月的数据,按电影计算平均评分。
每个子查询都采用同一套选第一名规则:先按指标降序,再按名称升序,最后
LIMIT 1。这样指标并列时会稳定选择字典序最小的名称。日期使用半开区间
created_at >= '2020-02-01' AND created_at < '2020-03-01'。它完整覆盖闰年的 2 月 29 日,也兼容DATETIME的时间部分。两行用
UNION ALL合并。不能用UNION,因为用户名可能恰好等于电影名,去重会错误地只保留一行。为明确保持“用户在前、电影在后”,给两行附加排序号,最外层只输出results并按排序号排列。结果不变量:第一个派生表至多保留一名“评分记录数最多且名称最小”的用户;第二个派生表至多保留一部“2 月平均分最高且片名最小”的电影;
UNION ALL不改变这两行的内容和数量。正确性:用户子查询对全部评分逐用户计数,排序规则与第一问完全一致;电影子查询先过滤月份再求平均,排序规则与第二问一致。两个局部最优结果按规定次序拼接,因此最终两行分别且准确回答两问。
解题步骤
- 连接
MovieRating与Users,按用户分组,使用COUNT(*) DESC, name ASC排序并取一行。- 连接
MovieRating与Movies,先用半开区间筛选 2020 年 2 月,再按电影分组,使用AVG(rating) DESC, title ASC排序并取一行。- 为两行分别标记排序号 1、2,用
UNION ALL合并。- 最外层按排序号排序,只返回列
results。官方样例中 Daniel 与 Monica 都有 3 条评分,名称升序选择 Daniel;Frozen 2 与 Joker 的 2 月平均分同为 3.5,片名升序选择 Frozen 2。
若用户和电影都叫
Joker,仍必须输出两行,因此只能使用UNION ALL。2 月 29 日23:59:59的评分满足半开区间,而 3 月 1 日00:00:00不满足。
代码实现
SELECT results
FROM (
SELECT top_user.results, 1 AS sort_order
FROM (
SELECT u.name AS results
FROM MovieRating AS r
JOIN Users AS u ON u.user_id = r.user_id
GROUP BY r.user_id, u.name
ORDER BY COUNT(*) DESC, u.name ASC
LIMIT 1
) AS top_user
UNION ALL
SELECT top_movie.results, 2 AS sort_order
FROM (
SELECT m.title AS results
FROM MovieRating AS r
JOIN Movies AS m ON m.movie_id = r.movie_id
WHERE r.created_at >= '2020-02-01'
AND r.created_at < '2020-03-01'
GROUP BY r.movie_id, m.title
ORDER BY AVG(r.rating) DESC, m.title ASC
LIMIT 1
) AS top_movie
) AS ranked
ORDER BY sort_order;
复杂度分析
- 时间复杂度:设评分记录数为
R,用户组与电影组总数为G。连接键有索引时,扫描和聚合约为 $O(R)$,组内结果排序为 $O(G\log G)$。- 空间复杂度:$O(G)$,用于聚合与排序中间结果;具体实现取决于数据库执行计划。
关键点总结
- 两问的分组键与过滤范围不同,应分别聚合后再拼接。
- “指标最大、名称最小”对应
ORDER BY metric DESC, name ASC LIMIT 1。- 月份过滤必须发生在
AVG之前,半开区间同时处理闰日和时间分量。UNION ALL保留语义不同但文本可能相同的两行;外层排序号保证行序。
易错点总结
- 使用
UNION:同名用户和电影会被去重,结果不足两行。- 月份条件写进
HAVING:平均值已经使用全量记录计算,过滤时机过晚。- 使用评分总和:评分次数多会抬高总和,题目比较的是算术平均值。
- 省略第二排序键:指标并列时数据库可返回任意一行。
- 日期写到 2 月 28 日或 29 日午夜:会漏掉闰日或当天带时间的记录;应使用小于 3 月 1 日的半开区间。
- 按用户名而非
user_id分组:同名的不同用户会被错误合并。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 175. 组合两个表 | 简单 | 必须用 LEFT JOIN 保留无地址的人,是「主表该选谁」的最基础练习 |
| 181. 超过经理收入的员工 | 简单 | 同一张表自连接,靠别名区分员工与经理两个角色 |
| 182. 查找重复的电子邮箱 | 简单 | 分组后用 HAVING COUNT(*) > 1 筛组,正好是本题「筛行还是筛组」的反面例子 |
| 183. 从不订购的客户 | 简单 | 反连接场景,NOT IN 遇到 NULL 会整体失效,需换 NOT EXISTS 或左连接判空 |
| 1280. 学生们参加各科测试的次数 | 简单 | 需要先做学生与科目的交叉连接补全零次记录,再左连接计数 |
| 176. 第二高的薪水 | 中等 | 用 LIMIT 1 OFFSET 1 取第二名,还要在无结果时返回 NULL 而非空集 |
| 177. 第N高的薪水 | 中等 | 把上题参数化写进函数,OFFSET 不接受表达式需先算好变量 |
| 178. 分数排名 | 中等 | 并列同名次且名次不跳号,正是 DENSE_RANK() 与 RANK() 的分界 |
| 184. 部门工资最高的员工 | 中等 | 每组取最大且并列全保留,与本题「并列只取一个」形成对照 |
| 185. 部门工资前三高的所有员工 | 困难 | 分组取前 N,LIMIT 无法按组生效,必须用 DENSE_RANK() 窗口函数 |
| 570. 至少有5名直接下属的经理 | 中等 | 自连接后按经理分组,用 HAVING 对聚合结果设阈值 |
| 197. 上升的温度 | 简单 | 按日期差自连接,需要用 DATEDIFF 而非直接减,体现日期函数的正确用法 |
| 1204. 最后一个能进入巴士的人 | 中等 | 需要累计求和的窗口函数,再筛出总重不超限的最后一行 |
| 1661. 每台机器的进程平均运行时间 | 简单 | 同样是分组求平均,但要先把开始与结束两行配对成一条耗时记录 |