Dapatkan catatan dengan nilai maksimal untuk setiap grup hasil SQL yang dikelompokkan

229

Bagaimana Anda mendapatkan baris yang berisi nilai maks untuk setiap set yang dikelompokkan?

Saya telah melihat beberapa variasi yang terlalu rumit pada pertanyaan ini, dan tidak ada yang memiliki jawaban yang bagus. Saya telah mencoba menyusun contoh yang paling sederhana:

Diberikan tabel seperti itu di bawah ini, dengan orang, grup, dan kolom usia, bagaimana Anda mendapatkan orang tertua di setiap grup? (Dasi dalam grup harus memberikan hasil alfabet pertama)

Person | Group | Age
---
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    
Laura | 2     | 39  
Yarin
sumber
3
Perhatian: Jawaban yang Diterima berfungsi pada 2012 saat ditulis. Namun, itu tidak lagi berfungsi karena berbagai alasan, seperti yang diberikan dalam Komentar.
Rick James

Jawaban:

132

Ada cara super sederhana untuk melakukan ini di mysql:

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

Ini berfungsi karena di mysql Anda diizinkan untuk tidak mengumpulkan kolom non-grup-oleh, dalam hal ini mysql hanya mengembalikan baris pertama . Solusinya adalah dengan terlebih dahulu memesan data sedemikian rupa sehingga untuk setiap grup, baris yang Anda inginkan adalah yang pertama, kemudian kelompokkan dengan kolom yang Anda inginkan nilainya.

Anda menghindari subkueri rumit yang mencoba menemukan max()dll, dan juga masalah mengembalikan beberapa baris ketika ada lebih dari satu dengan nilai maksimum yang sama (seperti jawaban lain akan lakukan)

Catatan: Ini adalah solusi mysql-only . Semua database lain yang saya tahu akan melempar kesalahan sintaksis SQL dengan pesan "kolom tidak teragregasi tidak terdaftar dalam grup dengan klausa" atau serupa. Karena solusi ini menggunakan perilaku tidak berdokumen , yang lebih berhati-hati mungkin ingin menyertakan tes untuk menyatakan bahwa tetap berfungsi jika versi MySQL di masa depan mengubah perilaku ini.

Pembaruan versi 5.7:

Sejak versi 5.7, sql-modepengaturan termasuk ONLY_FULL_GROUP_BYsecara default, jadi untuk membuat ini berfungsi, Anda tidak boleh memiliki opsi ini (edit file opsi untuk server untuk menghapus pengaturan ini).

Bohemian
sumber
66
"mysql baru saja mengembalikan baris pertama." - mungkin ini cara kerjanya tetapi tidak dijamin. The dokumentasi mengatakan: "Server bebas memilih nilai apapun dari masing-masing kelompok, jadi kecuali mereka adalah sama, nilai-nilai yang dipilih adalah tak tentu." . Server tidak memilih baris tetapi nilai (tidak harus dari baris yang sama) untuk setiap kolom atau ekspresi yang muncul dalam SELECTklausa dan tidak dihitung menggunakan fungsi agregat.
Aksioma
16
Perilaku ini berubah pada MySQL 5.7.5 dan secara default, ini menolak permintaan ini karena kolom dalam SELECTklausa tidak secara fungsional tergantung pada GROUP BYkolom. Jika dikonfigurasi untuk menerimanya (`ONLY_FULL_GROUP_BY` dinonaktifkan), ini berfungsi seperti versi sebelumnya (yaitu nilai kolom tersebut tidak ditentukan).
Aksioma
17
Saya terkejut jawaban ini mendapat banyak upvotes. Itu salah dan itu buruk. Kueri ini tidak dijamin berfungsi. Data dalam subquery adalah set unordered terlepas dari urutan oleh klausa. MySQL mungkin benar - benar memesan catatan sekarang dan menyimpan pesanan itu, tetapi tidak akan melanggar aturan apa pun jika berhenti melakukannya di beberapa versi mendatang. Kemudian GROUP BYmengembun menjadi satu rekaman, tetapi semua bidang akan dipilih secara sewenang-wenang dari catatan. Ini mungkin bahwa MySQL saat ini hanya selalu mengambil baris pertama, tetapi bisa juga memilih setiap baris lain atau bahkan nilai-nilai dari berbagai baris dalam versi masa depan.
Thorsten Kettner
9
Oke, kami tidak setuju di sini. Saya tidak menggunakan fitur tidak berdokumen yang kebetulan berfungsi saat ini dan mengandalkan beberapa tes yang diharapkan akan mencakup ini. Anda tahu bahwa Anda hanya beruntung bahwa implementasi saat ini memberi Anda catatan lengkap pertama di mana dokumen dengan jelas menyatakan bahwa Anda mungkin mendapatkan nilai yang tidak pasti sebagai gantinya, tetapi Anda masih menggunakannya. Beberapa sesi atau pengaturan basis data sederhana dapat mengubahnya kapan saja. Saya menganggap ini terlalu berisiko.
Thorsten Kettner
3
Jawaban ini sepertinya salah. Per dokumen , server bebas untuk memilih nilai apa pun dari setiap grup ... Selain itu, pemilihan nilai dari setiap grup tidak dapat dipengaruhi dengan menambahkan klausa ORDER BY. Penyortiran kumpulan hasil terjadi setelah nilai dipilih, dan ORDER BY tidak memengaruhi nilai mana dalam setiap grup yang dipilih server.
Tgr
296

