Erhalten Sie die besten n Datensätze für jede Gruppe gruppierter Ergebnisse

140

Das Folgende ist das einfachste mögliche Beispiel, obwohl jede Lösung in der Lage sein sollte, auf so viele n Top-Ergebnisse zu skalieren, die benötigt werden:

Wie würden Sie bei einer folgenden Tabelle mit Spalten für Personen, Gruppen und Alter die beiden ältesten Personen in jeder Gruppe ermitteln? (Bindungen innerhalb von Gruppen sollten nicht zu mehr Ergebnissen führen, sondern die ersten 2 in alphabetischer Reihenfolge)

+ -------- + ------- + ----- +
| Person | Gruppe | Alter |
+ -------- + ------- + ----- +
| Bob | 1 | 32 |
| Jill | 1 | 34 |
| Shawn | 1 | 42 |
| Jake | 2 | 29 |
| Paul | 2 | 36 |
| Laura | 2 | 39 |
+ -------- + ------- + ----- +

Gewünschte Ergebnismenge:

+ -------- + ------- + ----- +
| Shawn | 1 | 42 |
| Jill | 1 | 34 |
| Laura | 2 | 39 |
| Paul | 2 | 36 |
+ -------- + ------- + ----- +

ANMERKUNG: Diese Frage baut auf einer früheren Frage auf: Abrufen von Datensätzen mit maximalem Wert für jede Gruppe gruppierter SQL-Ergebnisse - zum Abrufen einer einzelnen oberen Zeile aus jeder Gruppe, die von @Bohemian eine hervorragende MySQL-spezifische Antwort erhalten hat:

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

Würde gerne in der Lage sein, dies auszubauen, obwohl ich nicht sehe, wie.

Yarin
quelle
2
Überprüfen Sie dieses Beispiel. Es ist ziemlich nah an dem, was Sie fragen: stackoverflow.com/questions/1537606/…
Savas Vedova
Verwenden Sie LIMIT in GROUP BY, um N Ergebnisse pro Gruppe zu erhalten? stackoverflow.com/questions/2129693/…
Edye Chan

Antworten:

88

Hier ist eine Möglichkeit, dies mit UNION ALL(siehe SQL Fiddle with Demo ) zu tun . Dies funktioniert mit zwei Gruppen. Wenn Sie mehr als zwei Gruppen haben, müssen Sie die groupAnzahl angeben und Abfragen für jede hinzufügen group:

(
  select *
  from mytable 
  where `group` = 1
  order by age desc
  LIMIT 2
)
UNION ALL
(
  select *
  from mytable 
  where `group` = 2
  order by age desc
  LIMIT 2
)

Es gibt verschiedene Möglichkeiten, dies zu tun. In diesem Artikel finden Sie die beste Route für Ihre Situation:

http://www.xaprb.com/blog/2006/12/07/how-to-select-the-firstleastmax-row-per-group-in-sql/

Bearbeiten:

Dies funktioniert möglicherweise auch für Sie. Es wird eine Zeilennummer für jeden Datensatz generiert. Wenn Sie ein Beispiel aus dem obigen Link verwenden, werden nur die Datensätze mit einer Zeilennummer von weniger als oder gleich 2 zurückgegeben:

select person, `group`, age
from 
(
   select person, `group`, age,
      (@num:=if(@group = `group`, @num +1, if(@group := `group`, 1, 1))) row_number 
  from test t
  CROSS JOIN (select @num:=0, @group:=null) c
  order by `Group`, Age desc, person
) as x 
where x.row_number <= 2;

Siehe Demo

Taryn
quelle
52
Wenn er mehr als 1 000 Gruppen hat, würde das das nicht ein bisschen beängstigend machen?
Charles Forest
1
@CharlesForest ja, das würde es und deshalb habe ich angegeben, dass Sie es für mehr als zwei Gruppen angeben müssten. Es würde hässlich werden.
Taryn
1
@ CharlesForest Ich denke, ich habe eine bessere Lösung gefunden, siehe meine Bearbeitung
Taryn
1
Ein Hinweis für alle, die dies lesen: Die Version der Variablen ist nahezu korrekt. MySQL garantiert jedoch nicht die Reihenfolge der Auswertung von Ausdrücken in der SELECT(und bewertet sie manchmal sogar nicht in der richtigen Reihenfolge). Der Schlüssel zur Lösung besteht darin, alle Variablenzuweisungen in einem einzigen Ausdruck zusammenzufassen. Hier ist ein Beispiel: stackoverflow.com/questions/38535020/… .
Gordon Linoff
1
@ GordonLinoff Meine Antwort wurde aktualisiert, danke, dass Sie darauf hingewiesen haben. Es hat auch viel zu lange gedauert, bis ich es aktualisiert habe.
Taryn
63

