MySQL实现排名、分组筛选、TopN问题

之前在学习SQL时刷过一遍LeetCode上的SQL题,不过只做一遍效果并不是很好,很快也忘记了具体的解题思路。在这里将对其中的:Q176(第二高薪水) 、 Q177(第N高薪水) 、 Q178(分数排名) 、 Q184(部门工资最高的员工) 、 Q185(部门工资前三高的员工) 进行归纳总结,从而更进一步的去理解有关排名和分组筛选相关的问题。
LeetCode上的SQL答案可详见Github-LeetCode,欢迎Start,Issue
Leetcode上这五道题放在一起看,其考察的知识点可以拓展为下面三个方向:

  • 不分组筛选问题(最二值、第N个值、前N个值)
  • 分组筛选问题(最大值、第N个值、前N个值)
  • 排名问题

乍一看排名问题貌似和筛选关系不大,实则不然。基于排名的思想可以很容易实现比较复杂的筛选问题。当然相比于分组筛选,不分组筛选的实现难度还是比较低的。接下来将会逐一分析这三类问题。首先来说说排名问题。
注:本文所有的样例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实现排名、分组筛选、TopN问题_第1张图片

排名

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;

MySQL实现排名、分组筛选、TopN问题_第2张图片

跳跃式同分同名排名

排名的结果是不连续的,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;

MySQL实现排名、分组筛选、TopN问题_第3张图片
相比于非跳跃式的实现,跳跃式排名需要额外增加一个变量row_num来记录当前数据对应的行数(从1开始自增长),排名的依据也必须基于该row_num,然后非是rank+1。

不分组筛选

不分组的筛选将从通用场景(获取第N个值,获取前N个值)和特殊场景(获取第二个值,获取第一和第二值)两个维度来进行讨论。其中实现通用场景的方法通常有三个:

  • 1,自身左连接法
  • 2,子查询法
  • 3,排序分页法
    从实现思路的角度考虑,方法1和方法2其实是一回事。只是基于一种思想的两种不同实现方式而已。因为SQL的left join都是可以正常改写成子查询的方式,只是在查询性能上要远好于子查询。

不分组筛选 - 获取前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;

MySQL实现排名、分组筛选、TopN问题_第4张图片

子查询法

将上述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个值的思路和获取前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;

MySQL实现排名、分组筛选、TopN问题_第5张图片

子查询法

将上述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;

你可能感兴趣的:(MySQL)