Verwenden Sie LIMIT in GROUP BY, um N Ergebnisse pro Gruppe zu erhalten?


387

Die folgende Abfrage:

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

Ausbeuten:

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

Was ich möchte, sind nur die Top 5 Ergebnisse für jede 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

Gibt es eine Möglichkeit, dies mit einem LIMIT-ähnlichen Modifikator zu tun, der innerhalb von GROUP BY funktioniert?


10
Dies kann in MySQL erfolgen, ist jedoch nicht so einfach wie das Hinzufügen einer LIMITKlausel. Hier ist ein Artikel, der das Problem ausführlich erklärt: So wählen Sie die erste / kleinste / maximale Zeile pro Gruppe in SQL aus Es ist ein guter Artikel - er führt eine elegante, aber naive Lösung für das Problem "Top N pro Gruppe" ein und dann schrittweise verbessert es.
Danben

SELECT * FROM (SELECT Jahr, ID, Rate FROM h WHERE Jahr ZWISCHEN 2000 UND 2009 UND ID IN (SELECT rid FROM table2) GRUPPE NACH ID, Jahr ORDER BY ID, Rate DESC) LIMIT 5
Mixcoatl

Antworten:


115

Sie könnten verwenden GROUP_CONCAT aggregiert Funktion , um alle Jahre in einer einzigen Spalte zu erhalten, gruppiert nach idund geordnet nach rate:

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

Ergebnis:

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

Und dann könnten Sie FIND_IN_SET verwenden , das die Position des ersten Arguments innerhalb des zweiten zurückgibt, z.

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

Mit einer Kombination aus GROUP_CONCATund FIND_IN_SETund Filtern nach der von find_in_set zurückgegebenen Position können Sie dann diese Abfrage verwenden, die nur die ersten 5 Jahre für jede ID zurückgibt:

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;

Bitte sehen Sie Geige hier .

Beachten Sie, dass Sie, wenn mehr als eine Zeile dieselbe Rate haben kann, die Verwendung von GROUP_CONCAT (DISTINCT rate ORDER BY rate) in der Tarifspalte anstelle der Jahresspalte in Betracht ziehen sollten.

Die maximale Länge der von GROUP_CONCAT zurückgegebenen Zeichenfolge ist begrenzt. Dies funktioniert daher gut, wenn Sie für jede Gruppe einige Datensätze auswählen müssen.


3
Das ist wunderschön performant, vergleichsweise einfach und eine großartige Erklärung. ich danke dir sehr. Bis zu Ihrem letzten Punkt, wo eine vernünftige maximale Länge berechnet werden kann, kann man SET SESSION group_concat_max_len = <maximum length>;im Fall des OP ein Nicht-Problem verwenden (da der Standardwert 1024 ist), aber zum Beispiel sollte group_concat_max_len mindestens 25: 4 (max Länge einer Jahreszeichenfolge) + 1 (Trennzeichen), mal 5 (erste 5 Jahre). Die Zeichenfolgen werden abgeschnitten, anstatt einen Fehler auszulösen. Achten Sie daher auf Warnungen wie z 1054 rows in set, 789 warnings (0.31 sec).
Timothy Johns

Wenn ich will genau zwei Zeilen holen , anstatt 1 bis 5 als das, was soll ich mit verwenden FIND_IN_SET(). Ich habe versucht, FIND_IN_SET() =2aber nicht das erwartete Ergebnis zu zeigen.
Amogh

FIND_IN_SET ZWISCHEN 1 und 5 nimmt die ersten 5 Positionen von GROUP_CONCAT ein, wenn die Größe gleich oder größer als 5 ist. FIND_IN_SET = 2 nimmt also nur die Daten mit der 2. Position in Ihrer GROUP_CONCAT. Wenn Sie 2 Zeilen erhalten, können Sie ZWISCHEN 1 und 2 für die 1. und 2. Position versuchen, vorausgesetzt, der Satz enthält 2 Zeilen.
jDub9