In anderen Datenbanken können Sie dies mit verwenden ROW_NUMBER. MySQL wird nicht unterstützt, ROW_NUMBERaber Sie können Variablen verwenden, um es zu emulieren:

SELECT
    person,
    groupname,
    age
FROM
(
    SELECT
        person,
        groupname,
        age,
        @rn := IF(@prev = groupname, @rn + 1, 1) AS rn,
        @prev := groupname
    FROM mytable
    JOIN (SELECT @prev := NULL, @rn := 0) AS vars
    ORDER BY groupname, age DESC, person
) AS T1
WHERE rn <= 2

Sehen Sie, wie es online funktioniert: sqlfiddle


Bearbeiten Ich habe gerade bemerkt, dass bluefeet eine sehr ähnliche Antwort gepostet hat: +1 für ihn. Diese Antwort hat jedoch zwei kleine Vorteile:

  1. Es ist eine einzelne Abfrage. Die Variablen werden in der SELECT-Anweisung initialisiert.
  2. Es behandelt Krawatten wie in der Frage beschrieben (alphabetische Reihenfolge nach Namen).

Also lasse ich es hier, falls es jemandem helfen kann.

Mark Byers
quelle
1
Mark: Das funktioniert gut für uns. Vielen Dank für die Bereitstellung einer weiteren guten Alternative zu Kompliment @ bluefeet's - sehr geschätzt.
Yarin
+1. Das hat bei mir funktioniert. Wirklich sauber und auf den Punkt Antwort. Können Sie bitte erklären, wie genau das funktioniert? Was ist die Logik dahinter?
Aditya Hajare
3
Gute Lösung, aber es scheint, dass sie in meiner Umgebung nicht funktioniert (MySQL 5.6), da die order by-Klausel nach select angewendet wird, sodass nicht das beste Ergebnis zurückgegeben wird. Siehe meine alternative Lösung zur Behebung dieses Problems
Laurent PELE
Während ich dies ausführte, konnte ich löschen JOIN (SELECT @prev := NULL, @rn := 0) AS vars. Ich habe die Idee, leere Variablen zu deklarieren, aber es scheint für MySql irrelevant.
Joseph Cho
1
Dies funktioniert gut für mich in MySQL 5.7, aber es wäre großartig, wenn jemand erklären könnte, wie es funktioniert
George B
41

Versuche dies:

SELECT a.person, a.group, a.age FROM person AS a WHERE 
(SELECT COUNT(*) FROM person AS b 
WHERE b.group = a.group AND b.age >= a.age) <= 2 
ORDER BY a.group ASC, a.age DESC

DEMO

Schnupftabak
quelle
6
Schnupftabak kommt aus dem Nichts mit der einfachsten Lösung! Ist das eleganter als das von Ludo / Bill Karwin ? Kann ich einen Kommentar bekommen
Yarin
Hm, ich bin mir nicht sicher, ob es eleganter ist. Aber nach den Stimmen zu urteilen, könnte Bluefeet die bessere Lösung haben.
Schnupftabak
2
Es gibt ein Problem damit. Wenn es ein Unentschieden um den zweiten Platz innerhalb der Gruppe gibt, wird nur ein Top-Ergebnis zurückgegeben. Siehe modifizierte Demo
Yarin
2
Es ist kein Problem, wenn es gewünscht wird. Sie können die Reihenfolge festlegen a.person.
Alberto Leal
Nein, es funktioniert in meinem Fall nicht, und die DEMO funktioniert auch nicht
Choix
31

Wie wäre es mit Self-Joining:

CREATE TABLE mytable (person, groupname, age);
INSERT INTO mytable VALUES('Bob',1,32);
INSERT INTO mytable VALUES('Jill',1,34);
INSERT INTO mytable VALUES('Shawn',1,42);
INSERT INTO mytable VALUES('Jake',2,29);
INSERT INTO mytable VALUES('Paul',2,36);
INSERT INTO mytable VALUES('Laura',2,39);

SELECT a.* FROM mytable AS a
  LEFT JOIN mytable AS a2 
    ON a.groupname = a2.groupname AND a.age <= a2.age
GROUP BY a.person
HAVING COUNT(*) <= 2
ORDER BY a.groupname, a.age DESC;

