Her gruplandırılmış sonuç grubu için en iyi n kaydı alın


140

Aşağıdakiler mümkün olan en basit örnektir, ancak herhangi bir çözüm ölçeklendirilebilmelidir, ancak birçok en iyi sonuca ihtiyaç vardır:

Kişi, grup ve yaş sütunlarıyla aşağıdaki gibi bir tablo verildiğinde , her gruptaki en yaşlı 2 kişiyi nasıl elde edersiniz? (Grup içindeki bağlar daha fazla sonuç vermemeli, ilk 2'yi alfabetik sıraya göre vermelidir)

+ -------- + ------- + ----- +
| Kişi | Grup | Yaş |
+ -------- + ------- + ----- +
| Bob | 1 | 32 |
| Jill | 1 | 34 |
| Shawn | 1 | 42 |
| Jake | 2 | 29 |
| Paul | 2 | 36 |
| Laura | 2 | 39 |
+ -------- + ------- + ----- +

İstenen sonuç kümesi:

+ -------- + ------- + ----- +
| Shawn | 1 | 42 |
| Jill | 1 | 34 |
| Laura | 2 | 39 |
| Paul | 2 | 36 |
+ -------- + ------- + ----- +

NOT: Bu soru, bir önceki grup - Her gruptan tek bir üst satır almak için ve @Bohemian'dan büyük bir MySQL'e özel yanıt alan her gruplanmış SQL sonuç grubu için maksimum değeri olan kayıtları alın :

select * 
from (select * from mytable order by `Group`, Age desc, Person) x
group by `Group`

Nasıl yapılacağını bilmesem de, bunu inşa edebilmek isterdim.



2
Bu örneği kontrol edin. İstediğinize oldukça yakın: stackoverflow.com/questions/1537606/…
Savas Vedova

Grup başına N sonuç almak için GROUP BY içinde LIMIT mi kullanıyorsunuz? stackoverflow.com/questions/2129693/…
Edye Chan

Yanıtlar:


88

İşte bunu yapmanın bir yolu UNION ALL(Bkz . Demo ile SQL Fiddle ). Bu, ikiden fazla grubunuz varsa, iki grupla çalışır, o zaman groupnumarayı belirtmeniz ve her biri için sorgu eklemeniz gerekir group:

(
  select *
  from mytable 
  where `group` = 1
  order by age desc
  LIMIT 2
)
UNION ALL
(
  select *
  from mytable 
  where `group` = 2
  order by age desc
  LIMIT 2
)

Bunu yapmanın çeşitli yolları vardır, durumunuza en uygun rotayı belirlemek için bu makaleye bakın:

http://www.xaprb.com/blog/2006/12/07/how-to-select-the-firstleastmax-row-per-group-in-sql/

Düzenle:

Bu sizin için de işe yarayabilir, her kayıt için bir satır numarası oluşturur. Yukarıdaki bağlantıdan bir örnek kullanıldığında, yalnızca satır sayısı 2'den küçük veya ona eşit olan kayıtlar döndürülür:

select person, `group`, age
from 
(
   select person, `group`, age,
      (@num:=if(@group = `group`, @num +1, if(@group := `group`, 1, 1))) row_number 
  from test t
  CROSS JOIN (select @num:=0, @group:=null) c
  order by `Group`, Age desc, person
) as x 
where x.row_number <= 2;

Demoya Bakın


52
1000'den fazla grubu varsa, bu biraz korkutucu olmaz mı?
Charles Forest

1
@CharlesForest evet, olur ve bu yüzden ikiden fazla grup için belirtmeniz gerektiğini söyledim. Çirkinleşecekti.
Taryn

1
@CharlesForest Ben daha iyi bir çözüm bulduğumu düşünüyorum, benim düzenleme bakın
Taryn

1
Bunu okuyan herkes için bir not: Sürüm, değişkenlerin doğru olmaya yakın olmasıdır. Bununla birlikte, MySQL ifadelerdeki ifadelerin değerlendirme sırasını garanti etmez SELECT(ve aslında bazen sıra dışı değerlendirir). Çözümün anahtarı, tüm değişken atamalarını tek bir ifadeye koymaktır; işte bir örnek: stackoverflow.com/questions/38535020/… .
Gordon Linoff

1
@GordonLinoff Cevabımı güncelledim, işaret ettiğiniz için teşekkürler. Güncellemesi çok uzun sürdü.
Taryn

63

Diğer veritabanlarında bunu kullanarak yapabilirsiniz ROW_NUMBER. MySQL desteklemiyor ROW_NUMBERancak taklit etmek için değişkenleri kullanabilirsiniz:

SELECT
    person,
    groupname,
    age
FROM
(
    SELECT
        person,
        groupname,
        age,
        @rn := IF(@prev = groupname, @rn + 1, 1) AS rn,
        @prev := groupname
    FROM mytable
    JOIN (SELECT @prev := NULL, @rn := 0) AS vars
    ORDER BY groupname, age DESC, person
) AS T1
WHERE rn <= 2

Çevrimiçi çalıştığını görün: sqlfiddle


Düzenleme Sadece bluefeet çok benzer bir cevap gönderdi: +1 ona. Ancak bu cevabın iki küçük avantajı vardır:

  1. Tek bir sorgu. Değişkenler SELECT deyimi içinde başlatılır.
  2. Soruda açıklandığı gibi bağları işler (isme göre alfabetik sıraya göre).

Bu yüzden birine yardım etmesi için burada bırakacağım.


1
Mark- Bu bizim için iyi çalışıyor. @ Bluefeet - çok takdir iltifat için iyi bir alternatif sağladığınız için teşekkür ederiz.
Yarin

+1. Bu benim için çalıştı. Gerçekten temiz ve noktaya cevap. Bunun tam olarak nasıl çalıştığını açıklayabilir misiniz? Bunun arkasındaki mantık nedir?
Aditya Hajare

3
Güzel bir çözüm ama benim ortamımda (MySQL 5.6) çalışmıyor gibi görünüyor çünkü
seçimden

Bunu çalıştırırken silebilirdim JOIN (SELECT @prev := NULL, @rn := 0) AS vars. Ben boş değişkenler beyan etmek olduğunu düşünüyorum, ama MySql için yabancı gibi görünüyor.
Joseph Cho

1
Bu MySQL 5.7'de benim için harika çalışıyor, ancak birisi nasıl çalıştığını açıklayabilirse harika olurdu
George B

41

Bunu dene:

SELECT a.person, a.group, a.age FROM person AS a WHERE 
(SELECT COUNT(*) FROM person AS b 
WHERE b.group = a.group AND b.age >= a.age) <= 2 
ORDER BY a.group ASC, a.age DESC

DEMO


6
snuffin en basit çözüm ile hiçbir yerden çıkıyor! Bu Ludo'nun / Bill Karwin'inkinden daha mı zarif ? Biraz yorum alabilir miyim
Yarin

Hm, daha zarif olup olmadığından emin değilim. Ama oylardan yola çıkarak, sanırım bluefeet daha iyi bir çözüme sahip olabilir.
enfiye

2
Bununla ilgili bir sorun var. Grup içinde ikinci sırada bir beraberlik varsa, sadece bir üst sonuç döndürülür. Değiştirilmiş demoyu
Yarin

2
İstenirse sorun değil. Sırasını ayarlayabilirsiniz a.person.
Alberto Leal

hayır, benim durumumda çalışmıyor, DEMO da çalışmıyor
Choix

31

Kendi kendine katılmaya ne dersiniz:

CREATE TABLE mytable (person, groupname, age);
INSERT INTO mytable VALUES('Bob',1,32);
INSERT INTO mytable VALUES('Jill',1,34);
INSERT INTO mytable VALUES('Shawn',1,42);
INSERT INTO mytable VALUES('Jake',2,29);
INSERT INTO mytable VALUES('Paul',2,36);
INSERT INTO mytable VALUES('Laura',2,39);

SELECT a.* FROM mytable AS a
  LEFT JOIN mytable AS a2 
    ON a.groupname = a2.groupname AND a.age <= a2.age
GROUP BY a.person
HAVING COUNT(*) <= 2
ORDER BY a.groupname, a.age DESC;

bana verir:

a.person    a.groupname  a.age     
----------  -----------  ----------
Shawn       1            42        
Jill        1            34        
Laura       2            39        
Paul        2            36      

Her kategori için en iyi 10 rekoru seçmek için Bill Karwin'in cevabından çok etkilendim.

Ayrıca, SQLite kullanıyorum, ancak bu MySQL üzerinde çalışmalıdır.

Başka bir şey: yukarıda, kolaylık sağlamak için groupsütunu bir groupnamesütunla değiştirdim .

Düzenle :

OP'nin eksik kravat sonuçlarıyla ilgili yorumunu takiben, tüm bağları göstermek için snuffin'in cevabını artırdım. Bu, sonuncular bağlarsa, aşağıda gösterildiği gibi 2'den fazla satır döndürülebileceği anlamına gelir:

.headers on
.mode column

CREATE TABLE foo (person, groupname, age);
INSERT INTO foo VALUES('Paul',2,36);
INSERT INTO foo VALUES('Laura',2,39);
INSERT INTO foo VALUES('Joe',2,36);
INSERT INTO foo VALUES('Bob',1,32);
INSERT INTO foo VALUES('Jill',1,34);
INSERT INTO foo VALUES('Shawn',1,42);
INSERT INTO foo VALUES('Jake',2,29);
INSERT INTO foo VALUES('James',2,15);
INSERT INTO foo VALUES('Fred',1,12);
INSERT INTO foo VALUES('Chuck',3,112);


SELECT a.person, a.groupname, a.age 
FROM foo AS a 
WHERE a.age >= (SELECT MIN(b.age)
                FROM foo AS b 
                WHERE (SELECT COUNT(*)
                       FROM foo AS c
                       WHERE c.groupname = b.groupname AND c.age >= b.age) <= 2
                GROUP BY b.groupname)
ORDER BY a.groupname ASC, a.age DESC;

bana verir:

person      groupname   age       
----------  ----------  ----------
Shawn       1           42        
Jill        1           34        
Laura       2           39        
Paul        2           36        
Joe         2           36        
Chuck       3           112      

@ Ludo- Bill Karwin'in yanıtını gördüm - buraya uyguladığınız için teşekkürler
Yarin

Snuffin'in cevabı hakkında ne düşünüyorsun?
İkisini

2
Bununla ilgili bir sorun var. Grup içindeki ikincilik için beraberlik varsa, sadece bir üst sonuç returned- bakınız olan demo
Yarin

1
@ Ludo- orijinal gereksinim, her grubun kesin n sonuç döndürmesi ve herhangi bir bağın alfabetik olarak çözülmesiydi
Yarin

Bağları içeren düzenleme benim için işe yaramıyor. ERROR 1242 (21000): Subquery returns more than 1 rowMuhtemelen yüzünden alıyorum GROUP BY. SELECT MINYalnızca alt sorguyu yürüttüğümde, üç satır oluşturur: 34, 39, 112ve orada ikinci değer 39 değil, 36 olmalıdır.
verbamour

12

Snuffin çözümü, çok sayıda satıra sahip olduğunuzda yürütmek için oldukça yavaş görünüyor ve Mark Byers / Rick James ve Bluefeet çözümleri çevremde çalışmıyor (MySQL 5.6) çünkü siparişin seçilmesinden sonra uygulandığı için bir varyant var Bu sorunu çözmek için Marc Byers / Rick James çözümlerinin açıklaması (ekstra karmaşık bir seçimle):

select person, groupname, age
from
(
    select person, groupname, age,
    (@rn:=if(@prev = groupname, @rn +1, 1)) as rownumb,
    @prev:= groupname 
    from 
    (
        select person, groupname, age
        from persons 
        order by groupname ,  age desc, person
    )   as sortedlist
    JOIN (select @prev:=NULL, @rn :=0) as vars
) as groupedlist 
where rownumb<=2
order by groupname ,  age desc, person;

5 milyon satır içeren bir tabloda benzer sorgu denedim ve 3 saniyeden az sonuç döndürür


3
Bu benim ortamımda çalışan tek sorgu. Teşekkürler!
herrherr

3
LIMIT 9999999İle türetilmiş herhangi bir tabloya ekleyin ORDER BY. Bu ,ORDER BY yok sayılmasını engelleyebilir .
Rick James

Birkaç bin satır içeren bir tabloda benzer bir sorgu çalıştırdım ve bir sonuç döndürmek 60 saniye sürdü, bu yüzden ... yazı için teşekkürler, bu benim için bir başlangıç. (ETA: 5 saniyeye kadar. Güzel!)
Evan

10

Şuna bir bak:

SELECT
  p.Person,
  p.`Group`,
  p.Age
FROM
  people p
  INNER JOIN
  (
    SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`
    UNION
    SELECT MAX(p3.Age) AS Age, p3.`Group` FROM people p3 INNER JOIN (SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`) p4 ON p3.Age < p4.Age AND p3.`Group` = p4.`Group` GROUP BY `Group`
  ) p2 ON p.Age = p2.Age AND p.`Group` = p2.`Group`
