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沒有提供可供直接使用的排名函數,所以我們必須自己來實現。常見的排名場景可以分爲如下三類:

  • 非跳躍式同分同名排名
  • 跳躍式同分同名排名
  • 同分不同名排名

因爲同分不同名在實際使用的時候並不多見,而且實現起來也比較容易,所以在合理就不做討論;主要來看看其他兩種應用場景。

非跳躍式同分同名排名

顧名思義排名的方式是不間斷的,是連續的。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個值)和特殊場景(獲取第二個值,獲取第一和第二值)兩個維度來進行討論。其中實現通用場景的方法通常有三個:

  • 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;

在這裏插入圖片描述

子查詢法

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

在這裏插入圖片描述

子查詢法

將上述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;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章