gibt mir:

a.person    a.groupname  a.age     
----------  -----------  ----------
Shawn       1            42        
Jill        1            34        
Laura       2            39        
Paul        2            36      

Die Antwort von Bill Karwin, Top 10-Datensätze für jede Kategorie auszuwählen, hat mich stark inspiriert

Ich verwende auch SQLite, aber dies sollte unter MySQL funktionieren.

Eine andere Sache: Oben habe ich die groupSpalte der Einfachheit halber durch eine groupnameSpalte ersetzt.

Bearbeiten :

Nach dem Kommentar des OP zu fehlenden Gleichstandsergebnissen erhöhte ich die Antwort von Snuffin, um alle Gleichstände anzuzeigen. Dies bedeutet, dass, wenn die letzten Bindungen sind, mehr als 2 Zeilen zurückgegeben werden können, wie unten gezeigt:

.headers on
.mode column

CREATE TABLE foo (person, groupname, age);
INSERT INTO foo VALUES('Paul',2,36);
INSERT INTO foo VALUES('Laura',2,39);
INSERT INTO foo VALUES('Joe',2,36);
INSERT INTO foo VALUES('Bob',1,32);
INSERT INTO foo VALUES('Jill',1,34);
INSERT INTO foo VALUES('Shawn',1,42);
INSERT INTO foo VALUES('Jake',2,29);
INSERT INTO foo VALUES('James',2,15);
INSERT INTO foo VALUES('Fred',1,12);
INSERT INTO foo VALUES('Chuck',3,112);


SELECT a.person, a.groupname, a.age 
FROM foo AS a 
WHERE a.age >= (SELECT MIN(b.age)
                FROM foo AS b 
                WHERE (SELECT COUNT(*)
                       FROM foo AS c
                       WHERE c.groupname = b.groupname AND c.age >= b.age) <= 2
                GROUP BY b.groupname)
ORDER BY a.groupname ASC, a.age DESC;

gibt mir:

person      groupname   age       
----------  ----------  ----------
Shawn       1           42        
Jill        1           34        
Laura       2           39        
Paul        2           36        
Joe         2           36        
Chuck       3           112      
Gemeinschaft
quelle
@ Ludo- Habe gerade diese Antwort von Bill Karwin gesehen - danke, dass du sie hier
Yarin
Was denkst du über Snuffins Antwort? Ich versuche die beiden zu vergleichen
Yarin
2
Es gibt ein Problem damit. Wenn es ein Unentschieden um den zweiten Platz in der Gruppe gibt, wird nur ein Top-Ergebnis zurückgegeben - siehe Demo
Yarin
1
@ Ludo - die ursprüngliche Anforderung war, dass jede Gruppe die genauen n Ergebnisse zurückgibt, wobei alle Bindungen alphabetisch aufgelöst werden
Yarin
Die Bearbeitung, um die Krawatten einzuschließen, funktioniert bei mir nicht. Ich bekomme ERROR 1242 (21000): Subquery returns more than 1 rowvermutlich wegen der GROUP BY. Wenn ich die SELECT MINUnterabfrage alleine ausführe , werden drei Zeilen generiert: 34, 39, 112und dort scheint der zweite Wert 36 zu sein, nicht 39.
Verbamour
12

Die Ausführung der Snuffin-Lösung scheint ziemlich langsam zu sein, wenn Sie viele Zeilen haben und die Lösungen von Mark Byers / Rick James und Bluefeet in meiner Umgebung (MySQL 5.6) nicht funktionieren, da die Reihenfolge nach der Ausführung von select angewendet wird. Hier ist also eine Variante von Marc Byers / Rick James-Lösungen zur Behebung dieses Problems (mit einer zusätzlichen Auswahl):

select person, groupname, age
from
(
    select person, groupname, age,
    (@rn:=if(@prev = groupname, @rn +1, 1)) as rownumb,
    @prev:= groupname 
    from 
    (
        select person, groupname, age
        from persons 
        order by groupname ,  age desc, person
    )   as sortedlist
    JOIN (select @prev:=NULL, @rn :=0) as vars
) as groupedlist 
where rownumb<=2
order by groupname ,  age desc, person;

Ich habe eine ähnliche Abfrage für eine Tabelle mit 5 Millionen Zeilen versucht und das Ergebnis wird in weniger als 3 Sekunden zurückgegeben

