新闻详情

新闻详情

首页 / 资讯中心 / 详情

刷透LeetCode 1341:SQL分组聚合与并列排序的坑一次踩完

发布时间:2026/9/28 14:04:21来源:尧图网络
刷透LeetCode 1341:SQL分组聚合与并列排序的坑一次踩完
刷完这题你大概能把SQL面试里一半的坑都踩一遍。LeetCode 1341《电影评分》贴的是Easy标签但它把表连接、分组聚合、日期筛选、并列排序这几个考点全揉在一起了没刷明白之前我身边不少朋友在这题上折过好几次。这题表面是“查电影评分”实际上考的是你能不能真正理解 SQL 的执行顺序、GROUP BY 和排序的配合以及多表关联时怎么控制数据范围。这篇文章我会把 1341 从审题、拆解、写 SQL、踩坑到提交完整复盘一遍你照着走一遍基本就能吃透。1. 先把这个题目的考点看穿1.1 三张表的关系建模主键设计就已经在给你提示题目给了三张表Movies、Users、MovieRating。Movies 表存电影基本信息Users 表存用户基本信息MovieRating 表存用户对电影的评分记录。很多新手一上来就急着写 SQL但没先看表结构结果后面越写越乱。MovieRating 表是这题的核心它有三列看起来普通实际很关键movie_id、user_id、rating、created_at并且 (movie_id, user_id) 是复合主键。复合主键意味着什么意味着同一个用户对同一部电影只能有一条评分记录。这一点在题目描述里可能不起眼但这个约束直接影响你后面怎么写聚合逻辑。因为你不需要担心 COUNT 出来的评分记录会因重复评分而翻倍也不需要加 DISTINCT 去防止重复行。如果表不设复合主键而是每行由一个独立的 rating_id 标识那同一用户对同一部电影可能有多条记录业务语义就完全是另一回事了。用现实场景类比就像豆瓣上同一个账号对同一部电影只能打分一次。这样设计表结构才能保证“评分次数”这种统计是稳定且可信的。1.2 题目里的两个“口语化”需求到底在问什么题目要求输出两个结果第一句是“查找评价电影数量最多的用户名”第二句是“查找 2020 年 2 月平均评分最高的电影标题”。把这两句话翻译成 SQL 操作其实就是要你干三件事第一问里“评价电影数量最多”的本质是把评分记录按用户分组统计每个人的评分条数然后按条数降序排取第一条。第二问里“2020 年 2 月平均评分最高”的本质是先筛选出 2020 年 2 月的所有评分记录再把评分记录按电影分组计算每部电影的平均评分然后按平均分降序排取第一条。两个问题都需要处理“并列”情况。题目明确说如果有多个用户评价数量相同取字典序最小的用户名如果有多个电影平均分相同取字典序最小的电影标题。这里最容易理解错的是“评价电影数量”这个表述。注意它说的是“评价电影的数量”不是“评分分数的总和”也不是“被评分电影的 ID 加起来”。所以聚合函数用 COUNT不要用 SUM。第二个问题里容易忽略的是“2020 年 2 月”这个时间窗口。不是所有评分都参与平均只有 created_at 落在 2020 年 2 月的记录才算。很多人第一遍没带这个条件后面发现结果对不上再回头看题目才反应过来。1.3 SQL 执行顺序这道题 80% 的错误来自记错执行顺序我刷 SQL 题有个习惯不管题目多简单先在心里过一遍 SQL 语句的执行顺序。SQL 的书写顺序是 SELECT、FROM、WHERE、GROUP BY、HAVING、ORDER BY、LIMIT但真正执行顺序是 FROM、JOIN、WHERE、GROUP BY、HAVING、SELECT、ORDER BY、LIMIT。这和你直觉上可能完全不一样。比如很多人以为 WHERE 是最后才执行的其实 WHERE 在 GROUP BY 之前就执行了。所以第二个查询里对 2020 年 2 月的条件过滤必须在 WHERE 里做不能在 HAVING 里做更不能在 SELECT 计算平均值之后再做。同样ORDER BY 在 SELECT 之后执行所以 SELECT 里定义的别名可以在 ORDER BY 中使用但一共WHERE、GROUP BY 阶段的别名是不生效的。这些细节你初看觉得“不就是个 Easy 题吗”但把这些执行顺序理顺以后遇到大数据的慢查询和复杂报表时排查问题的速度会快很多。2. 第一个查询找谁打分打得多2.1 最朴素的拆解先 JOIN 再分组计数第一个查询的目标用户是“评价电影数量最多的人”。评价记录都在 MovieRating 表里名字在 Users 表里所以第一步就是把这两张表关联起来。连接条件很简单SELECT u.name FROM Users u JOIN MovieRating mr ON u.user_id mr.user_id这里我直接用 INNER JOIN。因为我们要找的是“给电影评价过的用户”如果一个用户一条评分记录都没有那他不可能是“评价数量最多的人”用 INNER JOIN 反而更精确还能过滤掉那些从不评分的用户。接着按用户分组统计每个人有多少条评分记录SELECT u.name FROM Users u JOIN MovieRating mr ON u.user_id mr.user_id GROUP BY u.user_id, u.name ORDER BY COUNT(mr.movie_id) DESC LIMIT 1;GROUP BY 里我写了 u.user_id 和 u.name 两列。很多人会问只按 u.user_id 分组不行吗在多数数据库里用户 ID 理论上唯一决定用户名只要 u.name 和 u.user_id 在同一张 Users 表里按 u.user_id 分组后u.name 其实也没有歧义。但 MySQL 在启用了 ONLY_FULL_GROUP_BY 之后SELECT 里的非聚合字段必须出现在 GROUP BY 中或者被聚合函数包裹。为了跨数据库兼容和减少报错把 u.name 也加进 GROUP BY 是最稳妥的写法。COUNT(mr.movie_id) 里我数的是 movie_id 而非 rating。按我们前面说的复合主键约束这里数 movie_id 和数 * 的结果完全一样不会因为 NULL 产生差异因为 movie_id 是主键的一部分非空且唯一。2.2 并列与字典序ORDER BY 两个字段的妙处第一个查询里有个极其容易漏掉的细节题目说如果有多个用户评价数量相同返回字典序最小的名字。什么叫字典序在 SQL 里就是按字符串排序。字典序最小就是字符串比较后最靠前的那一个。如果我只写ORDER BY COUNT(mr.movie_id) DESC LIMIT 1;那遇到并列时数据库返回哪一行是不确定的。在 MySQL 里遇到并列时可能会返回先扫描到的那一条但“先扫描到哪一条”受执行计划影响完全不可控。这次提交能过下次可能就挂了。正确做法是把排序条件写成两层ORDER BY COUNT(mr.movie_id) DESC, u.name ASC先按评分数量降序排数量相同的人再按名字升序排。因为名字升序排完以后字典序最小的已经在最上面LIMIT 1 拿到的必然是正确的。这条经验以后写业务报表也用得上。比如后端要做一个用户贡献榜单只看贡献值排序贡献值一样时按注册时间排本质上就是这个套路。2.3 这是我踩过的坑为什么不直接找 MAX(COUNT(...))我第一次做这题时第一反应是用子查询找出最多的评价数量然后再查谁的数量等于这个最大值。思路看着对但写起来很别扭SELECT u.name FROM Users u JOIN MovieRating mr ON u.user_id mr.user_id GROUP BY u.user_id, u.name HAVING COUNT(mr.movie_id) ( SELECT MAX(cnt) FROM ( SELECT COUNT(movie_id) AS cnt FROM MovieRating GROUP BY user_id ) t );这种写法在逻辑上确实能找到所有并列的用户但要套两层子查询读起来费劲性能也不如 LIMIT 写法好。而且如果题目要求“返回一个用户”你还要再在外面包一层取最小名字SQL 层级会非常深。所以刷到“取最大/最小那一行”的题型优先考虑 ORDER BY LIMIT 1这个组合比 MAX HAVING 子查询更直接。当然如果你要的不是一行而是所有并列的行那就不能无脑用 LIMIT 1得用窗口函数或者上述 HAVING 方法。题目不同方案不同不要一套公式走天下。3. 第二个查询找出 2020 年 2 月评价最高的电影3.1 先过滤日期再做聚合WHERE 的时机第二个查询比第一个多了一个条件只看 2020 年 2 月的数据。这里很多人写错是因为把平均分算完之后才想起来要过滤日期然后就顺手往 HAVING 里加条件。比如这样SELECT m.title FROM Movies m JOIN MovieRating mr ON m.movie_id mr.movie_id GROUP BY m.movie_id, m.title HAVING AVG(mr.rating) ALL (...)但你有没有想过HAVING 只能作用于聚合之后的结果集它没法把“3 月的评分”从平均值计算里剔除。你必须在聚合之前就只留下 2020 年 2 月的评分记录否则平均值被其他月份的评分污染了结果肯定偏。正确的位置是在 GROUP BY 之前用 WHERE 过滤SELECT m.title FROM Movies m JOIN MovieRating mr ON m.movie_id mr.movie_id WHERE mr.created_at BETWEEN 2020-02-01 AND 2020-02-29 GROUP BY m.movie_id, m.title ORDER BY AVG(mr.rating) DESC, m.title ASC LIMIT 1;这里 JOIN 先于 WHERE 执行所以 WHERE 条件完全可以引用 MovieRating 表的 created_at 字段。先通过 JOIN 找到电影标题再在聚合前把非 2 月的记录排除逻辑上无懈可击。3.2 手算一遍题目示例看懂并列是怎么产生的光看代码印象不深我把题目示例里的数据搬出来手算一遍你就知道为什么第二个查询结果会是“Frozen 2”而不是别的。2020 年 2 月的评分记录有这些电影用户评分评分日期Avengers242020-02-11Avengers322020-02-12Frozen 2152020-02-17Frozen 2222020-02-01Joker132020-02-22Joker242020-02-25Avengers 在 2 月的平均分 (4 2) / 2 3Frozen 2 在 2 月的平均分 (5 2) / 2 3.5Joker 在 2 月的平均分 (3 4) / 2 3.5Frozen 2 和 Joker 平均分相同都是 3.5。按题目要求平均分相同就取字典序最小的标题。Frozen 2 的首字母是 FJoker 的首字母是 JF 排在 J 前面所以正确答案是 Frozen 2。这个例子也提醒我题目示例的并列不是随机的就是专门设来检查你会不会处理并列的。如果没在 ORDER BY 里把标题排序条件加上这题在 LeetCode 上很可能得出 Joker然后提交失败。3.3 取“评分最高”时别把排序方向写反这个看着像低级错误但我在评论区见过不少次。ORDER BY AVG(mr.rating) 默认是升序即从小到大排。如果你忘了写 DESCLIMIT 1 取到的是平均分最低的电影而不是最高的。有些中等题甚至会在你只写 ASC 时给你报错因为结果完全不同。我一般写完排序后会自己口头验证一遍最高分应该是第一个还是最后一个。这个习惯救了我好几次。不要光看 SQL 语义数据分析里“取最高的”和“取飙升最猛的”是两种完全不同的场景排序方向写反了整个报表就废了。3.4 日期边界怎么取才稳妥2020 年 2 月这个条件我一上来写的是 BETWEEN 2020-02-01 AND 2020-02-29因为 2020 年是闰年2 月有 29 天。但这里有两个值得注意的细节。BETWEEN 是包含边界的所以上面的写法会包含 2020-02-01 和 2020-02-29 这两天的全部记录正确。但如果你写 BETWEEN 2020-02-01 AND 2020-03-01就会把 3 月 1 日的评分也圈进来多算一天的数据平均分可能被带偏。更通用、更不容易踩闰年坑的写法是半开区间WHERE mr.created_at 2020-02-01 AND mr.created_at 2020-03-01这个写法在碰到任意“某年某月”的时候都成立不用去管这个月有 28 天、29 天、30 天还是 31 天。你只要把区间写成 [月初, 下月月初)数据库自然会把这个月所有日期捞出来。这也是我在生产环境写月报统计时最常用的模式。4. 合并查询最终提交的完整答案4.1 为什么两个查询靠 UNION 组合而不是硬拼成一大条第一问返回用户名字第二问返回电影标题两个结果集各只有一列行数也各只有一行。LeetCode 最终期望的输出是一个两行的结果集第一行是用户第二行是电影。要把两个 SELECT 的结果“纵向拼起来”最直接的工具就是 UNION。有人可能会想能不能用一条超复杂 SQL 一次搞定可以但没必要。写两个查询再用 UNION 连接可读性、维护性、排错性都更好。这里解释一下 UNION 和 UNION ALL 的差别。UNION 默认会对结果集去重UNION ALL 不去重。本题两个查询各返回一行且用户行和电影行内容不同不存在重复所以 UNION 和 UNION ALL 效果一样。但如果你觉得“结果集内不可能重复”直接用 UNION ALL 更安全因为它跳过去重步骤执行理论上也更快一点。4.2 完整代码把两个查询放进 UNION ALL 里注意别名要统一这里我把两列都起名为 results( SELECT u.name AS results FROM Users u JOIN MovieRating mr ON u.user_id mr.user_id GROUP BY u.user_id, u.name ORDER BY COUNT(mr.movie_id) DESC, u.name ASC LIMIT 1 ) UNION ALL ( SELECT m.title AS results FROM Movies m JOIN MovieRating mr ON m.movie_id mr.movie_id WHERE mr.created_at 2020-02-01 AND mr.created_at 2020-03-01 GROUP BY m.movie_id, m.title ORDER BY AVG(mr.rating) DESC, m.title ASC LIMIT 1 );我特别给两个子句加了括号因为 MySQL 里如果 UNION 前后的 SELECT 各自带 ORDER BY 和 LIMIT必须用括号包起来否则 ORDER BY 可能作用到整个 UNION 的结果集上导致语义错误。这是很多人直接把两个查询用 UNION ALL 一拼就提交、结果报语法错误的原因。4.3 运行结果验证为什么列名也叫 results执行上面的 SQL系统会返回一个两行的表每行一个字符串resultsDanielFrozen 2为什么输出列名是 results因为我在两个 SELECT 里都写了 AS results。LeetCode 对最终列名没有硬性要求但你要保证两段 SELECT 的列名一致UNION 才能把它合并成一个统一的列标题。如果第一个写 AS name第二个写 AS title在一些数据库里不报错但结果集的表头会奇怪而且在 LeetCode 的判定环境下不够整洁。这里也顺带验证了第一问为什么答案是 Daniel示例数据里有三位用户评过分Daniel 评了 3 部Monica 也评了 3 部两人并列最多按字典序 Daniel 排在 Monica 前面所以取 Daniel。5. 刷完这题值得记住的四个通用经验5.1 遇到“并列 取一个”时永远记得把并列条件写进 ORDER BY这题的考点虽然是电影评分但背后是一个特别大的题型族群分组后取 Top NTop N 里又有并列。无论是“销量最高的商品”“评分最高的餐厅”还是“活跃度最高的用户”只要题目提到“如果有多个并列取 XX 最小/最大”你就要立刻联想到 ORDER BY 字段1 DESC, 字段2 ASC。很多人平时写业务代码没有这个习惯遇到并列就取一条随机结果短时间看不出问题时间长了线上数据不稳定用户会莫名其妙看到不同的排榜结果。所以我把这条放在最前面它比 SQL 语法本身更影响数据质量。5.2 COUNT(*) 和 COUNT(某列) 不要凭感觉乱用在分组统计里COUNT(*) 统计的是分组内的行数COUNT(mr.movie_id) 统计的是该列非 NULL 的行数。在本题里movie_id 是主键的一部分不会有 NULL所以两者相同。但换个场景如果某张表的某一列允许为空COUNT(该列) 会跳过 NULL这时候统计口径就不一样了。我的习惯是想数“行数”就用 COUNT(*)想数“某个字段有值”的数量才用 COUNT(字段)。千万别因为“我一直这么写没出过错”就忽略了这点NULL 带来的坑往往在数据量大了以后才爆出来。5.3 WHERE 里别用 SELECT 中的别名执行顺序告诉我们SELECT 阶段在 WHERE 之后才执行。你给某个表达式起了别名WHERE 里想直接用这个别名数据库大概率会报“Unknown column”。别问我为什么知道这题评论区里关于“WHERE AVG(rating) 4 为什么报错”的帖子我翻到过很多次。AVG(rating) 是聚合计算它是在 GROUP BY 阶段产生的WHERE 阶段根本还没聚合当然也不能引用它的结果。你只能在 HAVING 里对聚合结果做条件判断。5.4 生产环境里这三张表该建什么索引我个人刷题时不会只盯着“能不能通过”还会想如果这张表有百万条评分数据线上会怎么跑这时候索引策略就得跟上。MovieRating 表是两张维表之间的桥梁它的查询主要发生在两个方向按 user_id 分组统计、按 created_at 过滤后按 movie_id 分组。所以最基础的做法是在这个表上建两三个索引索引目标建议类型按用户统计评分次数普通索引 (user_id, movie_id)按时间窗口统计电影评分普通索引 (created_at, movie_id)与 Movies、Users 表关联主键自然覆盖 (movie_id, user_id)索引不是越多越好每多一个索引插入和更新都会变慢。但表和数据量大的时候没有索引的评分记录表跑一次月报聚合能把数据库 CPU 拉满。这个意识在面试里稍微带一句面试官通常会觉得你有真实项目经验。6. 错题回顾与自测排查6.1 为什么我的结果只有一行如果你把两个查询用 UNION ALL 拼接后只返回一行大概率是其中一个子句的 LIMIT 或排序把整条结果链带偏了。检查一下是不是忘了给两个 SELECT 分别加括号。没有括号时MySQL 可能把后一个 ORDER BY 和 LIMIT 解析成对 UNION 整体生效这样第二个子句的行可能被过滤掉。6.2 为什么第二行返回了 Joker 而不是 Frozen 2说明平均分排序没有处理并列字段。题目要求平均分相同时取字典序最小的标题你必须写 ORDER BY AVG(mr.rating) DESC, m.title ASC只写平均分排序不够。如果之前确实写了再检查日期条件是不是把 3 月的记录带进来了导致平均分重新计算Frozen 2 不再排第一。6.3 为什么第一个查询返回了 Monica 而不是 Daniel这是同一个病因的不同表现只按数量排序没按名字升序排。Daniel 和 Monica 都是 3 条评分记录但字典序是 Daniel 在前。补上 u.name ASC 后LIMIT 1 就会稳定取到 Daniel。6.4 我的 SQL 里用了 DISTINCT有必要吗本题不需要。因为复合主键保证了同一用户和同一电影之间评分记录唯一DISTINCT 既不会改变结果也掩盖不了我们对表结构约束的理解。如果换一张没有唯一约束的表你可能真要小心重复数据但那属于数据质量问题和题目本身无关。我这题刷完最大的感受是LeetCode 的题目看着是给你练算法的但 1341 这种题更像数据库基本功的体检。你平时写 CRUD 写得多未必真理解 GROUP BY 执行时机和 ORDER BY 的确定性。把这道题吃透以后再做那些“连续登录天数”“考勤打卡统计”之类的 SQL 题会顺很多。你自己拿示例数据跑一遍再把并列排序和日期边界这两个细节记牢基本就可以把 1341 稳稳拿下了。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