Solusi yang benar adalah:

SELECT o.*
FROM `Persons` o                    # 'o' from 'oldest person in group'
  LEFT JOIN `Persons` b             # 'b' from 'bigger age'
      ON o.Group = b.Group AND o.Age < b.Age
WHERE b.Age is NULL                 # bigger age not found

Bagaimana itu bekerja:

Ini cocok dengan setiap baris odengan semua baris dari bmemiliki nilai yang sama di kolom Groupdan nilai yang lebih besar di kolom Age. Baris apa pun yang otidak memiliki nilai maksimum grup dalam kolom Ageakan cocok dengan satu atau lebih baris dari b.

The LEFT JOINmembuatnya cocok dengan orang tertua dalam kelompok (termasuk orang-orang yang sendirian di kelompok mereka) dengan deretan penuh NULLs dari b( 'tidak ada usia terbesar dalam kelompok').
Menggunakan INNER JOINmembuat baris-baris ini tidak cocok dan mereka diabaikan.

The WHEREklausul terus hanya baris memiliki NULLs di bidang diekstrak dari b. Mereka adalah orang tertua dari masing-masing kelompok.

Bacaan lebih lanjut

Solusi ini dan banyak lainnya dijelaskan dalam buku SQL Antipatterns: Avoiding the Pitfalls of Database Programming

aksioma
sumber
43
BTW ini dapat mengembalikan dua baris atau lebih untuk grup yang sama jika o.Age = b.Age, misalnya jika Paul dari grup 2 aktif pada 39 seperti Laura. Namun, jika kita tidak menginginkan perilaku seperti itu, kita dapat melakukan:ON o.Group = b.Group AND (o.Age < b.Age or (o.Age = b.Age and o.id < b.id))
Todor
8
Luar biasa! Untuk catatan 20 juta rasanya 50 kali lebih cepat dari algoritma "naif" (bergabung dengan subquery dengan max ())
user2706534
3
Berfungsi sempurna dengan komentar @Toror. Saya akan menambahkan bahwa jika ada kondisi permintaan lebih lanjut, mereka harus ditambahkan di FROM dan di LEFT JOIN. Sesuatu SEPERTI: DARI (PILIH * DARI ORANG DI MANA Umur! = 32) o KIRI BERGABUNG (PILIH * DARI ORANG MANA Umur! = 32) b - jika Anda ingin memberhentikan orang yang berusia 32
Alain Zelink
1
@AlainZelink bukankah "kondisi permintaan lebih lanjut" ini lebih baik dimasukkan dalam daftar kondisi WHERE akhir, agar tidak memperkenalkan subqueries - yang tidak diperlukan dalam jawaban axiac @ asli?
tarilab
5
Solusi ini berhasil; namun, itu mulai dilaporkan dalam log kueri lambat ketika dicoba dengan 10.000+ baris yang berbagi ID yang sama. BERGABUNG di kolom yang diindeks. Kasus yang jarang, tapi kupikir layak disebut.
chaseisabelle
50

