Dapatkan catatan atas untuk setiap kelompok hasil yang dikelompokkan

140

Berikut ini adalah contoh paling sederhana yang mungkin, meskipun solusi apa pun harus dapat menskalakan namun banyak hasil teratas diperlukan:

Diberikan tabel seperti itu di bawah ini, dengan orang, grup, dan kolom umur, bagaimana Anda mendapatkan 2 orang tertua di setiap grup? (Ikatan dalam kelompok tidak boleh menghasilkan lebih banyak hasil, tetapi berikan 2 pertama dalam urutan abjad)

+ -------- + ------- + ----- +
| Orang | Grup | Umur |
+ -------- + ------- + ----- +
| Bob | 1 | 32 |
| Jill | 1 | 34 |
| Shawn | 1 | 42 |
| Jake | 2 | 29 |
| Paul | 2 | 36 |
| Laura | 2 | 39 |
+ -------- + ------- + ----- +

Kumpulan hasil yang diinginkan:

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

CATATAN: Pertanyaan ini dibangun di atas yang sebelumnya - Dapatkan catatan dengan nilai maksimal untuk setiap grup hasil SQL yang dikelompokkan - untuk mendapatkan satu baris teratas dari setiap grup, dan yang menerima jawaban khusus khusus MySQL dari @Bohemian:

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

Senang bisa membangun dari ini, meskipun saya tidak melihat caranya.

Yarin
sumber
2
Lihat contoh ini. Itu cukup dekat dengan apa yang Anda tanyakan: stackoverflow.com/questions/1537606/…
Savas Vedova
Menggunakan LIMIT dalam GROUP BY untuk mendapatkan hasil N per grup? stackoverflow.com/questions/2129693/…
Edye Chan

Jawaban:

88

Berikut adalah salah satu cara untuk melakukan ini, menggunakan UNION ALL(Lihat SQL Fiddle with Demo ). Ini berfungsi dengan dua grup, jika Anda memiliki lebih dari dua grup, maka Anda perlu menentukan groupangka dan menambahkan kueri untuk masing-masing 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
)

Ada berbagai cara untuk melakukan ini, lihat artikel ini untuk menentukan rute terbaik untuk situasi Anda:

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

Edit:

Ini mungkin bekerja untuk Anda juga, itu menghasilkan nomor baris untuk setiap catatan. Menggunakan contoh dari tautan di atas, ini hanya akan mengembalikan catatan-catatan dengan jumlah baris kurang dari atau sama dengan 2:

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;

Lihat Demo

Taryn
sumber
52
jika dia memiliki 1.000+ grup, bukankah itu akan membuat ini sedikit menakutkan?
Charles Forest
1
@CharlesForest ya, itu akan dan itu sebabnya saya menyatakan bahwa Anda harus menentukannya untuk lebih dari dua grup. Itu akan menjadi jelek.
Taryn
1
@CharlesForest Saya pikir saya menemukan solusi yang lebih baik, lihat hasil edit saya
Taryn
1
Catatan untuk siapa pun yang membaca ini: Versi ini adalah variabel yang hampir benar. Namun, MySQL tidak menjamin urutan evaluasi ekspresi di SELECT(dan, pada kenyataannya, kadang-kadang mengevaluasi mereka keluar dari urutan). Kunci dari solusi ini adalah untuk menempatkan semua tugas variabel dalam satu ekspresi; berikut adalah contohnya: stackoverflow.com/questions/38535020/… .
Gordon Linoff
1
@GordonLinoff Memperbarui jawaban saya, terima kasih telah menunjukkannya. Butuh waktu terlalu lama bagi saya untuk memperbaruinya.
Taryn
63

Di basis data lain Anda bisa melakukan ini menggunakan ROW_NUMBER. MySQL tidak mendukung ROW_NUMBERtetapi Anda dapat menggunakan variabel untuk menirunya:

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