BMS绝缘检测与继电器诊断:从原理到标定的实战指南 2026/9/28 15:47:32

BMS绝缘检测与继电器诊断:从原理到标定的实战指南

BMS做久了你会发现一个有意思的现象:SOC算不准、SOP估偏了,用户最多抱怨两句,但如果绝缘检测误报、继电器诊断漏判,那直接就是安全事故。我经手的几个量产项目里,DV/PV阶段出问题最多的恰恰不是算法模块,而…

阅读更多 →
PICkit4烧录PIC16F15355开发板接线与排查实战指南 2026/9/28 15:47:32

PICkit4烧录PIC16F15355开发板接线与排查实战指南

上周有个同事拿着刚焊好的PIC16F15355最小系统板来找我,说芯片换了好几片,程序就是烧不进去。我拿万用表量了两分钟,发现问题不在芯片,也不在代码,而是PICkit4跟目标板之间的接线完全接反了。这情况在Microchip的入门圈…

阅读更多 →
YOLO26:面向仓储物流的轻量级目标检测工程实践 2026/9/28 15:47:26

YOLO26:面向仓储物流的轻量级目标检测工程实践

1. 这不是又一个YOLO复刻项目:为什么“YOLO26”在箱子与仓库场景里真能跑起来你搜“YOLO26”,满屏都是“RTX3060性能数据”“yolo26导入电脑摄像头视频”“未安装 pyside6。请运行:python -m pip install pyside6”——但没人告诉你,YOLO26根…