Laurent PELE
quelle
3
Dies ist die einzige Abfrage, die in meiner Umgebung funktioniert hat. Vielen Dank!
Herrherr
3
Fügen Sie LIMIT 9999999zu jeder abgeleiteten Tabelle mit einem hinzu ORDER BY. Dies kann verhindern, dass das ORDER BYignoriert wird.
Rick James
Ich habe eine ähnliche Abfrage für eine Tabelle mit einigen tausend Zeilen ausgeführt, und es dauerte 60 Sekunden, bis ein Ergebnis zurückgegeben wurde. Also ... danke für den Beitrag, es ist ein Anfang für mich. (ETA: bis zu 5 Sekunden. Gut!)
Evan
10

Überprüfen Sie dies heraus:

SELECT
  p.Person,
  p.`Group`,
  p.Age
FROM
  people p
  INNER JOIN
  (
    SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`
    UNION
    SELECT MAX(p3.Age) AS Age, p3.`Group` FROM people p3 INNER JOIN (SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`) p4 ON p3.Age < p4.Age AND p3.`Group` = p4.`Group` GROUP BY `Group`
  ) p2 ON p.Age = p2.Age AND p.`Group` = p2.`Group`
ORDER BY
  `Group`,
  Age DESC,
  Person;

SQL Fiddle: http://sqlfiddle.com/#!2/cdbb6/15

Travestie3
quelle
5
Mann, andere haben viel einfachere Lösungen gefunden ... Ich habe gerade mal 15 Minuten damit verbracht und war unglaublich stolz auf mich, dass ich auch eine so komplizierte Lösung gefunden habe. Das ist Scheiße.
Travesty3
Ich musste eine interne Versionsnummer finden, die 1 weniger als die aktuelle war - dies gab mir die Antwort, um dies zu tun: max(internal_version - 1)- also weniger Stress :)
Jamie Strauss
7

Wenn die anderen Antworten nicht schnell genug sind Probieren Sie diesen Code aus :

SELECT
        province, n, city, population
    FROM
      ( SELECT  @prev := '', @n := 0 ) init
    JOIN
      ( SELECT  @n := if(province != @prev, 1, @n + 1) AS n,
                @prev := province,
                province, city, population
            FROM  Canada
            ORDER BY
                province   ASC,
                population DESC
      ) x
    WHERE  n <= 3
    ORDER BY  province, n;

Ausgabe:

+---------------------------+------+------------------+------------+
| province                  | n    | city             | population |
+---------------------------+------+------------------+------------+
| Alberta                   |    1 | Calgary          |     968475 |
| Alberta                   |    2 | Edmonton         |     822319 |
| Alberta                   |    3 | Red Deer         |      73595 |
| British Columbia          |    1 | Vancouver        |    1837970 |
| British Columbia          |    2 | Victoria         |     289625 |
| British Columbia          |    3 | Abbotsford       |     151685 |
| Manitoba                  |    1 | ...
Rick James
quelle
Sie haben sich Ihre Website angesehen - woher würde ich die Datenquelle für die Bevölkerung der Städte beziehen? TIA und rgs.
Vérace
maxmind.com/de/worldcities - Ich finde es praktisch, um mit lat / lng-Suchen , Abfragen, Partitionierung usw. zu experimentieren . Es ist groß genug, um interessant zu sein, aber lesbar genug, um die Antworten zu erkennen. Die kanadische Untergruppe ist praktisch für diese Art von Fragen. (Weniger Provinzen als US-Städte.)
Rick James
2

Ich wollte dies teilen, weil ich lange nach einer einfachen Möglichkeit gesucht habe, dies in einem Java-Programm zu implementieren, an dem ich arbeite. Dies gibt nicht ganz die Ausgabe, die Sie suchen, aber es ist nah. Die aufgerufene Funktion in MySQL GROUP_CONCAT()funktionierte sehr gut, um anzugeben, wie viele Ergebnisse in jeder Gruppe zurückgegeben werden sollen. Die Verwendung LIMIToder eine der anderen ausgefallenen Methoden, um dies zu versuchen, hat bei COUNTmir nicht funktioniert. Wenn Sie also bereit sind, eine modifizierte Ausgabe zu akzeptieren, ist dies eine großartige Lösung. Nehmen wir an, ich habe eine Tabelle mit dem Namen "Student" mit Studentenausweisen, Geschlecht und gpa. Nehmen wir an, ich möchte 5 gpas für jedes Geschlecht erreichen. Dann kann ich die Abfrage so schreiben