Diese Lösung bietet eine viel bessere Leistung als die von Salman für große Datenmengen. Ich habe beiden sowieso einen Daumen hoch für solch clevere Lösungen gegeben. Vielen Dank!!
Tiomno

105

Die ursprüngliche Abfrage verwendete Benutzervariablen und ORDER BYabgeleitete Tabellen. Das Verhalten beider Macken ist nicht garantiert. Überarbeitete Antwort wie folgt.

In MySQL 5.x können Sie den Rang eines armen Mannes über der Partition verwenden, um das gewünschte Ergebnis zu erzielen. Verbinden Sie die Tabelle einfach mit sich selbst und zählen Sie für jede Zeile die Anzahl der Zeilen, die kleiner als diese sind. Im obigen Fall ist die kleinere Reihe die mit der höheren Rate:

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 und Ergebnis :

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

Beachten Sie, dass, wenn die Preise Bindungen hatten, zum Beispiel:

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

Die obige Abfrage gibt 6 Zeilen zurück:

100, 90, 90, 80, 80, 80

Wechseln Sie zu HAVING COUNT(DISTINCT l.rate) < 5, um 8 Zeilen zu erhalten:

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

Oder wechseln Sie zu ON t.id = l.id AND (t.rate < l.rate OR (t.rate = l.rate AND t.pri_key > l.pri_key)), um 5 Zeilen zu erhalten:

 100, 90, 90, 80, 80

In MySQL 8 oder verwenden Sie später nur die RANK, DENSE_RANKoderROW_NUMBER Funktionen:

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

7
Ich denke, es ist erwähnenswert, dass der Schlüsselteil darin besteht, NACH ID zu BESTELLEN, da jede Änderung des Werts von ID die Zählung im Rang neu startet.
Ruuter

Warum sollte ich es zweimal ausführen, um die Antwort von zu erhalten WHERE rank <=5? Zum ersten Mal bekomme ich nicht 5 Zeilen von jeder ID, aber danach kann ich bekommen, wie du gesagt hast.
Brenno Leal

@BrennoLeal Ich denke, Sie vergessen die SETAussage (siehe erste Abfrage). Es ist notwendig.
Salman A

3
In neueren Versionen können und werden die ORDER BYin der abgeleiteten Tabelle enthaltenen häufig ignoriert. Dies besiegt das Ziel. Effiziente gruppenweise finden Sie hier .
Rick James

1
+1 Ihre Antwortumschreibung ist sehr gültig, da moderne MySQL / MariaDB-Versionen mehr den ANSI / ISO SQL-Standards 1992/1999/2003 folgen, wo sie ORDER BYin solchen Lieferungen / Unterabfragen nie wirklich verwendet werden durften. Dies ist der Grund dafür Moderne MySQL / MariaDB-Versionen ignorieren die ORDER BYIn-Unterabfrage ohne Verwendung LIMIT. Ich glaube, ANSI / ISO SQL-Standards 2008/2011/2016 machen ORDER BYLiefer- / Unterabfragen legal, wenn sie in Kombination mitFETCH FIRST n ROWS ONLY
Raymond Nijland,

21

Für mich so etwas wie

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

funktioniert perfekt. Keine komplizierte Abfrage.


Zum Beispiel: Holen Sie sich die Top 1 für jede Gruppe

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;

Ihre Lösung hat perfekt funktioniert, aber ich möchte auch Jahr und andere Spalten aus der Unterabfrage abrufen. Wie können wir das tun?
MaNn

9

Nein, Sie können Unterabfragen nicht willkürlich begrenzen (Sie können dies in neueren MySQLs in begrenztem Umfang tun, jedoch nicht für 5 Ergebnisse pro Gruppe).

Dies ist eine gruppenweise maximale Typabfrage, die in SQL nicht trivial ist. Es gibt verschiedene Möglichkeiten , um das anzugehen, was in einigen Fällen effizienter sein kann, aber für Top-n im Allgemeinen sollten Sie sich Bills Antwort auf eine ähnliche vorherige Frage ansehen .