Lihat berfungsi online: sqlfiddle


Sunting Saya baru memperhatikan bahwa bluefeet memposting jawaban yang sangat mirip: +1 kepadanya. Namun jawaban ini memiliki dua keunggulan kecil:

  1. Itu adalah satu permintaan. Variabel diinisialisasi dalam pernyataan SELECT.
  2. Ini menangani ikatan seperti yang dijelaskan dalam pertanyaan (urutan abjad dengan nama).

Jadi saya akan meninggalkannya di sini kalau-kalau itu bisa membantu seseorang.

Mark Byers
sumber
1
Mark - Ini bekerja dengan baik untuk kita. Terima kasih telah memberikan alternatif lain yang baik untuk memuji @ bluefeet's- sangat kami hargai.
Yarin
+1. Ini berhasil untuk saya. Benar-benar bersih dan to the point menjawab. Bisakah Anda jelaskan bagaimana tepatnya ini bekerja? Apa logika di balik ini?
Aditya Hajare
3
Solusi yang bagus tetapi tampaknya itu tidak berfungsi di lingkungan saya (MySQL 5.6) karena urutan demi klausa diterapkan setelah pilih sehingga tidak mengembalikan hasil teratas, lihat solusi alternatif saya untuk memperbaiki masalah ini
Laurent PELE
Saat menjalankan ini saya bisa menghapus JOIN (SELECT @prev := NULL, @rn := 0) AS vars. Saya mendapatkan idenya adalah mendeklarasikan variabel kosong, tetapi sepertinya tidak cocok untuk MySql.
Joseph Cho
1
Ini berfungsi baik untuk saya di MySQL 5.7, tetapi akan luar biasa jika seseorang dapat menjelaskan cara kerjanya
George B
41

Coba ini:

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

tembakau
sumber
6
snuffin keluar entah dari mana dengan solusi paling sederhana! Apakah ini lebih elegan daripada Ludo / Bill Karwin ? Bisakah saya mendapatkan komentar
Yarin
Hm, tidak yakin apakah itu lebih elegan. Tapi kalau dilihat dari suara, saya kira bluefeet mungkin punya solusi yang lebih baik.
snuffn
2
Ada masalah dengan ini. Jika ada dasi untuk posisi kedua dalam grup, hanya satu hasil teratas yang dikembalikan. Lihat demo yang
Yarin
2
Itu tidak masalah jika diinginkan. Anda dapat mengatur urutan a.person.
Alberto Leal
tidak, ini tidak berfungsi dalam kasus saya, begitu juga DEMO tidak berfungsi
Choix
31

Bagaimana dengan menggunakan self-join:

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;

memberi saya:

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

Saya sangat terinspirasi oleh jawaban dari Bill Karwin untuk Memilih 10 catatan teratas untuk setiap kategori

Juga, saya menggunakan SQLite, tetapi ini harus bekerja pada MySQL.

Hal lain: di atas, saya mengganti groupkolom dengan groupnamekolom untuk kenyamanan.

Edit :

Menindaklanjuti komentar OP mengenai hasil dasi yang hilang, saya menambahkan jawaban snuffin untuk menunjukkan semua ikatan. Ini berarti bahwa jika yang terakhir adalah ikatan, lebih dari 2 baris dapat dikembalikan, seperti yang ditunjukkan di bawah ini:

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

memberi saya:

