题目描述

✅ 1341. 电影评分

image-20260929080512142

image-20261003004605010

image-20260929080512307

image-20261003004605011

题意分析

查询两类结果:第一行是评价过最多电影的用户名,统计范围是全部评分记录;第二行是 2020 年 2 月平均评分最高的电影名,只统计该月评分。各自的指标并列时,选择名称字典序最小的一项。

Users 提供用户名称,Movies 提供电影名称,MovieRating 提供用户、电影、评分和日期。评分表以电影与用户的组合作为主键,因此同一用户对同一电影只有一条记录,按用户统计评分行数就等于评价电影数量。输出列名为 results,用户在前、电影在后。

解法:SQL 查询建模

核心思路

[!blue]

两个问题的分组对象、聚合指标和时间范围都不同,应分别求出各自的一行结果,再合并。不能先对所有评分统一筛月份,否则用户评价数量也会被错误限制在二月。

用户分支连接 MovieRating 与 Users,按用户编号和名称分组,用 COUNT(*) 统计电影数量。按数量降序、名称升序排序后取第一行,便同时完成最大值选择与并列时的字典序规则。

电影分支连接 MovieRating 与 Movies,先筛选日期,再按电影编号和片名分组求 AVG(rating)。日期范围使用左闭右开区间 [2020-02-01, 2020-03-01),既保留二月二十九日,也排除三月一日;然后按均分降序、片名升序取第一行。

两次连接都通过实体主键取名称,不会把一条评分复制成多条,聚合粒度仍分别是一位用户和一部电影。名称作为并列排序键只在指标相等时决定先后,不能先按名称选行再聚合。

让两个子查询各自完成排序与 LIMIT 1,外面加上顺序编号一和二,用 UNION ALL 保留两类结果。最外层再按编号排序、只输出 results,显式保证行顺序,不依赖两个子查询写在 SQL 中的先后。

解题步骤

  1. 用户子查询连接用户名,按全部评分记录计数,按次数降序、姓名升序取一行。
  2. 电影子查询先筛出 2020 年二月评分,再计算每部电影平均分,按均分降序、片名升序取一行。
  3. 两支结果分别附加 sort_order = 1 和 2,用 UNION ALL 拼接。
  4. 最外层按 sort_order 排序,只返回 results 列。

代码实现

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 为两个分支产生的分组总数。SQL 的实际复杂度取决于索引、连接方式和执行计划。

  • 在线性连接与聚合的计划下,处理评分记录为 $O(R)$,对分组结果完整排序为 $O(G\log(G+1))$;执行器也可能使用取首行优化。
  • 聚合与分组排序通常需要 $O(G)$ 级中间状态,具体内存及临时存储还取决于执行器。

关键点总结

[!green]

  • 用户按所有评分次数排名,电影仅按指定月份的平均分排名,统计范围不能混用。
  • 主键保证一位用户对同一电影只评一次,因此 COUNT(*) 对应评价电影数。
  • 两个子查询各自先完成双排序键和取首行,再合并结果。
  • 排名用于选择哪一行,外层顺序编号用于安排两类结果,职责不同。

易错点总结

[!yellow]

  • 电影使用评分总和会偏向评分记录多的电影,应比较平均分。
  • 先算全年平均再筛月份,月份外的评分已经混入,无法得到二月均分。
  • 把日期条件也用于用户分支,会少统计其他月份评价过的电影。
  • 缺少名称升序排序键,指标并列时无法保证选到字典序最小项。
  • 二月结束边界写到二十八日会漏掉闰日,使用次月一日之前更清楚。
  • 只给子查询排序,不明确外层行顺序,不能保证用户结果在电影前面。

相似题目

题目 难度 关联与区别
184. 部门工资最高的员工 中等 同样先按组聚合再选择最高指标,本题分别按评分次数及平均评分筛选,并处理字典序并列。
178. 分数排名 中等 排序依据来自聚合后的结果,本题选首项并按名称打破并列,原题给每条分数分配密集名次。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2021/91268711
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!