在mysql的查询场景中,分组排序是指先按照指定字段对数据进行分组,再在每个分组内部按照某个规则对数据排序,这种需求在数据统计分析、组内TopN查询等场景中非常常见。实现分组排序可以根据mysql的版本选择不同的方案,低版本可以使用子查询配合排序实现,高版本可以直接使用窗口函数完成。

方法一:使用窗口函数实现分组排序
mysql 8.0及以上版本支持窗口函数,其中ROW_NUMBER()、RANK()、DENSE_RANK()都可以配合PARTITION BY和ORDER BY实现分组排序,三者的主要区别是处理并列排名的逻辑不同。
常用窗口函数说明
ROW_NUMBER():每个分组内按顺序生成连续唯一的序号,即使排序字段值相同,序号也不会重复RANK():排序字段值相同的行序号相同,下一个序号会跳过重复的数量,比如两个第1,下一个就是第3DENSE_RANK():排序字段值相同的行序号相同,下一个序号连续,比如两个第1,下一个就是第2
示例代码
假设有一张学生成绩表student_score,结构如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 记录ID |
| class_id | int | 班级ID |
| student_name | varchar(50) | 学生姓名 |
| score | int | 考试成绩 |
需求是按班级分组,每个班级内按成绩从高到低排序,生成每个学生的班级内排名,使用ROW_NUMBER()实现的sql如下:
-- 按班级分组,组内按成绩降序排序,生成连续排名
SELECT
class_id,
student_name,
score,
ROW_NUMBER() OVER (PARTITION BY class_id ORDER BY score DESC) AS class_rank
FROM student_score;
如果需要并列排名不跳过序号,把ROW_NUMBER()替换成DENSE_RANK()即可:
-- 按班级分组,组内按成绩降序排序,生成连续不跳号的并列排名
SELECT
class_id,
student_name,
score,
DENSE_RANK() OVER (PARTITION BY class_id ORDER BY score DESC) AS class_rank
FROM student_score;
方法二:低版本mysql使用子查询实现分组排序
如果mysql版本低于8.0,不支持窗口函数,可以通过子查询的方式实现分组排序,核心思路是给每个分组内的数据手动计算排序序号。
同样以上面的学生成绩表为例,实现按班级分组、组内按成绩降序排序的需求,sql如下:
-- 低版本mysql实现分组排序,通过子查询计算组内排名
SELECT
t1.class_id,
t1.student_name,
t1.score,
(SELECT COUNT(*) + 1
FROM student_score t2
WHERE t2.class_id = t1.class_id
AND t2.score > t1.score) AS class_rank
FROM student_score t1
ORDER BY t1.class_id, t1.score DESC;
这段sql的逻辑是,对于t1表中的每一行数据,去t2表中查询同一个班级内成绩比当前行高的记录数量,数量加1就是当前行的组内排名。如果成绩相同,排名也会相同,不过这种方式性能比窗口函数差,数据量大的时候不建议使用。
分组排序取TopN的场景
很多时候分组排序的需求是取每个组内的前N条数据,比如取每个班级的前3名,使用窗口函数的实现方式如下:
-- 取每个班级成绩前3名的学生
WITH ranked_score AS (
SELECT
class_id,
student_name,
score,
ROW_NUMBER() OVER (PARTITION BY class_id ORDER BY score DESC) AS class_rank
FROM student_score
)
SELECT class_id, student_name, score, class_rank
FROM ranked_score
WHERE class_rank <= 3;
低版本mysql实现每个班级前3名的查询如下:
-- 低版本mysql取每个班级成绩前3名
SELECT t1.*
FROM student_score t1
WHERE (
SELECT COUNT(*)
FROM student_score t2
WHERE t2.class_id = t1.class_id
AND t2.score > t1.score
) < 3
ORDER BY t1.class_id, t1.score DESC;
注意事项
- 使用
GROUP BY分组的时候,如果直接配合ORDER BY,排序是对整个结果集排序,不是对分组内排序,不要混淆这个逻辑 - 窗口函数中的
PARTITION BY才是用来指定分组字段的,ORDER BY才是分组内的排序规则 - 如果分组内排序的字段有NULL值,mysql会默认把NULL值排在最前面,如果需要调整NULL值的排序位置,可以在
ORDER BY后面加IS NULL判断,比如ORDER BY score IS NULL, score DESC,这样NULL值会排在最后 - 数据量较大的时候优先使用窗口函数实现分组排序,性能远优于子查询的方式