ORDER BY
  `Group`,
  Age DESC,
  Person;

SQL Fiddle: http://sqlfiddle.com/#!2/cdbb6/15


5
Adamım, diğerleri çok daha basit çözümler buldular ... Bu konuda sadece 15 dakika harcadım ve böyle karmaşık bir çözüm bulduğum için kendimle inanılmaz gurur duydum. Bu berbat.
Travesti3

Akımdan 1 daha az olan bir dahili sürüm numarası bulmak zorunda kaldım - bu bana bunu yapmak için cevap verdi: max(internal_version - 1)- çok stres daha az :)
Jamie Strauss

8

Diğer yanıtlar yeterince hızlı değilse Bu kodu deneyin:

SELECT
        province, n, city, population
    FROM
      ( SELECT  @prev := '', @n := 0 ) init
    JOIN
      ( SELECT  @n := if(province != @prev, 1, @n + 1) AS n,
                @prev := province,
                province, city, population
            FROM  Canada
            ORDER BY
                province   ASC,
                population DESC
      ) x
    WHERE  n <= 3
    ORDER BY  province, n;

Çıktı:

+---------------------------+------+------------------+------------+
| province                  | n    | city             | population |
+---------------------------+------+------------------+------------+
| Alberta                   |    1 | Calgary          |     968475 |
| Alberta                   |    2 | Edmonton         |     822319 |
| Alberta                   |    3 | Red Deer         |      73595 |
| British Columbia          |    1 | Vancouver        |    1837970 |
| British Columbia          |    2 | Victoria         |     289625 |
| British Columbia          |    3 | Abbotsford       |     151685 |
| Manitoba                  |    1 | ...