person      groupname   age       
----------  ----------  ----------
Shawn       1           42        
Jill        1           34        
Laura       2           39        
Paul        2           36        
Joe         2           36        
Chuck       3           112      
Komunitas
sumber
@ Ludo- Baru saja melihat jawaban dari Bill Karwin - terima kasih telah menerapkannya di sini
Yarin
Apa pendapat Anda tentang jawaban Snuffin? Saya mencoba membandingkan keduanya
Yarin
2
Ada masalah dengan ini. Jika ada dasi untuk posisi kedua dalam grup, hanya satu hasil teratas yang dikembalikan- Lihat demo
Yarin
1
@ Ludo- persyaratan asli adalah bahwa setiap kelompok mengembalikan hasil yang tepat, dengan ikatan apa pun diselesaikan secara abjad
Yarin
Hasil edit untuk menyertakan ikatan tidak berfungsi untuk saya. Saya mendapatkan ERROR 1242 (21000): Subquery returns more than 1 row, mungkin karena GROUP BY. Ketika saya menjalankan SELECT MINsubquery sendirian, itu menghasilkan tiga baris: 34, 39, 112dan di sana nampak nilai kedua harus 36, bukan 39.
verbamour
12

Solusi snuffin tampaknya cukup lambat untuk dieksekusi ketika Anda memiliki banyak baris dan solusi Mark Byers / Rick James dan Bluefeet tidak berfungsi pada lingkungan saya (MySQL 5.6) karena pesanan oleh diterapkan setelah eksekusi pilih, jadi di sini ada varian solusi Marc Byers / Rick James untuk memperbaiki masalah ini (dengan pilihan tambahan yang diisikan):

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;

Saya mencoba permintaan serupa pada tabel yang memiliki 5 juta baris dan hasilnya kembali dalam waktu kurang dari 3 detik

Laurent PELE
sumber
3
Ini adalah satu-satunya permintaan yang berfungsi di lingkungan saya. Terima kasih!
herrherr
3
Tambahkan LIMIT 9999999ke tabel turunan mana pun dengan ORDER BY. Ini dapat mencegah ORDER BYdari diabaikan.
Rick James
Saya menjalankan kueri serupa di atas meja yang berisi beberapa ribu baris, dan butuh 60 detik untuk mengembalikan satu hasil, jadi ... terima kasih untuk postingnya, ini awal bagi saya. (ETA: turun ke 5 detik. Bagus!)
Evan
10

Lihat ini:

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

Travesty3
sumber
5
Sobat, yang lain menemukan solusi yang lebih sederhana ... Saya hanya menghabiskan waktu 15 menit untuk ini dan sangat bangga pada diri saya sendiri karena menghasilkan solusi yang rumit juga. Itu menyebalkan.
Travesty3
Saya harus menemukan nomor versi internal yang 1 kurang dari saat ini - ini memberi saya jawaban untuk melakukan ini: max(internal_version - 1)- jadi lebih sedikit stres :)
Jamie Strauss
8

Jika jawaban lain tidak cukup cepat. Cobalah kode ini :

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;

Keluaran:

+---------------------------+------+------------------+------------+
| 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 | ...
Rick James
sumber
Melihat situs Anda - di mana saya akan mendapatkan sumber data untuk populasi kota? TIA dan rgs.
Vérace
maxmind.com/en/worldcities - Saya merasa mudah untuk bereksperimen dengan pencarian lat / lng , pertanyaan, partisi, dll. Cukup besar untuk menjadi menarik, namun cukup mudah dibaca untuk mengenali jawaban. Subset Kanada berguna untuk pertanyaan seperti ini. (Lebih sedikit provinsi daripada kota di AS.)
Rick James
2

Saya ingin membagikan ini karena saya menghabiskan waktu yang lama mencari cara mudah untuk mengimplementasikan ini dalam program java yang sedang saya kerjakan. Ini tidak cukup memberikan output yang Anda cari tetapi lebih dekat. Fungsi di mysql disebut GROUP_CONCAT()berfungsi dengan sangat baik untuk menentukan berapa banyak hasil yang dikembalikan di masing-masing kelompok. Menggunakan LIMITatau cara-cara mewah lainnya untuk mencoba melakukan ini dengan COUNTtidak berhasil untuk saya. Jadi, jika Anda mau menerima hasil modifikasi, itu solusi yang bagus. Katakanlah saya memiliki meja yang disebut 'siswa' dengan id siswa, jenis kelamin mereka, dan gpa. Katakanlah saya ingin top 5 gpa untuk setiap jenis kelamin. Maka saya bisa menulis kueri seperti ini

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

