Grup başına N sonuç almak için GROUP BY içinde LIMIT mi kullanıyorsunuz?


387

Aşağıdaki sorgu:

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

verim:

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

Ne istiyorum sadece her id için ilk 5 sonuç:

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

Bunu GROUP BY içinde çalışan bir tür LIMIT değiştirici kullanarak yapmanın bir yolu var mı?


10
Bu MySQL'de yapılabilir, ancak bir LIMITcümle eklemek kadar basit değildir . İşte sorunu ayrıntılı olarak açıklayan bir makale: SQL'de grup başına ilk / en az / maksimum satırı seçme Bu iyi bir makale - "Grup başına en üst N" sorununa zarif ama naif bir çözüm getiriyor ve sonra yavaş yavaş üzerinde geliştirir.
danben

SELECT * FROM (SELECT yıl, id, oran 2000
HABER

Yanıtlar:


115

Sen kullanabilirsiniz GROUP_CONCAT tek bir sütun, göre gruplandırılmış içine bütün yıl almak için toplanması işlevi idtarafından ve sipariş rate:

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

Sonuç:

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

Ve sonra , ilk argümanın ikincisinin içindeki konumunu döndüren FIND_IN_SET komutunu kullanabilirsiniz .

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

Ve birleşimini kullanarak GROUP_CONCATve FIND_IN_SETfind_in_set tarafından döndürülen konuma göre filtreleme yaparak, her kimlik için yalnızca ilk 5 yılı döndüren bu sorguyu kullanabilirsiniz:

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;

Lütfen buraya bakın .

Birden fazla satırın aynı ücrete sahip olması durumunda, ücret sütunu üzerinde yıl sütunu yerine GROUP_CONCAT (DISTINCT oranı ORDER BY oranı) kullanmayı düşünmeniz gerektiğini lütfen unutmayın.

GROUP_CONCAT tarafından döndürülen dizenin maksimum uzunluğu sınırlıdır, bu nedenle her grup için birkaç kayıt seçmeniz gerekirse bu işe yarar.


3
Budur güzel , nispeten basit ve büyük açıklama ölçülebilir; çok teşekkür ederim. Son noktanıza, Makul bir maksimum uzunluğun hesaplanabileceği yerlerde, SET SESSION group_concat_max_len = <maximum length>;OP'nin durumunda, sorun olmayan (varsayılan 1024 olduğundan) bir sorun olabilir, ancak örnek olarak, group_concat_max_len en az 25: 4 (maks. yıl dizesi uzunluğu) + 1 (ayırıcı karakter), çarpı 5 (ilk 5 yıl). Dizeler hata atmak yerine kısaltılır, bu nedenle gibi uyarılara dikkat edin 1054 rows in set, 789 warnings (0.31 sec).
Timothy Johns

Ne ile kullanmalıyım 1'den 5 yerine tam 2 satır getirmek istiyorsanız FIND_IN_SET(). Denedim FIND_IN_SET() =2ama beklendiği gibi sonuç göstermiyor.
Amogh

FIND_IN_SET BETWEEN 1 ve 5, boyut 5'e eşit veya daha büyükse GROUP_CONCAT setinin ilk 5 konumunu alacaktır. Bu nedenle FIND_IN_SET = 2, GROUP_CONCAT'inizde yalnızca 2. konuma sahip verileri alacaktır. 2 satır almak, setin vermek için 2 satır olduğunu varsayarak 1. ve 2. konum için 1 ve 2 ARASINDA deneyebilirsiniz.
jDub9

Bu çözüm, büyük veri kümeleri için Salman'dan çok daha iyi bir performansa sahiptir. Zaten böyle akıllı çözümler için her ikisine de bir başparmak verdim. Teşekkürler!!
tiomno

105

Orijinal sorgu Kullanıcı değişkenleri ve kullanılan ORDER BYtüretilen tablolarda; her iki tuhaflığın davranışı garanti edilmez. Cevabı aşağıdaki gibi revize etti.

MySQL 5.x sürümünde, istediğiniz sonuca ulaşmak için bölümlere göre kötü adam sıralamasını kullanabilirsiniz. Sadece dış tabloyu kendisi ile birleştirin ve her satır için satır sayısını ondan daha az sayın . Yukarıdaki durumda, daha az satır, daha yüksek orana sahip satırdır:

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 ve Sonuç :

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

Oranların bağları varsa, örneğin:

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

Yukarıdaki sorgu 6 satır döndürür:

100, 90, 90, 80, 80, 80

HAVING COUNT(DISTINCT l.rate) < 58 satır almak için değiştirin :

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

Veya ON t.id = l.id AND (t.rate < l.rate OR (t.rate = l.rate AND t.pri_key > l.pri_key))5 satır almak için olarak değiştirin :

 100, 90, 90, 80, 80

MySQL 8 veya sonraki sürümlerinde RANK, DENSE_RANKveyaROW_NUMBER işlevlerini kullanın :

SELECT *
FROM (
    SELECT *, RANK() OVER (PARTITION BY id ORDER BY rate DESC) AS rnk
    FROM t
) AS x
WHERE rnk <= 5

7
Ben id değeri değerinde herhangi bir değişiklik rütbe sayma yeniden başlayacaktır çünkü anahtar kısmı ORDER BY id olduğunu belirtmek gerekir.
ruuter

Yanıt almak için neden iki kez çalıştırmalıyım WHERE rank <=5? İlk kez im her id 5 satır almıyorum, ama ondan sonra dediğin gibi alabilirim.
Brenno Leal

@BrennoLeal Bence SETifadeyi unutuyorsunuz (ilk sorguya bakınız). Bu gerekli.
Salman A

3
Daha yeni sürümlerde, ORDER BYtüretilmiş tablodaki ve genellikle göz ardı edilebilir. Bu hedefi yener. Etkin grup bilimi burada bulunur .
Rick James

1
+1 modern MySQL / MariaDB sürümleri ANSI / ISO SQL 1992/1999/2003 standartlarını daha fazla takip ettiği için yanıtınızı yeniden yazmak çok geçerli çünkü ORDER BYböyle teslim / alt sorgularda kullanılmasına asla izin verilmedi. Modern MySQL / MariaDB sürümleri ORDER BYkullanmadan alt sorguda görmezden gelir, LIMITinanıyorum ANSI / ISO SQL Standartları 2008/2011/2016 ORDER BYile birlikte kullanıldığında teslim / alt sorguları yasal hale getirirFETCH FIRST n ROWS ONLY
Raymond Nijland

21

Benim için böyle bir şey

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

Mükemmel çalışıyor. Karmaşık sorgu yok.


örneğin: her grup için ilk 1 olsun

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;

Çözümünüz mükemmel çalıştı, ancak alt sorgudan yıl ve diğer sütunları da almak istiyorum, Bunu nasıl yapabiliriz?
MaNn

9

Hayır, alt sorguları keyfi olarak LIMIT edemezsiniz (daha yeni MySQL'lerde sınırlı ölçüde yapabilirsiniz, ancak grup başına 5 sonuç için yapamazsınız).

Bu, SQL'de yapılması önemsiz olmayan grupsal olarak maksimum tipte bir sorgudur. Bazı durumlarda daha verimli olabilen bununla başa çıkmak için çeşitli yollar vardır , ancak genel olarak top-n için Bill'in benzer bir önceki soruya verdiği cevaba bakmak istersiniz .

Bu soruna yönelik çoğu çözümde olduğu gibi, aynı ratedeğere sahip birden fazla satır varsa, beşten fazla satır döndürebilir , bu nedenle bunu kontrol etmek için bir miktar işlem sonrası gerekebilir.


9

Bu, değerleri sıralamak, sınırlamak ve gruplandırırken toplamı gerçekleştirmek için bir dizi alt sorgu gerektirir

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

9

Bunu dene:

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;

1
alan listesindeki bilinmeyen sütun a. türü
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

Alt sorgu, sorgunuzla neredeyse aynıdır. Yalnızca değişiklik ekleniyor

row_number() over (partition by id order by rate DESC)

8
Bu güzel ama MySQL'in pencere işlevi yok (gibi ROW_NUMBER()).
ypercubeᵀᴹ

3
MySQL 8.0 itibariyle row_number()olan mevcut .
erickg

4

Sanal sütunları (Oracle'daki RowID gibi) oluşturun

tablosu:

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

veri:

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 böyle:

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;

t3'teki where yan tümcesini silerseniz şu şekilde görünür:

resim açıklamasını buraya girin

"TOP N Kaydını" ALIN -> "yan tümcesine <= 3" ifadesini ekleyin (burada t3'ün nerede-cümlesi);

"Yılı" SEÇİN -> 2000 ve 2009 yılları arasında "t3'ün nerede-hükmü" nü ekleyin;


Aynı kimlik için yinelenen oranlarınız varsa, rowNum sayınız daha yüksek olacağı için bu çalışmaz; her satırda 3, 0, 1 veya 2 elde edemezsiniz. Buna herhangi bir çözüm düşünebiliyor musunuz?
starvator

@starvator "t1.rate <= t2.rate" ifadesini "t1.rate <t2.rate" olarak değiştirin, eğer en iyi oran aynı kimlikte aynı değerlere sahipse, hepsi aynı rownum'a sahiptir, ancak daha fazla artmaz; "id p01 id'de rate 8" gibi, tekrarlarsa "t1.rate <t2.rate" kullanarak, "p01 id'de rate 8" in her ikisi de aynı rownum 0'a sahiptir; "t1.rat <= t2.rat" kullanılıyorsa, rownum 2'dir;
Wang Wen'an

3

Biraz çalıştım, ama çözümümün hem zarif hem de oldukça hızlı göründüğü için paylaşılacak bir şey olacağını düşünüyorum.

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

Bu örneğin soru amacıyla belirtildiğini ve diğer benzer amaçlar için kolayca değiştirilebileceğini unutmayın.


2

Aşağıdaki yazı: sql: grup başına en iyi N kaydı seçerek , bunu alt sorgular olmadan başarmanın karmaşık yolunu açıklar.

Burada sunulan diğer çözümlerde gelişir:

  • Her şeyi tek bir sorguda yapmak
  • Endeksleri uygun şekilde kullanabilme
  • MySQL'de kötü yürütme planları oluşturduğu bilinen alt sorgulardan kaçınmak

Ancak hoş değil. MySQL'de Pencere İşlevleri (diğer bir deyişle Analitik İşlevler) etkinleştirilmişse iyi bir çözüm elde edilebilirdi - ancak bu mümkün değil. Söz konusu yazıda kullanılan hile, bazen "fakir adamın MySQL için Pencere Fonksiyonları" olarak tanımlanan GROUP_CONCAT kullanır.


1

benim gibi sorular zaman aşımına uğrayanlar için. Belirli bir grup tarafından limitleri ve başka bir şeyi kullanmak için aşağıdakileri yaptım.

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 $$

bir etki alanı listesinde dolaşır ve ardından her biri yalnızca 200'lük bir sınır ekler


1

Bunu dene:

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;

0

Lütfen aşağıda saklı yordamı deneyin. Zaten doğruladım. Doğru sonuç alıyorum ama kullanmadan 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
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.