如何在mysql中查詢每個分組的前幾名


 

問題

在工作中常會遇到將數據分組排序的問題,如在考試成績中,找出每個班級的前五名等。 在orcale等數據庫中可以使用partition 語句來解決,但在mysql中就比較麻煩了。這次翻譯的文章就是專門解決這個問題的

原文地址: How to select the first/least/max row per group in SQL

翻譯

在使用SQL的過程中,我們經常遇到這樣一類問題:如何找出每個程序最近的日志條目?如何找出每個用戶的最高分?在每個分類中最受歡迎的商品是什么?通常這類“找出每個分組中最高分的條目”的問題可以使用相同的技術來解決。在這篇文章里我將介紹如何解決這類問題,而且會介紹如何找出最高的前幾名而不僅僅是第一名。

這篇文章會用到行數(row number),我在原來的文章 MySQL-specific 和 generic techniques 中已經提到過如何為每個分組設置行數了。在這里我會使用與原來的文章中相同的表格,但會加入新的price 字段

 1 +--------+------------+-------+
 2 | type   | variety    | price |
 3 +--------+------------+-------+
 4 | apple  | gala       |  2.79 | 
 5 | apple  | fuji       |  0.24 | 
 6 | apple  | limbertwig |  2.87 | 
 7 | orange | valencia   |  3.59 | 
 8 | orange | navel      |  9.36 | 
 9 | pear   | bradford   |  6.05 | 
10 | pear   | bartlett   |  2.14 | 
11 | cherry | bing       |  2.55 | 
12 | cherry | chelan     |  6.33 | 
13 +--------+------------+-------+

選擇每個分組中的最高分

這里我們要說的是如何找出每個程序最新的日志記錄或審核表中最近的更新或其他類似的排序問題。這類問題在IRC頻道和郵件列表中出現的越來越頻繁。我使用水果問題來作為示例,在示例中我們要選出每類水果中最便宜的一個,我們期望的結果如下

+--------+----------+-------+
| type   | variety  | price |
+--------+----------+-------+
| apple  | fuji     |  0.24 | 
| orange | valencia |  3.59 | 
| pear   | bartlett |  2.14 | 
| cherry | bing     |  2.55 | 
+--------+----------+-------+

這個問題有幾種解法,但基本上就是這兩步:找出最低的價格,然后找出和這個價格同一行的其他數據

其中一個常用的方法是使用自連接(self-join),第一步根據type(apple, cherry etc)進行分組,並找出每組中price的最小值

select type, min(price) as minprice

from fruits group by type; +--------+----------+ | type | minprice | +--------+----------+ | apple | 0.24 | | cherry | 2.55 | | orange | 3.59 | | pear | 2.14 | +--------+----------+

第二步是將剛剛結果與原來的表進行連接。既然剛剛給結果已經被分組了,我們將剛剛的查詢語句作為子查詢以便於連接沒有被分組的原始表格。

select f.type, f.variety, f.price
from (
   select type, min(price) as minprice
   from fruits group by type
) as x inner join fruits as f on f.type = x.type and f.price = x.minprice;

+--------+----------+-------+
| type   | variety  | price |
+--------+----------+-------+
| apple  | fuji     |  0.24 | 
| cherry | bing     |  2.55 | 
| orange | valencia |  3.59 | 
| pear   | bartlett |  2.14 | 
+--------+----------+-------+

還可以使用相關子查詢(correlated subquery)的方式來解決。這種方法在不同的mysql優化系統下,可能性能會有一點點下降,但這種方法會更直觀一些。

select type, variety, price
from fruits
where price = (select min(price) from fruits as f where f.type = fruits.type);
+--------+----------+-------+
| type   | variety  | price |
+--------+----------+-------+
| apple  | fuji     |  0.24 | 
| orange | valencia |  3.59 | 
| pear   | bartlett |  2.14 | 
| cherry | bing     |  2.55 | 
+--------+----------+-------+

這兩種查詢在邏輯上是一樣的,他們性能也基本相同

找出每組中前N個值

這個問題會稍微復雜一些。我們可以使用聚集函數(MIN(), MAX()等等)來找一行,但是找前幾行不能直接使用這些函數,因為它們都只返回一個值。但這個問題還是可以解決的。

這次我們找出每個類型(type)中最便宜的前兩種水果,首先我們嘗試

select type, variety, price
from fruits
where price = (select min(price) from fruits as f where f.type = fruits.type)
or price = (select min(price) from fruits as f where f.type = fruits.type
and price > (select min(price) from fruits as f2 where f2.type = fruits.type));
+--------+----------+-------+
| type   | variety  | price |
+--------+----------+-------+
| apple  | gala     |  2.79 | 
| apple  | fuji     |  0.24 | 
| orange | valencia |  3.59 | 
| orange | navel    |  9.36 | 
| pear   | bradford |  6.05 | 
| pear   | bartlett |  2.14 | 
| cherry | bing     |  2.55 | 
| cherry | chelan   |  6.33 | 
+--------+----------+-------+

是的,我們可以寫成自連接(self-join)的形式,但是仍不夠好(我將這個練習留給讀者)。這種方式在N變大(前三名,前4名)的時候性能會越來越差。我們可以使用其他的表現形式編寫這個查詢,但是它們都不夠好,它們都相當的笨重和效率低下。(譯者注:這種方式獲取的結果時,如果第N個排名是重復的時候最后選擇的結果會超過N,比如上面例子還有一個apple價格也是0.24,那最后的結果就會有3個apple)

我們有一種稍好的方式,在每個種類中選擇不超過該種類第二便宜的水果

select type, variety, price
from fruits
where (
   select count(*) from fruits as f
   where f.type = fruits.type and f.price <= fruits.price
) <= 2;

這次的代碼要優雅很多,而且在N增加時不需要重新代碼(非常棒!)。但是這個查詢在功能上和原來的是一樣。他們的時間復雜度均為分組中條目數的二次方。而且,很多優化器都不能優化這種查詢,使得它的耗時最好為全表行數的二次方(尤其在沒有設置正確的索引時),而且數據量大時,可能將服務器會停止響應。那么還有更好的方法嗎?有沒有辦法可以僅僅掃描一次數據,而不是通過子查詢進行多次掃描。(譯者注:這種方法有一個問題,就是如果排名並列第一的數字超過N后,這個分組會選不出數據,比如price為2.79的apple有3個,那么結果中就沒有apple了)

使用 UNION

如果已經為type, price設置了索引,而且在每個分組中去除的數據要多於包含的數據,一種非常高效的單次掃描的方法是將查詢拆分成多個獨立的查詢(尤其對mysql,對其他的RDBMSs也有效),再使用UNION將結果拼到一起。mysql的寫法如下:

(select * from fruits where type = 'apple' order by price limit 2)
union all
(select * from fruits where type = 'orange' order by price limit 2)
union all
(select * from fruits where type = 'pear' order by price limit 2)
union all
(select * from fruits where type = 'cherry' order by price limit 2)

注意:這里要使用UNION ALL,而不是UNION。后者會在合並的時候會將重復的條目清除掉。在我們的這個示例中沒有去除重復的需求,所以我們告訴服務器不要清除重復,清除重復在這個問題中是無用的,而且會造成性能的大幅下降。

原文出處: http://my.oschina.net/u/1032146/blog/149300

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM