Menggunakan LIMIT dalam GROUP BY untuk mendapatkan hasil N per grup?

388

Pertanyaan berikut:

SELECT
year, id, rate
FROM h
WHERE year BETWEEN 2000 AND 2009
AND id IN (SELECT rid FROM table2)
GROUP BY id, year
ORDER BY id, rate DESC

hasil:

year    id  rate
2006    p01 8
2003    p01 7.4
2008    p01 6.8
2001    p01 5.9
2007    p01 5.3
2009    p01 4.4
2002    p01 3.9
2004    p01 3.5
2005    p01 2.1
2000    p01 0.8
2001    p02 12.5
2004    p02 12.4
2002    p02 12.2
2003    p02 10.3
2000    p02 8.7
2006    p02 4.6
2007    p02 3.3

Yang saya inginkan hanya 5 hasil teratas untuk setiap id:

2006    p01 8
2003    p01 7.4
2008    p01 6.8
2001    p01 5.9
2007    p01 5.3
2001    p02 12.5
2004    p02 12.4
2002    p02 12.2
2003    p02 10.3
2000    p02 8.7

Apakah ada cara untuk melakukan ini menggunakan semacam LIMIT seperti pengubah yang berfungsi dalam GROUP BY?

Sumur
sumber
10
Ini bisa dilakukan di MySQL, tetapi tidak sesederhana menambahkan LIMITklausa. Berikut ini adalah artikel yang menjelaskan masalah secara terperinci: Cara memilih baris pertama / paling sedikit / maksimum per grup dalam SQL Ini adalah artikel yang bagus - ia memperkenalkan solusi elegan namun naif untuk masalah "Top N per grup", dan kemudian secara bertahap memperbaikinya.
danben
PILIH * DARI (PILIH tahun, id, nilai DARI h MANA tahun ANTARA 2000 DAN 2009 DAN id DALAM (PILIH DARI tabel2) KELOMPOK DENGAN id, tahun PESANAN DENGAN id, kurs DESC) LIMIT 5
Mixcoatl

Jawaban:

115

Anda dapat menggunakan fungsi agregat GROUP_CONCAT untuk mendapatkan semua tahun ke dalam satu kolom, dikelompokkan berdasarkan iddan dipesan oleh rate:

SELECT   id, GROUP_CONCAT(year ORDER BY rate DESC) grouped_year
FROM     yourtable
GROUP BY id

Hasil:

-----------------------------------------------------------
|  ID | GROUPED_YEAR                                      |
-----------------------------------------------------------
| p01 | 2006,2003,2008,2001,2007,2009,2002,2004,2005,2000 |
| p02 | 2001,2004,2002,2003,2000,2006,2007                |
-----------------------------------------------------------

Dan kemudian Anda bisa menggunakan FIND_IN_SET , yang mengembalikan posisi argumen pertama di dalam argumen kedua, misalnya.

SELECT FIND_IN_SET('2006', '2006,2003,2008,2001,2007,2009,2002,2004,2005,2000');
1

SELECT FIND_IN_SET('2009', '2006,2003,2008,2001,2007,2009,2002,2004,2005,2000');
6

Menggunakan kombinasi GROUP_CONCATdan FIND_IN_SET, dan pemfilteran oleh posisi yang dikembalikan oleh find_in_set, Anda kemudian dapat menggunakan kueri ini yang hanya mengembalikan 5 tahun pertama untuk setiap id:

SELECT
  yourtable.*
FROM
  yourtable INNER JOIN (
    SELECT
      id,
      GROUP_CONCAT(year ORDER BY rate DESC) grouped_year
    FROM
      yourtable
    GROUP BY id) group_max
  ON yourtable.id = group_max.id
     AND FIND_IN_SET(year, grouped_year) BETWEEN 1 AND 5
ORDER BY
  yourtable.id, yourtable.year DESC;

Silakan lihat biola di sini .

Harap perhatikan bahwa jika lebih dari satu baris dapat memiliki tarif yang sama, Anda harus mempertimbangkan untuk menggunakan GROUP_CONCAT (HUBUNGI tarif ORDER BY rate) pada kolom tarif alih-alih kolom tahun.

Panjang maksimum string yang dikembalikan oleh GROUP_CONCAT terbatas, jadi ini berfungsi dengan baik jika Anda perlu memilih beberapa catatan untuk setiap grup.

fthiella
sumber
3
Itu sangat bagus , relatif sederhana, dan penjelasan yang bagus; Terima kasih banyak. Sampai titik terakhir Anda, Di mana panjang maksimum yang masuk akal dapat dihitung, orang dapat menggunakan SET SESSION group_concat_max_len = <maximum length>;Dalam kasus OP, tidak ada masalah (karena standarnya adalah 1024), tetapi sebagai contoh, group_concat_max_len harus paling tidak 25: 4 (maks. panjang string tahun) + 1 (karakter pemisah), kali 5 (5 tahun pertama). String terpotong daripada melemparkan kesalahan, jadi perhatikan peringatan seperti 1054 rows in set, 789 warnings (0.31 sec).
Timothy Johns
Jika saya ingin mengambil tepat 2 baris daripada 1 sampai 5 dari apa yang harus saya gunakan FIND_IN_SET(). Saya mencoba FIND_IN_SET() =2tetapi tidak menunjukkan hasil seperti yang diharapkan.
Amogh
FIND_IN_SET ANTARA 1 dan 5 akan mengambil 5 posisi pertama dari GROUP_CONCAT yang ditetapkan jika ukurannya sama dengan atau lebih besar dari 5. Jadi FIND_IN_SET = 2 hanya akan mengambil data dengan posisi ke-2 di GROUP_CONCAT Anda. Mendapatkan 2 baris Anda dapat mencoba ANTARA 1 dan 2 untuk posisi 1 dan 2 dengan asumsi set memiliki 2 baris untuk diberikan.
jDub9
Solusi ini memiliki kinerja yang jauh lebih baik daripada Salman untuk kumpulan data besar. Saya memberikan jempol untuk solusi cerdas seperti itu. Terima kasih!!
tiomno
105

The query digunakan variabel pengguna dan ORDER BYpada tabel berasal; perilaku kedua kebiasaan tidak dijamin. Jawaban revisi sebagai berikut.

Di MySQL 5.x Anda dapat menggunakan peringkat orang miskin di atas partisi untuk mencapai hasil yang diinginkan. Luar gabung dengan tabel dengan sendirinya dan untuk setiap baris, hitung jumlah baris lebih rendah dari itu. Dalam kasus di atas, baris yang lebih rendah adalah baris dengan tingkat yang lebih tinggi:

SELECT t.id, t.rate, t.year, COUNT(l.rate) AS rank
FROM t
LEFT JOIN t AS l ON t.id = l.id AND t.rate < l.rate
GROUP BY t.id, t.rate, t.year
HAVING COUNT(l.rate) < 5
ORDER BY t.id, t.rate DESC, t.year

Demo dan Hasil :

| id  | rate | year | rank |
|-----|------|------|------|
| p01 |  8.0 | 2006 | 0    |
| p01 |  7.4 | 2003 | 1    |
| p01 |  6.8 | 2008 | 2    |
| p01 |  5.9 | 2001 | 3    |
| p01 |  5.3 | 2007 | 4    |
| p02 | 12.5 | 2001 | 0    |
| p02 | 12.4 | 2004 | 1    |
| p02 | 12.2 | 2002 | 2    |
| p02 | 10.3 | 2003 | 3    |
| p02 |  8.7 | 2000 | 4    |

Perhatikan bahwa jika tarif memiliki ikatan, misalnya:

100, 90, 90, 80, 80, 80, 70, 60, 50, 40, ...

Kueri di atas akan menghasilkan 6 baris:

100, 90, 90, 80, 80, 80

Ubah untuk HAVING COUNT(DISTINCT l.rate) < 5mendapatkan 8 baris:

100, 90, 90, 80, 80, 80, 70, 60

Atau ubah untuk ON t.id = l.id AND (t.rate < l.rate OR (t.rate = l.rate AND t.pri_key > l.pri_key))mendapatkan 5 baris:

 100, 90, 90, 80, 80

Di MySQL 8 atau lebih baru cukup gunakan RANK, DENSE_RANKatauROW_NUMBER fungsi:

SELECT *
FROM (
    SELECT *, RANK() OVER (PARTITION BY id ORDER BY rate DESC) AS rnk
    FROM t
) AS x
WHERE rnk <= 5
Salman A
sumber
7
Saya pikir perlu disebutkan bahwa bagian kuncinya adalah untuk MEMESAN OLEH id karena setiap perubahan nilai id akan mulai kembali menghitung dalam peringkat.
ruuter
Mengapa saya harus menjalankannya dua kali untuk mendapatkan respons WHERE rank <=5? Untuk pertama kalinya saya tidak mendapatkan 5 baris dari setiap id, tetapi setelah itu saya bisa mendapatkan seperti yang Anda katakan.
Brenno Leal
@ BrennoLeal Saya pikir Anda lupa SETpernyataan (lihat permintaan pertama). Itu perlu.
Salman A
3
Dalam versi yang lebih baru, ORDER BYdi dalam tabel turunan dapat, dan seringkali akan, diabaikan. Ini mengalahkan tujuan. Kelompok-bijaksana efisien ditemukan di sini .
Rick James
1
+1 jawaban Anda menulis ulang sangat valid, karena versi MySQL / MariaDB modern mengikuti standar ANSI / ISO SQL 1992/1999/2003 lebih banyak di mana itu tidak pernah benar-benar diizinkan untuk digunakan ORDER BYdi deliverd / subqueries seperti itu .. Itulah alasan mengapa versi modern MySQL / MariaDB mengabaikan ORDER BYsubquery tanpa menggunakan LIMIT, saya percaya ANSI / ISO SQL Standards 2008/2011/2016 membuat ORDER BYdeliverd / subqueries legal ketika menggunakannya dalam kombinasi denganFETCH FIRST n ROWS ONLY
Raymond Nijland
21

Bagi saya sesuatu seperti

SUBSTRING_INDEX(group_concat(col_name order by desired_col_order_name), ',', N) 

bekerja dengan sempurna. Tidak ada permintaan yang rumit.


misalnya: dapatkan 1 teratas untuk setiap grup

SELECT 
    *
FROM
    yourtable
WHERE
    id IN (SELECT 
            SUBSTRING_INDEX(GROUP_CONCAT(id
                            ORDER BY rate DESC),
                        ',',
                        1) id
        FROM
            yourtable
        GROUP BY year)
ORDER BY rate DESC;
Vishal Kumar
sumber
Solusi Anda bekerja dengan sempurna, tetapi saya juga ingin mengambil tahun dan kolom lainnya dari subquery, Bagaimana kita bisa melakukan itu?
Ny.
9

Tidak, Anda tidak dapat LIMIT subqueries secara sewenang-wenang (Anda dapat melakukannya sampai batas tertentu di MySQL yang lebih baru, tetapi tidak untuk 5 hasil per grup).

Ini adalah tipe query groupwise-maksimum, yang tidak sepele untuk dilakukan dalam SQL. Ada berbagai cara untuk mengatasi hal yang lebih efisien untuk beberapa kasus, tetapi untuk top-n pada umumnya Anda akan ingin melihat jawaban Bill untuk pertanyaan sebelumnya yang serupa.

Seperti kebanyakan solusi untuk masalah ini, ini dapat mengembalikan lebih dari lima baris jika ada beberapa baris dengan nilai yang sama rate, jadi Anda mungkin masih memerlukan sejumlah pasca pemrosesan untuk memeriksanya.

bobince
sumber
9

Ini membutuhkan serangkaian subquery untuk memeringkat nilai, membatasi mereka, lalu melakukan penjumlahan saat pengelompokan

@Rnk:=0;
@N:=2;
select
  c.id,
  sum(c.val)
from (
select
  b.id,
  b.bal
from (
select   
  if(@last_id=id,@Rnk+1,1) as Rnk,
  a.id,
  a.val,
  @last_id=id,
from (   
select 
  id,
  val 
from list
order by id,val desc) as a) as b
where b.rnk < @N) as c
group by c.id;
Brian L Cartwright
sumber
9

Coba ini:

SELECT h.year, h.id, h.rate 
FROM (SELECT h.year, h.id, h.rate, IF(@lastid = (@lastid:=h.id), @index:=@index+1, @index:=0) indx 
      FROM (SELECT h.year, h.id, h.rate 
            FROM h
            WHERE h.year BETWEEN 2000 AND 2009 AND id IN (SELECT rid FROM table2)
            GROUP BY id, h.year
            ORDER BY id, rate DESC
            ) h, (SELECT @lastid:='', @index:=0) AS a
    ) h 
