引言:
今天同事翻看以前我寫的sql時,問我我這個sql和他寫的相比查詢效率哪一個更好。乍眼一看,居然沒看懂他寫的sql,(⊙﹏⊙)b汗。仔細一看,還真是很巧妙,必需要研究研究!
因此便有了本篇內容:mysql如何先查詢後分組(求每一個分組的 top1)
問題重現:有這樣一個需求,須要查詢每一個分組的某個字段最新(最大)對應的整條記錄。舉個栗子:假若有個員工表,有id(主鍵),salary(薪水),depart_id(部門id),求出每一個部門薪水最高的員工記錄。mysql
實現:
在這以前,我所知道比較簡單明瞭的實現有下面這兩種(爲了簡單,我建立了一個測試表,只包含排序字段和分組字段)sql
如下是建表語句數據庫
DROP TABLE IF EXISTS `sort_group`; CREATE TABLE `sort_group` ( `id` int(11) NOT NULL AUTO_INCREMENT, `sort` int(11) DEFAULT NULL, `gp` int(11) DEFAULT NULL, `name` varchar(50) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4; insert into `sort_group`(`id`,`sort`,`gp`,`name`) values (1,1,1,'我是sort1,gp1'),(2,1,2,'我是sort1,gp2'),(3,2,1,'我是sort2,gp1'),(4,2,2,'我是sort2,gp2');
表中的數據:oracle
第一種實現:(先按正確的排序查詢出的結果做爲子查詢,而後以子查詢的結果集再分組,就會只剩下每一個分組的第一條記錄,覺得子表是正確排序的,因此子表的每一個分組的第一條記錄就是想要的結果)dom
SELECT a.id,a.sort,a.gp,a.name FROM ( SELECT * FROM sort_group ORDER BY sort DESC ) a GROUP BY a.gp
這種實現很好理解,按照語義就是先查詢後排序。可是仔細一看,能夠看出一點問題。用了分組查詢,查的字段卻沒有都進行分組(這裏指的是sort字段),在一些數據庫好比oracle,這段sql就會報錯。mysql沒有報錯可是總有取巧的嫌疑。函數
測試結果:測試
第二種實現,利用group_concat()函數(使用GROUP_CONCAT)把gp相同的spa
分幾步理解:3d
(1)利用GROUP_CONCAT把按照GROUP BY gp分組後造成的每條記錄的sort字段以","組合起來,而且組合的sort是按照DESC排序的code
SELECT GROUP_CONCAT(sort ORDER BY sort DESC),gp FROM sort_group GROUP BY gp;
結果:(第一條記錄表示gp爲1的分組由兩個sort組成,sort分別爲1,2,這裏降序排列了,因此爲2,1;同理第二條也是同樣)
(2)接下來要作的就是把GROUP_CONCAT的組合字段再分解,分解後取第一個位置的值便可
SELECT SUBSTRING_INDEX(GROUP_CONCAT(sort ORDER BY sort DESC),',',1),gp FROM sort_group GROUP BY gp;
結果:
須要注意的是:若是須要取到每一個sort對應的其餘記錄,咱們來看下結果:
SELECT SUBSTRING_INDEX(GROUP_CONCAT(sort ORDER BY sort DESC),',',1),gp,id,NAME FROM sort_group GROUP BY gp;
結果:(咱們會發現其餘字段只是group by gp的第一條記錄的,並非sort爲2對應的那條記錄的值)
因此若是須要全部的字段能夠考慮先查出每一個分組下最大的記錄對應的id,利用子查詢將整條記錄查出,以下:
(1).使用以sort的DESC排序,查詢出ID值
SELECT GROUP_CONCAT(id ORDER BY sort DESC),gp FROM sort_group GROUP BY gp;
結果:
(2).利用SUBSTRING_INDEX把ID從CONCAT裏面截取出來,再根據ID查詢記錄
SELECT * FROM `sort_group` WHERE id IN (SELECT SUBSTRING_INDEX(GROUP_CONCAT(id ORDER BY sort DESC),',',1) FROM sort_group GROUP BY gp);
查詢結果:
第二種實現:(使用max函數和group by分組函數結合使用,這種方法的侷限性也是不能直接查詢出max值對應的該行的其餘記錄)
SELECT MAX(sort),gp FROM sort_group GROUP BY gp;
結果:
第三種實現:
SELECT a.*,b.sort,b.gp FROM sort_group a LEFT JOIN sort_group b ON a.gp = b.gp AND a.sort < b.sort WHERE b.sort IS NULL
這種實現利用了左鏈接,乍一看很神奇是否是? 原理將表根據分組字段進行自鏈接,而後根據a.sort < b.sort過濾鏈接,那麼鏈接好的記錄中,右表爲空時,左表中的a.sort確定是最大的,這樣最後便獲得了需求的記錄(若是每組gp沒有並列的最大記錄,那麼WHERE b.sort IS NULL 在每一個不一樣的gp下只有一條記錄,且是最大值;如假設每組gp裏面的每一個sort都是同樣大,那麼獲取到的全部記錄的b.sort都爲null,且每一個sort也是最大值)如圖:
表中記錄值:
SQL:
SELECT a.*,b.sort,b.gp FROM sort_group a LEFT JOIN sort_group b ON a.gp = b.gp AND a.sort < b.sort
查詢結果:
假設每一個相同的gp裏面,對應的sort都是同樣大的,此時更改表數據,以下:
SQL:
SELECT a.*,b.sort,b.gp FROM sort_group a LEFT JOIN sort_group b ON a.gp = b.gp AND a.sort < b.sort
顯示結果(每組b.sort都是null,即每一個記錄裏面的sort都是最大值):
下面測試一下在不創建索引的狀況下執行效率。
爲了方便模擬數據,本人寫了一個存儲函數模擬插入數據
DELIMITER $$ CREATE PROCEDURE `random_insert` (IN s int,IN g int,IN len int) CONTAINS SQL BEGIN DECLARE i INT; SET i = 0; START TRANSACTION; WHILE i <= len DO INSERT into sort_group(sort,gp) VALUES (FLOOR(RAND() * s),FLOOR(RAND() * g)); SET i = i + 1; END WHILE; COMMIT; END$$ DELIMITER ;
先測試每一個組中平均有10條數據的狀況,爲了保證sort不重複,s值儘可能大,執行如下語句:
call random_insert(1000000,10000,100000);
基於此運行3條sql,花費的時間分別是:
0.105s 0.095s 100+s(汗)
接下啦測試每組中平均有1000條的狀況
call random_insert(1000000,100,100000);
0.126s 0.091s 100+s
而後咱們給兩個字段加上索引重複上面兩次測試
0.106 0.135s 1000+s
0.101s 0.120s 100+s
從測試結果上看 第三種徹底不可用,不難分析緣由,第三種產生了笛卡爾積,而且一個組內數量越多,產生的越多。
這裏就不經過explain分析查詢策略了,結果已經很明顯了。
我的建議使用第二種來完成需求。固然這也不是絕對的,須要具體狀況具體分析