SELECT sex, SUBSTRING_INDEX(GROUP_CONCAT(cast(gpa AS char ) ORDER BY gpa desc), ',',5) 
AS subcategories FROM student GROUP BY sex;

Beachten Sie, dass der Parameter '5' angibt, wie viele Einträge in jeder Zeile verkettet werden sollen

Und die Ausgabe würde ungefähr so ​​aussehen

+--------+----------------+
| Male   | 4,4,4,4,3.9    |
| Female | 4,4,3.9,3.9,3.8|
+--------+----------------+

Sie können die ORDER BYVariable auch ändern und anders anordnen. Wenn ich also das Alter des Schülers hätte, könnte ich das 'gpa desc' durch 'age desc' ersetzen und es wird funktionieren! Sie können der Gruppe auch Variablen per Anweisung hinzufügen, um mehr Spalten in der Ausgabe zu erhalten. Dies ist nur ein Weg, den ich gefunden habe, der ziemlich flexibel ist und gut funktioniert, wenn Sie nur Ergebnisse auflisten können.

Jon Bown
quelle
0

In SQL Server row_numer()ist eine leistungsstarke Funktion, die wie folgt leicht zu Ergebnissen führen kann

select Person,[group],age
from
(
select * ,row_number() over(partition by [group] order by age desc) rn
from mytable
) t
where rn <= 2
Prakash
quelle
Mit 8,0 und 10,2 als GA wird diese Antwort vernünftig.
Rick James
@ RickJames Was bedeutet "GA sein"? Fensterfunktionen ( dev.mysql.com/doc/refman/8.0/en/window-functions.html ) haben mein Problem sehr gut gelöst.
Iedmrc
1
@iedmrc - "GA" bedeutet "Allgemein verfügbar". Es ist technisch gesehen "bereit für die Hauptsendezeit" oder "veröffentlicht". Sie sind durch die Entwicklung der Version und werden sich auf Fehler konzentrieren, die sie verpasst haben. Dieser Link beschreibt die Implementierung von MySQL 8.0, die sich möglicherweise von der Implementierung von MariaDB 10.2 unterscheidet.
Rick James
-1

Bei MySQL gibt es eine wirklich gute Antwort auf dieses Problem - So erhalten Sie die besten N Zeilen pro Gruppe

Basierend auf der Lösung im Link, auf den verwiesen wird, lautet Ihre Anfrage wie folgt:

SELECT Person, Group, Age
   FROM
     (SELECT Person, Group, Age, 
                  @group_rank := IF(@group = Group, @group_rank + 1, 1) AS group_rank,
                  @current_group := Group 
       FROM `your_table`
       ORDER BY Group, Age DESC
     ) ranked
   WHERE group_rank <= `n`
   ORDER BY Group, Age DESC;

Wo nist das top nund your_tableist der Name Ihrer Tabelle.

Ich denke, die Erklärung in der Referenz ist wirklich klar. Zum schnellen Nachschlagen werde ich es hier kopieren und einfügen:

Derzeit unterstützt MySQL keine ROW_NUMBER () -Funktion, die eine Sequenznummer innerhalb einer Gruppe zuweisen kann. Als Problemumgehung können wir jedoch MySQL-Sitzungsvariablen verwenden.

Diese Variablen erfordern keine Deklaration und können in einer Abfrage verwendet werden, um Berechnungen durchzuführen und Zwischenergebnisse zu speichern.

@current_country: = country Dieser Code wird für jede Zeile ausgeführt und speichert den Wert der Länderspalte in der Variablen @current_country.

@country_rank: = IF (@current_country = country, @country_rank + 1, 1) Wenn @current_country in diesem Code gleich ist, erhöhen wir den Rang, andernfalls setzen wir ihn auf 1. Für die erste Zeile ist @current_country NULL, also der Rang auch auf 1 gesetzt.

Für ein korrektes Ranking benötigen wir ORDER BY Land, Bevölkerung DESC

kovac
quelle
Nun, es ist das Prinzip, das von Lösungen von Marc Byers, Rick James und mir verwendet wird.
Laurent PELE
Schwer zu sagen, welcher Beitrag (Stack Overflow oder SQLlines) der erste war
Laurent PELE
@LaurentPELE - Meins wurde im Februar 2015 veröffentlicht. Ich sehe keinen Zeitstempel oder Namen in SQLlines. MySQL-Blogs gibt es schon lange genug, dass einige von ihnen veraltet sind und entfernt werden sollten - die Leute zitieren falsche Informationen.
Rick James