目录

题目描述

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 是闰年);两个子查询各自只取一行;UsersMovies 表里可能有从未出现在 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 不改变这两行的内容和数量。

正确性:用户子查询对全部评分逐用户计数,排序规则与第一问完全一致;电影子查询先过滤月份再求平均,排序规则与第二问一致。两个局部最优结果按规定次序拼接,因此最终两行分别且准确回答两问。

解题步骤

  1. 连接 MovieRatingUsers,按用户分组,使用 COUNT(*) DESC, name ASC 排序并取一行。
  2. 连接 MovieRatingMovies,先用半开区间筛选 2020 年 2 月,再按电影分组,使用 AVG(rating) DESC, title ASC 排序并取一行。
  3. 为两行分别标记排序号 1、2,用 UNION ALL 合并。
  4. 最外层按排序号排序,只返回列 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. 每台机器的进程平均运行时间 简单 同样是分组求平均,但要先把开始与结束两行配对成一条耗时记录