之前在学习SQL时刷过一遍LeetCode上的SQL题,不过只做一遍效果并不是很好,很快也忘记了具体的解题思路。在这里将对其中的:Q176(第二高薪水) 、 Q177(第N高薪水) 、 Q178(分数排名) 、 Q184(部门工资最高的员工) 、 Q185(部门工资前三高的员工) 进行归纳总结,从而更进一步的去理解有关排名和分组筛选相关的问题。
LeetCode上的SQL答案可详见Github-LeetCode,欢迎Start,Issue
Leetcode上这五道题放在一起看,其考察的知识点可以拓展为下面三个方向:
乍一看排名问题貌似和筛选关系不大,实则不然。基于排名的思想可以很容易实现比较复杂的筛选问题。当然相比于分组筛选,不分组筛选的实现难度还是比较低的。接下来将会逐一分析这三类问题。首先来说说排名问题。
注:本文所有的样例SQL对应的数据表如下所示
CREATE TABLE `empl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`salary` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `employee` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`salary` int(11) NOT NULL,
`deparment` varchar(64) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
MySQL没有提供可供直接使用的排名函数,所以我们必须自己来实现。常见的排名场景可以分为如下三类:
因为同分不同名在实际使用的时候并不多见,而且实现起来也比较容易,所以在合理就不做讨论;主要来看看其他两种应用场景。
顾名思义排名的方式是不间断的,是连续的。Eg:对数据500, 400, 400, 300, 200, 100进行飞跳跃式排名,则结果为:1,2,2,3,4,5。以上述的empl表为例,实现SQL如下:
SELECT
salary,
rank
FROM
(
SELECT
salary,
@rank :=
IF (salary = @sal, @rank, @rank + 1) AS rank,
@sal := salary
FROM
empl a,
(SELECT @rank := 0, @sal := NULL) b
ORDER BY
salary DESC
) c;
排名的结果是不连续的,Eg:对数据500, 400, 400, 300, 200, 100进行飞跳跃式排名,则结果为:1,2,2,4,5,6。以上述的empl表为例,实现SQL如下:
SELECT
salary,
rank
FROM
(
SELECT
salary,
@rank :=
IF (salary = @sal, @rank, @row_num) AS rank,
@sal := salary,
@row_num :=@row_num + 1
FROM
empl a,
(
SELECT
@rank := 0,
@sal := NULL,
@row_num := 1
) b
ORDER BY
salary DESC
) c;
相比于非跳跃式的实现,跳跃式排名需要额外增加一个变量row_num来记录当前数据对应的行数(从1开始自增长),排名的依据也必须基于该row_num,然后非是rank+1。
不分组的筛选将从通用场景(获取第N个值,获取前N个值)和特殊场景(获取第二个值,获取第一和第二值)两个维度来进行讨论。其中实现通用场景的方法通常有三个:
筛选empl表中薪水最大的前三个值,实现思路:假设数据集中没有重复数据,则不难想到,最大的值不存在比其自身更大的数据项,第二大值只存在一个比自己大的数据项,第三大值有且只有两个比自己大的值;以此类推将自身和比自己大的数据表做join,则可以通过分组计数的方式获取前N个值。SQL如下
SELECT
a.salary AS salary
FROM
(
SELECT DISTINCT
salary
FROM
empl
) a
LEFT JOIN (
SELECT DISTINCT
salary
FROM
empl
) b ON (a.salary < b.salary)
GROUP BY
a.salary
HAVING
count(*) < 3
ORDER BY
salary DESC;
将上述left join改写成子查询,SQL如下:
SELECT
salary
FROM
(
SELECT DISTINCT
salary
FROM
empl
) a
WHERE
3 > (
SELECT
count(*)
FROM
(
SELECT DISTINCT
salary
FROM
empl
) b
WHERE
a.salary < b.salary
)
ORDER BY
salary DESC;
这是一种不同是思考方式,首先对原始数据进行倒排去重,然后根据limit + offset的组合实现筛选前N项。SQL如下:
SELECT DISTINCT
salary
FROM
empl
ORDER BY
salary DESC
LIMIT 3 OFFSET 0;
不分组获取第N个值的思路和获取前N个值完全一样,只是在筛选数据的时候改变一下数据的筛选条件即可。
如果使用的是left join或子查询的方式,则可以改写Having语句为HAVING count(*) = N - 1
即可。不过这里的N必须大于3,因为无法通过HAVING count(*) = 1
来筛选第二大值,无法通过HAVING count(*) = 0
来筛选最大值;因为left join()后的最大值的数据集为(id, salary,null, null),第二大值的数据集为(id, salary, id, max(salary)),所以对于这两种特殊场景建议使用特殊的筛选方式(详见下文)。
如果使用的是排序分页法,则可以通过LIMIT 1 OFFSET N - 1
来实现获取第N个值。
可以通过双Max()来实现
SELECT
max(salary) AS sal
FROM
empl
WHERE
salary <> (SELECT max(salary) FROM empl);
当然所有的场景均可以通过排名法来实现,具体的实现SQL在这里就不做列举,可参考前文中有关排名的SQL实现语句。
相比于不分组的筛选,实现分组筛选的核心思路基本不变,只是在原有的基础上增加了分组的操作。这里的分组不能通过MySQL提供的Group By来直接实现,因为Group By往往是要和聚合函数或者Having一起使用的,MySQL只有求最大、最小这类基本的函数,并没有Top-N的函数,所以还是需要基于条件的left join方法或者分组排名的方式来实现。
实现筛选表employee中各部门薪水最高的前三个;只需要在上文的基础上,增加分组操作即可。同样需要保证数据集无不重复元素,SQL如下:
SELECT
a.salary,
a.deparment
FROM
(
SELECT DISTINCT
salary,
deparment
FROM
employee
) a
LEFT JOIN (
SELECT DISTINCT
salary,
deparment
FROM
employee
) b ON (
a.deparment = b.deparment
AND a.salary < b.salary
)
GROUP BY
a.deparment,
a.salary
HAVING
count(*) < 3
ORDER BY
a.deparment,
a.salary DESC;
将上述left join进行改写,SQL如下:
SELECT
*
FROM
(
SELECT DISTINCT
deparment,
salary
FROM
employee
) a
WHERE
3 > (
SELECT
count(*)
FROM
(
SELECT DISTINCT
deparment,
salary
FROM
employee
) b
WHERE
a.deparment = b.deparment
AND a.salary < b.salary
)
ORDER BY
a.deparment,
a.salary DESC;
筛选各部门薪水最高的前三个,SQL如下:
SELECT DISTINCT
salary,
deparment
FROM
(
SELECT
deparment,
salary,
@rank :=
IF (
@dep = deparment,
IF (
@sal = salary,
@rank :=@rank,
@rank :=@rank + 1
),
1
) AS rank,
@sal := salary,
@dep := deparment
FROM
employee a,
(
SELECT
@rank := 0,
@sal := NULL,
@dep := NULL
) b
ORDER BY
deparment,
salary DESC
) c
WHERE
rank <= 3;
基于本地变量实现排名的前提是要保证数据的有序性(降序)。而分组做Top-N筛选则首先需要保证各分组的有序性(降序),然后每对一个分组进行排名;在每完成一个分组的排名并开始一下分组排名时,需要将rank置为1,只有这样才能保证下一个分组排名结果的正确性;所以这里采用了嵌套IF的方式,其中外层用于判断是否更换分组,而内层用于进行非跳跃式同分同名排名。也正因为同分同名的缘故,所以最后筛选时需要对薪水进行Distinct,保证结果的唯一性。
上面介绍了分组筛选的三种通用解决办法,这里在介绍一种求最大值这种特殊场景的特殊实现方法:首先分组计算最大值,然后连表做join;SQL如下:
SELECT DISTINCT
employee.salary,
employee.deparment
FROM
employee
INNER JOIN (
SELECT
max(salary) AS salary,
deparment
FROM
employee
GROUP BY
deparment
) AS tmp ON employee.salary = tmp.salary
AND employee.deparment = tmp.deparment;