WHERE h.indx <= 5;
Saharsh Shah
sumber
1
kolom tidak dikenal a.type dalam daftar bidang
anu
5
SELECT year, id, rate
FROM (SELECT
  year, id, rate, row_number() over (partition by id order by rate DESC)
  FROM h
  WHERE year BETWEEN 2000 AND 2009
  AND id IN (SELECT rid FROM table2)
  GROUP BY id, year
  ORDER BY id, rate DESC) as subquery
WHERE row_number <= 5

Subquery hampir identik dengan permintaan Anda. Hanya perubahan yang ditambahkan

row_number() over (partition by id order by rate DESC)
Ricky Moreno
sumber
8
Ini bagus tetapi MySQL tidak memiliki fungsi jendela (seperti ROW_NUMBER()).
ypercubeᵀᴹ
3
Pada MySQL 8.0, row_number()adalah tersedia .
erickg
4

Buat kolom virtual (seperti RowID di Oracle)

meja:

`
CREATE TABLE `stack` 
(`year` int(11) DEFAULT NULL,
`id` varchar(10) DEFAULT NULL,
`rate` float DEFAULT NULL) 
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
`

data:

insert into stack values(2006,'p01',8);
insert into stack values(2001,'p01',5.9);
insert into stack values(2007,'p01',5.3);
insert into stack values(2009,'p01',4.4);
insert into stack values(2001,'p02',12.5);
insert into stack values(2004,'p02',12.4);
insert into stack values(2005,'p01',2.1);
insert into stack values(2000,'p01',0.8);
insert into stack values(2002,'p02',12.2);
insert into stack values(2002,'p01',3.9);
insert into stack values(2004,'p01',3.5);
insert into stack values(2003,'p02',10.3);
insert into stack values(2000,'p02',8.7);
insert into stack values(2006,'p02',4.6);
insert into stack values(2007,'p02',3.3);
insert into stack values(2003,'p01',7.4);
insert into stack values(2008,'p01',6.8);

SQL seperti ini:

select t3.year,t3.id,t3.rate 
from (select t1.*, (select count(*) from stack t2 where t1.rate<=t2.rate and t1.id=t2.id) as rownum from stack t1) t3 
where rownum <=3 order by id,rate DESC;

jika menghapus klausa where di t3, ini menunjukkan seperti ini:

masukkan deskripsi gambar di sini

DAPATKAN "TOP N Record" -> tambahkan "rownum <= 3" di mana klausa (di mana-klausa t3);

PILIH "tahun" -> tambahkan "BETWEEN 2000 AND 2009" di mana klausa (di mana-klausa t3);

Wang Wen'an
sumber
Jika Anda memiliki tarif yang mengulang untuk id yang sama, maka ini tidak akan berhasil karena jumlah rowNum Anda akan meningkat lebih tinggi; Anda tidak akan mendapatkan 3 per baris, Anda bisa mendapatkan 0, 1 atau 2. Bisakah Anda memikirkan solusi apa pun untuk ini?
starvator
@starvator mengubah "t1.rate <= t2.rate" menjadi "t1.rate <t2.rate", jika tingkat terbaik memiliki nilai yang sama dalam id yang sama, semuanya memiliki rownum yang sama tetapi tidak akan meningkat lebih tinggi; seperti "rate 8 in id p01", jika diulangi, dengan menggunakan "t1.rate <t2.rate", kedua "rate 8 in id p01" memiliki rownum yang sama 0; jika menggunakan "t1.rate <= t2.rate", rownum adalah 2;
Wang Wen'an
3

Butuh beberapa kerja, tapi saya pikir solusi saya akan menjadi sesuatu untuk dibagikan karena tampaknya elegan dan juga cukup cepat.

SELECT h.year, h.id, h.rate 
  FROM (
    SELECT id, 
      SUBSTRING_INDEX(GROUP_CONCAT(CONCAT(id, '-', year) ORDER BY rate DESC), ',' , 5) AS l
      FROM h
      WHERE year BETWEEN 2000 AND 2009
      GROUP BY id
      ORDER BY id
  ) AS h_temp
    LEFT JOIN h ON h.id = h_temp.id 
      AND SUBSTRING_INDEX(h_temp.l, CONCAT(h.id, '-', h.year), 1) != h_temp.l

