web-dev-qa-db-de.com

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

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

ergibt:

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 einer Art LIMIT-Modifikator zu tun, der in GROUP BY funktioniert?

348
Wells

Sie können die aggregierte Funktion GROUP_CONCAT verwenden, um alle Jahre in eine einzelne Spalte zu bringen, die nach id gruppiert und nach rate sortiert ist:

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

Verwenden Sie eine Kombination aus GROUP_CONCAT und FIND_IN_SET und 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 siehe Geige hier .

Beachten Sie, dass Sie in Betracht ziehen sollten, GROUP_CONCAT (DISTINCT rate ORDER BY rate) in der Tarifspalte anstelle der Jahresspalte zu verwenden, wenn mehr als eine Zeile den gleichen Tarif haben kann.

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

101
fthiella

Die rsprüngliche Abfrage verwendete Benutzervariablen und ORDER BY Für abgeleitete 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. Einfach die Tabelle mit sich selbst verbinden und für jede Zeile die Anzahl der Zeilen zählen kleiner als sie. Im obigen Fall ist die kleinere Zeile diejenige 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 Kurse Krawatten 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

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

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

Oder ändern 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

Verwenden Sie in MySQL 8 oder höher einfach die Funktionen RANK, DENSE_RANK Oder ROW_NUMBER :

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

Für mich sowas

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

funktioniert perfekt. Keine komplizierte Abfrage.


zum Beispiel: Holen Sie sich 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;
20
Vishal Kumar

Nein, Sie können Unterabfragen nicht beliebig BEGRENZEN (in neueren MySQLs können Sie dies 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 solltest du Bills Antwort zu einem ähnlichen vorherigen Thema schauen Frage.

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 rate -Wert vorhanden sind. Daher ist möglicherweise noch eine gewisse Nachbearbeitung erforderlich, um dies zu überprüfen.

9
bobince

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:[email protected]+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;
9
Saharsh Shah

Erstellen Sie die virtuellen Spalten (z. B. 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 dies folgendermaßen angezeigt:

enter image description here

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

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

4
Wang Wen'an

Es hat ein bisschen Arbeit gekostet, aber ich denke, meine Lösung ist etwas, das man teilen kann, da sie sowohl elegant als auch recht schnell wirkt.

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 wird und für andere ähnliche Zwecke recht einfach geändert werden kann.

3
John
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 fügt hinzu

row_number() over (partition by id order by rate DESC)
2
Ricky Moreno

Der folgende Beitrag: sql: Auswahl von Top-N-Datensätzen pro Gruppe beschreibt den komplizierten Weg, dies ohne Unterabfragen zu erreichen.

Es verbessert andere Lösungen, die hier angeboten werden:

  • Alles in einer einzigen Abfrage erledigen
  • Indizes richtig nutzen können
  • Vermeiden von Unterabfragen, die bekanntermaßen zu schlechten Ausführungsplänen in MySQL führen

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

2
Shlomi Noach

für diejenigen wie mich, die Fragen hatten Auszeit. Ich habe das Folgende gemacht, um Limits 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
Dev-Ria

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;
1
MLF

Bitte versuchen Sie unten gespeicherte Prozedur. Habe ich schon verifiziert Ich erhalte ein korrektes Ergebnis, aber ohne groupby zu verwenden.

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
0
Himanshu Patel