Perhatikan bahwa parameter '5' menunjukkan berapa banyak entri untuk digabungkan ke dalam setiap baris

Dan hasilnya akan terlihat seperti

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

Anda juga dapat mengubah ORDER BYvariabel dan memesannya dengan cara yang berbeda. Jadi jika saya memiliki usia siswa saya dapat mengganti 'gpa desc' dengan 'age desc' dan itu akan berhasil! Anda juga dapat menambahkan variabel ke grup dengan pernyataan untuk mendapatkan lebih banyak kolom di output. Jadi ini hanya cara yang saya temukan yang cukup fleksibel dan berfungsi baik jika Anda ok dengan hanya daftar hasil.

Jon Bown
sumber
0

Dalam SQL Server row_numer()adalah fungsi yang kuat yang bisa mendapatkan hasil dengan mudah seperti di bawah ini

select Person,[group],age
from
(
select * ,row_number() over(partition by [group] order by age desc) rn
from mytable
) t
where rn <= 2
Prakash
sumber
Dengan 8.0 dan 10.2 menjadi GA, jawaban ini menjadi masuk akal.
Rick James
@ RickJames apa artinya 'menjadi GA'? Fungsi jendela ( dev.mysql.com/doc/refman/8.0/en/window-functions.html ) memecahkan masalah saya dengan sangat baik.
iedmrc
1
@iedmrc - "GA" berarti "Umumnya Tersedia". Ini adalah teknologi berbicara untuk "siap untuk prime time", atau "dirilis". Mereka melalui pengembangan versi dan akan fokus pada bug yang mereka lewatkan. Tautan itu membahas implementasi MySQL 8.0, yang mungkin berbeda dari implementasi MariaDB 10.2.
Rick James
-1

Ada jawaban yang sangat bagus untuk masalah ini di MySQL - Cara Mendapatkan Baris N Top per Setiap Grup

Berdasarkan solusi dalam tautan yang dirujuk, kueri Anda akan seperti:

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;

di mana nadalah top ndan your_tableadalah nama meja Anda.

Saya pikir penjelasan dalam referensi sangat jelas. Untuk referensi cepat saya akan menyalin dan menempelkannya di sini:

Saat ini MySQL tidak mendukung fungsi ROW_NUMBER () yang dapat menetapkan nomor urut dalam sebuah grup, tetapi sebagai solusi kita dapat menggunakan variabel sesi MySQL.

Variabel-variabel ini tidak memerlukan deklarasi, dan dapat digunakan dalam kueri untuk melakukan perhitungan dan untuk menyimpan hasil antara.

@current_country: = negara Kode ini dijalankan untuk setiap baris dan menyimpan nilai kolom negara ke @current_country variabel.

@country_rank: = JIKA (@current_country = negara, @country_rank + 1, 1) Dalam kode ini, jika @current_country adalah peringkat kenaikan yang sama, jika tidak maka setel ke 1. Untuk baris pertama @current_country adalah NULL, maka peringkatnya adalah juga diatur ke 1.

Untuk peringkat yang benar, kita perlu memiliki ORDER OLEH negara, populasi DESC

kovac
sumber
Nah, itu adalah prinsip yang digunakan oleh solusi Marc Byers, Rick James dan saya.
Laurent PELE
Sulit mengatakan pos mana (Stack Overflow atau SQLlines) adalah yang pertama
Laurent PELE
@LaurentPELE - Milik saya diposting Feb, 2015. Saya tidak melihat cap waktu atau nama di SQLlines. Blog MySQL telah ada cukup lama sehingga beberapa di antaranya ketinggalan zaman, dan harus dihapus - orang mengutip informasi yang salah.
Rick James