Perhatikan bahwa contoh ini ditentukan untuk tujuan pertanyaan dan dapat dimodifikasi dengan mudah untuk tujuan serupa lainnya.

John
sumber
2

Posting berikut: sql: memilih top N record per grup menjelaskan cara rumit untuk mencapai ini tanpa subqueries.

Ini meningkatkan solusi lain yang ditawarkan di sini oleh:

  • Melakukan semuanya dalam satu permintaan
  • Mampu memanfaatkan indeks dengan benar
  • Menghindari subkueri, yang terkenal dikenal untuk menghasilkan rencana eksekusi yang buruk di MySQL

Namun itu tidak cantik. Solusi yang baik akan dicapai adalah Fungsi Jendela (alias Fungsi Analitik) diaktifkan di MySQL - tetapi sebenarnya tidak. Trik yang digunakan dalam posting tersebut menggunakan GROUP_CONCAT, yang kadang-kadang digambarkan sebagai "Fungsi Jendela orang miskin untuk MySQL".

Shlomi Noach
sumber
1

untuk orang-orang seperti saya yang punya waktu tunggu. Saya membuat di bawah ini untuk menggunakan batas dan apa pun oleh kelompok tertentu.

DELIMITER $$
CREATE PROCEDURE count_limit200()
BEGIN
    DECLARE a INT Default 0;
    DECLARE stop_loop INT Default 0;
    DECLARE domain_val VARCHAR(250);
    DECLARE domain_list CURSOR FOR SELECT DISTINCT domain FROM db.one;

    OPEN domain_list;

    SELECT COUNT(DISTINCT(domain)) INTO stop_loop 
    FROM db.one;
    -- BEGIN LOOP
    loop_thru_domains: LOOP
        FETCH domain_list INTO domain_val;
        SET a=a+1;

        INSERT INTO db.two(book,artist,title,title_count,last_updated) 
        SELECT * FROM 
        (
            SELECT book,artist,title,COUNT(ObjectKey) AS titleCount, NOW() 
            FROM db.one 
            WHERE book = domain_val
            GROUP BY artist,title
            ORDER BY book,titleCount DESC
            LIMIT 200
        ) a ON DUPLICATE KEY UPDATE title_count = titleCount, last_updated = NOW();

        IF a = stop_loop THEN
            LEAVE loop_thru_domain;
        END IF;
    END LOOP loop_thru_domain;
END $$

loop melalui daftar domain dan kemudian hanya menyisipkan batas masing-masing 200

Dev-Ria
sumber
1

Coba ini:

SET @num := 0, @type := '';
SELECT `year`, `id`, `rate`,
    @num := if(@type = `id`, @num + 1, 1) AS `row_number`,
    @type := `id` AS `dummy`
FROM (
    SELECT *
    FROM `h`
    WHERE (
        `year` BETWEEN '2000' AND '2009'
        AND `id` IN (SELECT `rid` FROM `table2`) AS `temp_rid`
    )
    ORDER BY `id`
) AS `temph`
GROUP BY `year`, `id`, `rate`
HAVING `row_number`<='5'
ORDER BY `id`, `rate DESC;
MLF
sumber
0

Silakan coba prosedur tersimpan di bawah ini. Saya sudah memverifikasi. Saya mendapatkan hasil yang tepat tetapi tanpa menggunakan groupby.

CREATE DEFINER=`ks_root`@`%` PROCEDURE `first_five_record_per_id`()
BEGIN
DECLARE query_string text;
DECLARE datasource1 varchar(24);
DECLARE done INT DEFAULT 0;
DECLARE tenants varchar(50);
DECLARE cur1 CURSOR FOR SELECT rid FROM demo1;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;

    SET @query_string='';

      OPEN cur1;
      read_loop: LOOP

      FETCH cur1 INTO tenants ;

      IF done THEN
        LEAVE read_loop;
      END IF;

      SET @datasource1 = tenants;
      SET @query_string = concat(@query_string,'(select * from demo  where `id` = ''',@datasource1,''' order by rate desc LIMIT 5) UNION ALL ');

       END LOOP; 
      close cur1;

    SET @query_string  = TRIM(TRAILING 'UNION ALL' FROM TRIM(@query_string));  
  select @query_string;
PREPARE stmt FROM @query_string;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

END
Himanshu Patel
sumber