Sitenize baktım - şehirlerin nüfusu için veri kaynağını nereden alabilirim? TIA ve rgs.
Vérace

maxmind.com/en/worldcities - Lat / lng aramaları , sorgular, bölümleme vb. ile denemeler yapmak için kullanışlı buluyorum . İlginç olacak kadar büyük, ancak cevapları tanıyacak kadar okunabilir. Kanada altkümesi bu tür bir soru için kullanışlıdır. (ABD şehirlerinden daha az eyalet.)
Rick James

2

Bunu paylaşmak istedim çünkü üzerinde çalıştığım bir java programında bunu uygulamak için kolay bir yol aramak için uzun zaman harcadım. Bu aradığınız çıktıyı tam olarak vermez, yakındır. Mysql adlı işlev GROUP_CONCAT()her grupta kaç sonuç döndürüleceğini belirlemek için gerçekten iyi çalıştı. LIMITBunu yapmaya çalışmanın diğer süslü yollarını kullanmak veya herhangi bir şey COUNTbenim için işe yaramadı. Değiştirilmiş bir çıktıyı kabul etmek istiyorsanız, bu harika bir çözümdür. Diyelim ki öğrenci kimlikleri, cinsiyetleri ve gpa'ları olan 'öğrenci' adlı bir masam var. Diyelim ki her bir cinsiyet için 5 gpas almak istiyorum. Sonra böyle bir sorgu yazabilirim

SELECT sex, SUBSTRING_INDEX(GROUP_CONCAT(cast(gpa AS char ) ORDER BY gpa desc), ',',5) 
AS subcategories FROM student GROUP BY sex;

'5' parametresinin her satıra kaç girişin birleştirileceğini söylediğini unutmayın

Ve çıktı şöyle görünecekti:

+--------+----------------+
| Male   | 4,4,4,4,3.9    |
| Female | 4,4,3.9,3.9,3.8|
+--------+----------------+

Ayrıca ORDER BYdeğişkeni değiştirebilir ve farklı bir şekilde sipariş edebilirsiniz. Eğer öğrencinin yaşına sahip olsaydım 'gpa desc' yerine 'age desc' yazabilirim ve işe yarayacaktır! Çıktıda daha fazla sütun almak için gruba ifadeye değişkenler de ekleyebilirsiniz. Yani bu sadece oldukça esnek ve sadece sonuçları listelemeye uygunsanız iyi çalışan bulduğum bir yoldur.


0

SQL Server'da row_numer()aşağıdaki gibi kolayca sonuç alabilen güçlü bir işlevdir

select Person,[group],age
from
(
select * ,row_number() over(partition by [group] order by age desc) rn
from mytable
) t
where rn <= 2

8.0 ve 10.2'nin GA olmasıyla bu cevap makul hale geliyor.
Rick James

@RickJames 'GA olmak' ne demektir? Pencere fonksiyonları ( dev.mysql.com/doc/refman/8.0/en/window-functions.html ) sorunumu çok iyi çözdü.
iedmrc

1
@iedmrc - "GA", "Genel Olarak Kullanılabilir" anlamına gelir. "Prime time için hazır" veya "serbest bırakma" için teknik olarak konuşulur. Sürümü geliştirerek geçiyorlar ve kaçırdıkları hataya odaklanacaklar. Bu bağlantı MySQL 8.0'ın MariaDB 10.2'nin uygulamasından farklı olabilecek uygulamasını tartışıyor.
Rick James

-1

MySQL'de bu soruna gerçekten güzel bir cevap var - Her Grup İçin En İyi N Satırı Nasıl Alınır?

Referans verilen bağlantıdaki çözüme dayanarak, sorgunuz şöyle olacaktır:

SELECT Person, Group, Age
   FROM
     (SELECT Person, Group, Age, 
                  @group_rank := IF(@group = Group, @group_rank + 1, 1) AS group_rank,
                  @current_group := Group 
       FROM `your_table`
       ORDER BY Group, Age DESC
     ) ranked
   WHERE group_rank <= `n`
   ORDER BY Group, Age DESC;

nerede nolduğunutop n ve your_tablesizin tablonun adıdır.

Referanstaki açıklamanın çok açık olduğunu düşünüyorum. Hızlı başvuru için buraya kopyalayıp yapıştıracağım:

Şu anda MySQL, bir grup içinde sıra numarası atayabilen ROW_NUMBER () işlevini desteklememektedir, ancak geçici bir çözüm olarak MySQL oturum değişkenlerini kullanabiliriz.

Bu değişkenler bildirim gerektirmez ve bir sorguda hesaplamalar yapmak ve ara sonuçları saklamak için kullanılabilir.

@current_country: = country Bu kod her satır için yürütülür ve ülke sütununun değerini @current_country değişkenine depolar.

@country_rank: = IF (@current_country = ülke, @country_rank + 1, 1) Bu kodda, @current_country aynı ise sıralamayı yükseltiriz, aksi takdirde 1 olarak ayarlayın. İlk satır için @current_country NULL, yani sıra ayrıca 1 olarak ayarlanmıştır.

Doğru sıralama için ORDER BY ülke, nüfus DESC


Marc Byers, Rick James ve benimki tarafından kullanılan ilkedir.
Laurent PELE

Hangi gönderinin (Stack Overflow veya SQLlines) ilk olduğunu söylemek zor
Laurent PELE

@LaurentPELE - Mine Şubat 2015 tarihinde gönderildi. SQLlines üzerinde hiçbir zaman damgası veya ad görmüyorum. MySQL blogları, bazıları güncelliğini yitirecek kadar uzun süredir var ve kaldırılması gerekiyor - insanlar yanlış bilgi veriyor.
Rick James
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.