WHERE子查询匹配最大值最简但有陷阱:全表适用,分组需相关子查询;JOIN聚合更高效;MySQL 8.0+推荐ROW_NUMBER()窗口函数;老版本可用LEFT JOIN自关联兜底。

WHERE 中用子查询匹配最大值,最简但有陷阱
直接写 WHERE score = (SELECT MAX(score) FROM students) 能拿到最高分那几行,但只适用于「全表找最大」这种单层场景。一旦涉及分组(比如每个班级最高分),它就失效——子查询返回的是全局最大值,不是每组最大值。
常见错误现象:Subquery returns more than 1 row 或结果为空。这是因为你忘了加相关条件,子查询没和外层行绑定。
如果真要沿用 WHERE + 子查询,必须写成相关子查询:
SELECT s1.* FROM students s1 WHERE s1.score = ( SELECT MAX(s2.score) FROM students s2 WHERE s2.class = s1.class );
- 子查询里必须出现
s2.class = s1.class,否则就是查全表最大,不是每班最大 - 性能差:每行都触发一次子查询,10 万行 = 执行 10 万次子查询
- NULL 值会中断匹配:若某班所有
score都为NULL,该班整组消失
JOIN 聚合子查询,适合 MySQL 5.7+ 且需稳定结果
先用子查询算出每组最大值,再跟原表 JOIN 匹配,比相关子查询快,也更容易加索引。
关键点是:必须同时匹配分组字段和最大值字段,缺一不可。
SELECT s1.* FROM students s1 INNER JOIN ( SELECT class, MAX(score) AS max_score FROM students GROUP BY class ) s2 ON s1.class = s2.class AND s1.score = s2.max_score;
- 如果某班有多个学生并列最高分,这条语句会全部返回——这是正确行为,不是 bug
- 若业务要求“只取一条”,得额外加
AND s1.id = (SELECT MIN(id) FROM students s3 WHERE s3.class = s1.class AND s3.score = s1.score) - 索引建议建
(class, score, id),覆盖查询所有字段,避免回表
MySQL 8.0+ 推荐用 ROW_NUMBER(),逻辑最干净
窗口函数不依赖字段唯一性,也不怕重复值,ORDER BY 里加主键就能稳住顺序。
别用 RANK() 或 DENSE_RANK():它们对相同分数分配同序号,rn = 1 可能返回多行,而你未必想要。
SELECT id, name, class, score
FROM (
SELECT id, name, class, score,
ROW_NUMBER() OVER (PARTITION BY class ORDER BY score DESC, id ASC) AS rn
FROM students
) t
WHERE rn = 1;
-
PARTITION BY class定义分组,不是GROUP BY,不会丢字段 -
ORDER BY score DESC, id ASC确保并列时按主键取最小 id 的那条,结果可预期 - 大数据量时注意:全表扫描 + 排序开销大,
(class, score, id)复合索引能显著提速
LEFT JOIN 自关联,老版本 MySQL 的兜底方案
在没有窗口函数的 MySQL 5.6/5.7 里,这是最可靠、不依赖聚合也不怕 NULL 的办法。
核心逻辑:对每条记录,检查同组是否存在更大值;找不到,说明它就是本组最大。
SELECT s1.* FROM students s1 LEFT JOIN students s2 ON s1.class = s2.class AND s1.score
- 条件必须是
s1.score ,不能是,否则唯一记录会被自己干掉 - 如果存在完全重复的行(
class和score都一样),会返回全部——需提前去重或加DISTINCT - 务必给
class加索引,否则LEFT JOIN易退化成笛卡尔积
实际选哪种,取决于你手头的数据库版本、数据规模、是否允许并列、以及有没有主键可用。窗口函数写起来最直白,但老系统只能靠 JOIN 或相关子查询硬扛;而看似最简单的 WHERE 写法,恰恰最容易在分组场景下静默出错。