阅读更多 →
人员徘徊检测数据集解析:三种标注格式转换与YOLO训练避坑指南 2026/9/28 15:47:20

人员徘徊检测数据集解析:三种标注格式转换与YOLO训练避坑指南

简介:面向目标检测与行人行为分析任务,这份人员徘徊行走观望检测数据集共2926张实拍图片,覆盖“正常行走”与“观望”两类目标,手动标注精准,目标尺度与场景背景多样,适用于课程设计、科研实验及比赛项目。…

阅读更多 →
YOLOv8课堂行为检测实战:从数据标注到边缘部署全流程 2026/9/28 15:47:20

YOLOv8课堂行为检测实战:从数据标注到边缘部署全流程

课堂行为检测,听起来是个很具体的需求,实际动手做一轮才发现,它几乎能把“数据标注、模型训练、部署推理、边缘优化”整个目标检测流程都串起来。最近我基于YOLOv8深度学习目标检测算法做了一套课堂行为检测系统,从最初的方案选型…

阅读更多 →
AI能写编译器,Electron为何仍卡顿?架构优化与技术选型解析 2026/9/28 15:47:20

AI能写编译器,Electron为何仍卡顿?架构优化与技术选型解析

1. “AI 写编译器”这件事,为什么会和 Electron 卡顿扯到一起最近技术圈有两件事放在一起看特别有意思:一边是 AI 模型已经能生成完整的编译器代码,甚至能按指令产出带优化 pass 的后端;另一边是很多桌面用户还在吐槽“为什么一个…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