Anda dapat bergabung melawan subquery yang menarik MAX(Group)dan Age. Metode ini portabel di sebagian besar RDBMS.

SELECT t1.*
FROM yourTable t1
INNER JOIN
(
    SELECT `Group`, MAX(Age) AS max_age
    FROM yourTable
    GROUP BY `Group`
) t2
    ON t1.`Group` = t2.`Group` AND t1.Age = t2.max_age;
Michael Berkowski
sumber
Michael, terima kasih untuk ini- tetapi apakah Anda punya jawaban untuk masalah mengembalikan beberapa baris pada ikatan, sesuai komentar Bohemian?
Yarin
1
@Yarin Jika ada 2 baris misalnya di mana Group = 2, Age = 20, subquery akan mengembalikan salah satu dari mereka, tetapi ONklausa gabungan akan cocok dengan keduanya , jadi Anda akan mendapatkan 2 baris kembali dengan grup / umur yang sama meskipun vals berbeda untuk kolom lainnya, bukan satu.
Michael Berkowski
Jadi, apakah kita mengatakan tidak mungkin membatasi hasil menjadi satu per grup kecuali jika kita menggunakan rute khusus MySQL di Bohemia?
Yarin
@Yarin bukan tidak mustahil, hanya membutuhkan lebih banyak pekerjaan jika ada kolom tambahan - mungkin subquery bersarang lain untuk menarik id terkait terkait untuk setiap pasangan seperti kelompok / usia, kemudian bergabung melawan itu untuk mendapatkan sisa baris berdasarkan id.
Michael Berkowski
Ini harus menjadi jawaban yang diterima (jawaban yang saat ini diterima akan gagal pada sebagian besar RDBMS lainnya, dan bahkan akan gagal pada banyak versi MySQL).
Tim Biegeleisen
28

Solusi sederhana saya untuk SQLite (dan mungkin MySQL):

SELECT *, MAX(age) FROM mytable GROUP BY `Group`;

Namun itu tidak berfungsi di PostgreSQL dan mungkin beberapa platform lainnya.

Di PostgreSQL Anda dapat menggunakan klausa DISTINCT ON :

SELECT DISTINCT ON ("group") * FROM "mytable" ORDER BY "group", "age" DESC;
Igor Kulagin
sumber
@ Bohemian maaf, saya mengetahuinya, ini hanya MySQL karena mencakup kolom non-agregat
Cec
2
@IgorKulagin - Tidak berfungsi di Postgres- Pesan kesalahan: kolom "mytable.id" harus muncul di klausa GROUP BY atau digunakan dalam fungsi agregat
Yarin
13
Permintaan MySQL hanya dapat bekerja secara tidak sengaja pada banyak kesempatan. "SELECT *" dapat mengembalikan informasi yang tidak sesuai dengan MAX (usia) yang dimiliki. Jawaban ini salah. Ini mungkin juga kasus untuk SQLite.
Albert Hendriks
2
Tetapi ini cocok dengan kasus di mana kita perlu memilih kolom yang dikelompokkan dan kolom maks. Ini tidak sesuai dengan persyaratan di atas di mana ia akan menghasilkan ('Bob', 1, 42) tetapi hasil yang diharapkan adalah ('Shawn', 1, 42)
Ram Babu S
1
Baik untuk postgres
Karol Gasienica
4

Menggunakan metode peringkat.

SELECT @rn :=  CASE WHEN @prev_grp <> groupa THEN 1 ELSE @rn+1 END AS rn,  
   @prev_grp :=groupa,
   person,age,groupa  
FROM   users,(SELECT @rn := 0) r        
HAVING rn=1
ORDER  BY groupa,age DESC,person
sel
sumber
sel - butuh penjelasan - Aku belum pernah melihat :=sebelumnya - apa itu?
Yarin
1
: = adalah operator penugasan. Anda dapat membaca lebih lanjut di dev.mysql.com/doc/refman/5.0/id/user-variables.html
sel
Saya harus menggali ini- saya pikir jawabannya terlalu rumit skenario kita, tapi terima kasih telah mengajari saya sesuatu yang baru ..
Yarin
3

