这篇文章主要讲带有分组+统计的SQL会有的小坑, 以及解决方案.
可以通过一个简单的业务SQL需求切入了解.
查找每个班级中成绩最好的学生
下面是聚合一些主要数据后的表(方便看主要的SQL, 少了JOIN操作):
create table achievements
(
id bigint auto_increment
primary key,
student_id bigint not null,
class_id bigint not null,
subject varchar(30) not null comment '科目',
max_value varchar(50) not null,
value int not null
)
comment '成绩表';
对于这个需求, 肯定是要先对数据进行分组 然后再求分组中的最大值.
那么我们可能会得出以下SQL:
SELECT * , MAX(value) AS max_value FROM achievements GROUP BY class_id
这个SQL乍一看是没啥问题的, 但执行一下SQL
SELECT *, MAX(value) AS max_value, GROUP_CONCAT(value) AS all_value, GROUP_CONCAT(value) AS students
FROM achievements
GROUP BY class_id
会发现
也就是得到分组数据中, 并不一定是该组value最大的数据(其实选择的是分组中的第一条数据).
注意: MAX()聚合函数得到的是具体的值, 并不会得到一行数据.
那么用子查询+排序的方式, 先使用子查询根据成绩进行排序, 然后使用GROUP BY:
SELECT a.*
FROM (SELECT class_id, student_id, value FROM achievements ORDER BY value DESC) AS a
GROUP BY a.class_id;
执行后发现同样不行, 通过EXPLAIN对语句进行分析后发现只有一条执行情况 说明该语句是被优化器优化了(子查询消除).
那可以换种思路, 先查出分组数据和分组中最大的值, 然后再进行匹配:
SELECT a.*
FROM achievements a
INNER JOIN
(SELECT MAX(value) AS max_value, class_id
FROM achievements
GROUP BY class_id) AS b
ON a.value = b.max_value AND a.class_id = b.class_id
执行后得到了正确的结果.
开窗函数
在MySQL的8.x版本提供了开窗函数可以很简单直观地查出:SELECT a.*
FROM (SELECT ai.class_id,
ai.student_id,
ai.value,
RANK() OVER (PARTITION BY ai.class_id ORDER BY ai.value DESC) AS rk
FROM achievements ai) as a
WHERE rk <= 2;
其他等效的SQL:
SELECT *
FROM achievements
WHERE (class_id, value) IN
(SELECT class_id, MAX(value)
FROM achievements
GROUP BY class_id);
SELECT a.*
FROM achievements a
LEFT OUTER JOIN achievements b
ON a.class_id = b.class_id AND a.value < b.value
WHERE b.class_id IS NULL;
参考文章
stackoverflow: sql-select-only-rows-with-max-value-on-a-column 知乎: 聊聊MySql8.0中的group by 和 max函数取最新(优)一条记录的问题