Wie bei den meisten Lösungen für dieses Problem können mehr als fünf Zeilen zurückgegeben werden, wenn mehrere Zeilen mit demselben rateWert vorhanden sind. Daher müssen Sie möglicherweise noch einige Nachbearbeitungen durchführen, um dies zu überprüfen.


9

Dies erfordert eine Reihe von Unterabfragen, um die Werte zu ordnen, zu begrenzen und dann die Summe während der Gruppierung auszuführen

@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

Versuche dies:

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
unbekannte Spalte a.Typ in Feldliste
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

Die Unterabfrage ist fast identisch mit Ihrer Abfrage. Nur die Änderung wird hinzugefügt

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

8
Das ist schön, aber MySQL hat keine Fensterfunktionen (wie ROW_NUMBER()).
Ypercubeᵀᴹ

3
Ab MySQL 8.0 row_number()ist verfügbar .
Erickg

4

Erstellen Sie die virtuellen Spalten (wie RowID in Oracle)

Tabelle:

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

Daten:

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 wie folgt:

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;

Wenn Sie die where-Klausel in t3 löschen, wird Folgendes angezeigt:

Geben Sie hier die Bildbeschreibung ein

GET "TOP N Record" -> füge das "rownum <= 3" in die where-Klausel (die where-Klausel von t3) ein;

WÄHLEN SIE "das Jahr" -> fügen Sie das "ZWISCHEN 2000 UND 2009" in die where-Klausel (die where-Klausel von t3) ein;


Wenn Sie Raten haben, die sich für dieselbe ID wiederholen, funktioniert dies nicht, da Ihre rowNum-Anzahl höher wird. Sie erhalten nicht 3 pro Zeile, Sie können 0, 1 oder 2 erhalten. Können Sie sich eine Lösung dafür vorstellen?
Starvator

@starvator ändere die "t1.rate <= t2.rate" in "t1.rate <t2.rate". Wenn die beste Rate dieselben Werte in derselben ID hat, haben alle das gleiche Rownum, werden aber nicht höher. wie "rate 8 in id p01", wenn es wiederholt wird, indem "t1.rate <t2.rate" verwendet wird, haben beide von "rate 8 in id p01" das gleiche Rownum 0; Bei Verwendung von "t1.rate <= t2.rate" beträgt das Rownum 2;
Wang Wen'an

3

Es hat einige Arbeit gekostet, aber ich denke, meine Lösung wäre etwas zu teilen, da sie sowohl elegant als auch recht schnell erscheint.

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

Beachten Sie, dass dieses Beispiel für den Zweck der Frage angegeben wurde und für andere ähnliche Zwecke recht einfach geändert werden kann.


2

Der folgende Beitrag: sql: Die Auswahl des Top-N-Datensatzes pro Gruppe beschreibt die komplizierte Methode, um dies ohne Unterabfragen zu erreichen.

Es verbessert andere hier angebotene Lösungen von:

  • Alles in einer einzigen Abfrage erledigen
  • Indizes richtig nutzen können
  • Vermeiden von Unterabfragen, von denen bekannt ist, dass sie in MySQL zu schlechten Ausführungsplänen führen

Es ist jedoch nicht schön. Eine gute Lösung wäre erreichbar, wenn Fensterfunktionen (auch als analytische Funktionen bezeichnet) in MySQL aktiviert wären - dies ist jedoch nicht der Fall. Der in diesem Beitrag verwendete Trick verwendet GROUP_CONCAT, das manchmal als "Fensterfunktionen des armen Mannes für MySQL" bezeichnet wird.


1

für diejenigen wie mich, die eine Auszeit hatten. Ich habe das Folgende gemacht, um Grenzen und alles andere von einer bestimmten Gruppe zu verwenden.

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

Es durchläuft eine Liste von Domänen und fügt dann jeweils nur ein Limit von 200 ein


1

Versuche dies:

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

Bitte versuchen Sie es mit der unten gespeicherten Prozedur. Ich habe bereits überprüft. Ich erhalte das richtige Ergebnis, aber ohne es zu verwenden 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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.