Tidak yakin apakah MySQL memiliki fungsi row_number. Jika demikian, Anda dapat menggunakannya untuk mendapatkan hasil yang diinginkan. Pada SQL Server Anda dapat melakukan sesuatu yang mirip dengan:

CREATE TABLE p
(
 person NVARCHAR(10),
 gp INT,
 age INT
);
GO
INSERT  INTO p
VALUES  ('Bob', 1, 32);
INSERT  INTO p
VALUES  ('Jill', 1, 34);
INSERT  INTO p
VALUES  ('Shawn', 1, 42);
INSERT  INTO p
VALUES  ('Jake', 2, 29);
INSERT  INTO p
VALUES  ('Paul', 2, 36);
INSERT  INTO p
VALUES  ('Laura', 2, 39);
GO

SELECT  t.person, t.gp, t.age
FROM    (
         SELECT *,
                ROW_NUMBER() OVER (PARTITION BY gp ORDER BY age DESC) row
         FROM   p
        ) t
WHERE   t.row = 1;
pengguna130268
sumber
1
Ya, sejak 8.0.
Ilja Everilä
2

solusi axiac adalah yang paling berhasil bagi saya pada akhirnya. Namun saya memiliki kompleksitas tambahan: "nilai maksimum" yang dihitung, berasal dari dua kolom.

Mari kita gunakan contoh yang sama: Saya ingin orang tertua di setiap grup. Jika ada orang yang sama-sama tua, ambil orang yang paling tinggi.

Saya harus melakukan join kiri dua kali untuk mendapatkan perilaku ini:

SELECT o1.* WHERE
    (SELECT o.*
    FROM `Persons` o
    LEFT JOIN `Persons` b
    ON o.Group = b.Group AND o.Age < b.Age
    WHERE b.Age is NULL) o1
LEFT JOIN
    (SELECT o.*
    FROM `Persons` o
    LEFT JOIN `Persons` b
    ON o.Group = b.Group AND o.Age < b.Age
    WHERE b.Age is NULL) o2
ON o1.Group = o2.Group AND o1.Height < o2.Height 
WHERE o2.Height is NULL;

Semoga ini membantu! Saya kira seharusnya ada cara yang lebih baik untuk melakukan ini ...

Arthur C
sumber
2

Solusi saya hanya berfungsi jika Anda hanya perlu mengambil satu kolom, namun untuk kebutuhan saya adalah solusi terbaik yang ditemukan dalam hal kinerja (hanya menggunakan satu permintaan tunggal!):

SELECT SUBSTRING_INDEX(GROUP_CONCAT(column_x ORDER BY column_y),',',1) AS xyz,
   column_z
FROM table_name
GROUP BY column_z;

Ini menggunakan GROUP_CONCAT untuk membuat daftar concat yang diurutkan dan kemudian saya substring hanya yang pertama.

Antonio Giovanazzi
sumber
Dapat mengonfirmasi bahwa Anda bisa mendapatkan beberapa kolom dengan mengurutkan pada kunci yang sama di dalam group_concat, tetapi perlu menulis group_concat / index / substring yang terpisah untuk setiap kolom.
Rasika
Bonus di sini adalah bahwa Anda dapat menambahkan beberapa kolom ke pengurutan di dalam group_concat dan itu akan menyelesaikan ikatan dengan mudah dan menjamin hanya satu catatan per grup. Dilakukan dengan baik pada solusi yang sederhana dan efisien!
Rasika
2

Saya punya solusi sederhana dengan menggunakan WHERE IN

SELECT a.* FROM `mytable` AS a    
WHERE a.age IN( SELECT MAX(b.age) AS age FROM `mytable` AS b GROUP BY b.group )    
ORDER BY a.group ASC, a.person ASC
Khalid Musa Sagar
sumber
1

Menggunakan CTE - Ekspresi Tabel Umum:

WITH MyCTE(MaxPKID, SomeColumn1)
AS(
SELECT MAX(a.MyTablePKID) AS MaxPKID, a.SomeColumn1
FROM MyTable1 a
GROUP BY a.SomeColumn1
  )
SELECT b.MyTablePKID, b.SomeColumn1, b.SomeColumn2 MAX(b.NumEstado)
FROM MyTable1 b
INNER JOIN MyCTE c ON c.MaxPKID = b.MyTablePKID
GROUP BY b.MyTablePKID, b.SomeColumn1, b.SomeColumn2

--Note: MyTablePKID is the PrimaryKey of MyTable
Marvin
sumber
1

Dalam Oracle di bawah ini, kueri dapat memberikan hasil yang diinginkan.

SELECT group,person,Age,
  ROWNUMBER() OVER (PARTITION BY group ORDER BY age desc ,person asc) as rankForEachGroup
  FROM tablename where rankForEachGroup=1
Kiruba
sumber
0
with CTE as 
(select Person, 
[Group], Age, RN= Row_Number() 
over(partition by [Group] 
order by Age desc) 
from yourtable)`


`select Person, Age from CTE where RN = 1`
Harshad
sumber
0

Anda juga bisa mencoba

SELECT * FROM mytable WHERE age IN (SELECT MAX(age) FROM mytable GROUP BY `Group`) ;
Ritwik
sumber
1
Terima kasih, meskipun ini mengembalikan banyak rekaman untuk usia saat ada dasi
Yarin
Selain itu, kueri ini akan salah dalam hal ada 39 tahun di grup 1. Dalam hal itu, orang itu juga akan dipilih, meskipun usia maks di grup 1 lebih tinggi.
Joshua Richardson
0

Saya tidak akan menggunakan Grup sebagai nama kolom karena kata itu dilindungi undang-undang. Namun mengikuti SQL akan berhasil.

SELECT a.Person, a.Group, a.Age FROM [TABLE_NAME] a
INNER JOIN 
(
  SELECT `Group`, MAX(Age) AS oldest FROM [TABLE_NAME] 
  GROUP BY `Group`
) b ON a.Group = b.Group AND a.Age = b.oldest
Bae Cheol Shin
sumber
Terima kasih, meskipun ini mengembalikan banyak rekaman untuk usia saat ada dasi
Yarin
@Yarin, bagaimana memutuskan orang tertua mana yang benar? Beberapa jawaban tampaknya menjadi jawaban yang paling tepat jika tidak menggunakan batas dan ketertiban
Duncan
0

Metode ini memiliki manfaat memungkinkan Anda untuk memberi peringkat dengan kolom yang berbeda, dan tidak merusak data lainnya. Ini cukup berguna dalam situasi di mana Anda mencoba mendaftar pesanan dengan kolom untuk item, daftar yang paling berat terlebih dahulu.

Sumber: http://dev.mysql.com/doc/refman/5.0/id/group-by-functions.html#function_group-concat

SELECT person, group,
    GROUP_CONCAT(
        DISTINCT age
        ORDER BY age DESC SEPARATOR ', follow up: '
    )
FROM sql_table
GROUP BY group;
Ray Foss
sumber
0

biarkan nama tabel menjadi orang

select O.*              -- > O for oldest table
from people O , people T
where O.grp = T.grp and 
O.Age = 
(select max(T.age) from people T where O.grp = T.grp
  group by T.grp)
group by O.grp; 
pengguna3475425
sumber
0

Jika ID (dan semua coulmns) diperlukan dari mytable

SELECT
    *
FROM
    mytable
WHERE
    id NOT IN (
        SELECT
            A.id
        FROM
            mytable AS A
        JOIN mytable AS B ON A. GROUP = B. GROUP
        AND A.age < B.age
    )
mayank kumar
sumber
0

Ini adalah bagaimana saya mendapatkan baris N maks per grup di mysql

SELECT co.id, co.person, co.country
FROM person co
WHERE (
SELECT COUNT(*)
FROM person ci
WHERE  co.country = ci.country AND co.id < ci.id
) < 1
;

bagaimana itu bekerja:

  • gabung sendiri ke meja
  • kelompok dilakukan oleh co.country = ci.country
  • N elemen per grup dikontrol oleh ) < 1jadi untuk 3 elemen -) <3
  • untuk mendapatkan maks atau minimum tergantung pada: co.id < ci.id
    • co.id <ci.id - maks
    • co.id> ci.id - min

Contoh lengkap di sini:

mysql pilih n nilai maksimum per grup

Vanko
sumber