Abrufen von Datensätzen mit maximalem Wert für jede Gruppe gruppierter SQL-Ergebnisse

229

Wie erhalten Sie die Zeilen, die den Maximalwert für jede gruppierte Menge enthalten?

Ich habe einige übermäßig komplizierte Variationen dieser Frage gesehen und keine mit einer guten Antwort. Ich habe versucht, ein möglichst einfaches Beispiel zusammenzustellen:

Wie würden Sie bei einer solchen Tabelle mit Spalten für Personen, Gruppen und Alter die älteste Person in jeder Gruppe erhalten? (Ein Gleichstand innerhalb einer Gruppe sollte das erste alphabetische Ergebnis liefern.)

Person | Group | Age
---
Bob  | 1     | 32  
Jill | 1     | 34  
Shawn| 1     | 42  
Jake | 2     | 29  
Paul | 2     | 36  
Laura| 2     | 39  

Gewünschte Ergebnismenge:

Shawn | 1     | 42    
Laura | 2     | 39  
Yarin
quelle
3
Achtung: Die akzeptierte Antwort funktionierte 2012, als sie geschrieben wurde. Es funktioniert jedoch aus mehreren Gründen nicht mehr, wie in den Kommentaren angegeben.
Rick James

Antworten:

132

Es gibt eine supereinfache Möglichkeit, dies in MySQL zu tun:

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

Dies funktioniert, weil Sie in MySQL keine nicht gruppierten Spalten aggregieren dürfen. In diesem Fall gibt MySQL nur die erste Zeile zurück. Die Lösung besteht darin, zuerst die Daten so zu ordnen, dass für jede Gruppe zuerst die gewünschte Zeile und dann nach den Spalten gruppiert wird, für die Sie den Wert möchten.

Sie vermeiden komplizierte Unterabfragen, die versuchen, das max()usw. zu finden , und auch die Probleme, mehrere Zeilen zurückzugeben, wenn es mehr als eine mit demselben Maximalwert gibt (wie es die anderen Antworten tun würden).

Hinweis: Dies ist eine reine MySQL- Lösung. Alle anderen mir bekannten Datenbanken geben einen SQL-Syntaxfehler mit der Meldung "Nicht aggregierte Spalten werden nicht in der Gruppe nach Klausel aufgeführt" oder ähnlichem aus. Da diese Lösung undokumentiertes Verhalten verwendet, möchten die vorsichtigen möglicherweise einen Test einschließen, um zu bestätigen, dass sie weiterhin funktionieren, falls eine zukünftige Version von MySQL dieses Verhalten ändert.

Update Version 5.7:

Seit Version 5.7 enthält die sql-modeEinstellung ONLY_FULL_GROUP_BYstandardmäßig. Damit dies funktioniert, müssen Sie diese Option nicht haben (bearbeiten Sie die Optionsdatei für den Server, um diese Einstellung zu entfernen).

Böhmisch
quelle
66
"MySQL gibt nur die erste Zeile zurück." - Vielleicht funktioniert es so, aber es ist nicht garantiert. In der Dokumentation heißt es: "Der Server kann aus jeder Gruppe einen beliebigen Wert auswählen. Sofern diese nicht identisch sind, sind die ausgewählten Werte unbestimmt." . Der Server wählt keine Zeilen aus, sondern Werte (nicht unbedingt aus derselben Zeile) für jede Spalte oder jeden Ausdruck, der in der SELECTKlausel erscheint und nicht mit einer Aggregatfunktion berechnet wird.
Axiac
16
Dieses Verhalten hat sich in MySQL 5.7.5 geändert und lehnt diese Abfrage standardmäßig ab, da die Spalten in der SELECTKlausel nicht funktional von den GROUP BYSpalten abhängig sind. Wenn es so konfiguriert ist, dass es es akzeptiert (`ONLY_FULL_GROUP_BY` ist deaktiviert), funktioniert es wie in den vorherigen Versionen (dh die Werte dieser Spalten sind unbestimmt).
Axiac
17
Ich bin überrascht, dass diese Antwort so viele positive Stimmen erhalten hat. Es ist falsch und es ist schlecht. Es ist nicht garantiert, dass diese Abfrage funktioniert. Daten in einer Unterabfrage sind trotz der order by-Klausel eine ungeordnete Menge. MySQL kann die Datensätze jetzt wirklich bestellen und diese Reihenfolge beibehalten, aber es würde keine Regel brechen, wenn es in einer zukünftigen Version damit aufhören würde. Dann GROUP BYverdichtet sich das zu einem Datensatz, aber alle Felder werden willkürlich aus den Datensätzen ausgewählt. Es kann sein, dass MySQL derzeit einfach immer die erste Zeile auswählt, aber es könnte genauso gut jede andere Zeile oder sogar Werte aus verschiedenen Zeilen in einer zukünftigen Version auswählen .
Thorsten Kettner
9
Okay, wir sind uns hier nicht einig. Ich verwende keine undokumentierten Funktionen, die gerade funktionieren, und verlasse mich auf einige Tests, die dies hoffentlich abdecken. Sie wissen, dass Sie nur Glück haben, dass Sie mit der aktuellen Implementierung den vollständigen ersten Datensatz erhalten, in dem in den Dokumenten eindeutig angegeben ist, dass Sie möglicherweise stattdessen unbestimmte Werte erhalten haben, diese aber dennoch verwenden. Einige einfache Sitzungs- oder Datenbankeinstellungen können dies jederzeit ändern. Ich würde das für zu riskant halten.
Thorsten Kettner
3
Diese Antwort scheint falsch. Gemäß dem Dokument kann der Server einen beliebigen Wert aus jeder Gruppe auswählen. Darüber hinaus kann die Auswahl der Werte aus jeder Gruppe nicht durch Hinzufügen einer ORDER BY-Klausel beeinflusst werden. Die Sortierung der Ergebnismengen erfolgt nach Auswahl der Werte, und ORDER BY hat keinen Einfluss darauf, welchen Wert innerhalb jeder Gruppe der Server auswählt.
Tgr
296

Die richtige Lösung ist:

SELECT o.*
FROM `Persons` o                    # 'o' from 'oldest person in group'
  LEFT JOIN `Persons` b             # 'b' from 'bigger age'
      ON o.Group = b.Group AND o.Age < b.Age
WHERE b.Age is NULL                 # bigger age not found

Wie es funktioniert:

Es vergleicht jede Zeile omit allen Zeilen b, die denselben Wert in der Spalte Groupund einen größeren Wert in der Spalte haben Age. Jede Zeile, die onicht den Maximalwert ihrer Gruppe in der Spalte hat, Agestimmt mit einer oder mehreren Zeilen von überein b.

Das LEFT JOINmacht es die älteste Person in der Gruppe entspricht (die Personen umfasst , die allein in ihrer Gruppe sind) mit einer Reihe voller NULLs aus b( "no größten Alter in der Gruppe).
Durch INNER JOINdie Verwendung stimmen diese Zeilen nicht überein und werden ignoriert.

Die WHEREKlausel behält nur die Zeilen mit NULLs in den Feldern bei, aus denen extrahiert wurde b. Sie sind die ältesten Personen aus jeder Gruppe.

Weitere Lesungen

Diese und viele andere Lösungen werden im Buch SQL Antipatterns: Vermeiden der Fallstricke der Datenbankprogrammierung erläutert

Axiac
quelle
43
Übrigens kann dies zwei oder mehr Zeilen für dieselbe Gruppe zurückgeben, wenn o.Age = b.Agebeispielsweise Paul aus Gruppe 2 wie Laura auf 39 ist. Wenn wir jedoch kein solches Verhalten wollen, können wir Folgendes tun:ON o.Group = b.Group AND (o.Age < b.Age or (o.Age = b.Age and o.id < b.id))
Todor
8
Unglaublich! Für 20 Millionen Datensätze ist es ungefähr 50 Mal schneller als ein "naiver" Algorithmus (Join gegen eine Unterabfrage mit max ())
user2706534
3
Funktioniert perfekt mit @ Todd-Kommentaren. Ich würde hinzufügen, dass, wenn es weitere Abfragebedingungen gibt, diese im FROM und im LEFT JOIN hinzugefügt werden müssen. Etwas WIE: VON (SELECT * FROM Person WHERE Age! = 32) o LEFT JOIN (SELECT * FROM Person WHERE Age! = 32) b - wenn Sie Personen entlassen möchten, die 32 Jahre
alt
1
@AlainZelink Werden diese "weiteren Abfragebedingungen" nicht besser in die endgültige WHERE-Bedingungsliste aufgenommen, um keine Unterabfragen einzuführen - die in der ursprünglichen @ axiac-Antwort nicht benötigt wurden?
Tarilabs
5
Diese Lösung hat funktioniert; Es wurde jedoch im Protokoll für langsame Abfragen gemeldet, wenn versucht wurde, mehr als 10.000 Zeilen mit derselben ID zu verwenden. Hat sich der indizierten Spalte angeschlossen. Ein seltener Fall, aber es ist erwähnenswert.
Chaseisabelle
49

Sie können sich einer Unterabfrage anschließen, die das MAX(Group)und zieht Age. Diese Methode ist auf die meisten RDBMS portierbar.

SELECT t1.*
FROM yourTable t1
INNER JOIN
(
    SELECT `Group`, MAX(Age) AS max_age
    FROM yourTable
    GROUP BY `Group`
) t2
    ON t1.`Group` = t2.`Group` AND t1.Age = t2.max_age;
Michael Berkowski
quelle
Michael, danke dafür - aber hast du eine Antwort auf das Problem, mehrere Zeilen über Krawatten zurückzugeben, gemäß den Kommentaren von Bohemian?
Yarin
1
@Yarin Wenn es zum Beispiel 2 Zeilen Group = 2, Age = 20gäbe, würde die Unterabfrage eine davon zurückgeben, aber die Join- ONKlausel würde mit beiden übereinstimmen , sodass Sie 2 Zeilen mit derselben Gruppe / demselben Alter zurückerhalten würden, obwohl für die anderen Spalten unterschiedliche Werte gelten. eher als einer.
Michael Berkowski
Wollen wir damit sagen, dass es unmöglich ist, die Ergebnisse auf einen pro Gruppe zu beschränken, wenn wir nicht nur auf MySQL von Bohemians gehen?
Yarin
@Yarin nicht nicht unmöglich, erfordert nur mehr Arbeit, wenn zusätzliche Spalten vorhanden sind - möglicherweise eine weitere verschachtelte Unterabfrage, um die maximal zugeordnete ID für jedes ähnliche Paar von Gruppe / Alter abzurufen, und dann dagegen zu verbinden, um den Rest der Zeile basierend auf der ID zu erhalten.
Michael Berkowski
Dies sollte die akzeptierte Antwort sein (die derzeit akzeptierte Antwort schlägt bei den meisten anderen RDBMS fehl und würde sogar bei vielen Versionen von MySQL fehlschlagen).
Tim Biegeleisen
28

Meine einfache Lösung für SQLite (und wahrscheinlich MySQL):

SELECT *, MAX(age) FROM mytable GROUP BY `Group`;

Es funktioniert jedoch nicht in PostgreSQL und möglicherweise auf einigen anderen Plattformen.

In PostgreSQL können Sie die DISTINCT ON- Klausel verwenden:

SELECT DISTINCT ON ("group") * FROM "mytable" ORDER BY "group", "age" DESC;
Igor Kulagin
quelle
@Bohemian Entschuldigung, ich weiß, dies ist nur MySQL, da es nicht aggregierte Spalten enthält
Cec
2
@IgorKulagin - Funktioniert nicht in Postgres- Fehlermeldung: Spalte "mytable.id" muss in der GROUP BY-Klausel erscheinen oder in einer Aggregatfunktion verwendet werden
Yarin
13
Die MySQL-Abfrage funktioniert möglicherweise nur gelegentlich versehentlich. Das "SELECT *" kann Informationen zurückgeben, die nicht dem zugehörigen MAX (Alter) entsprechen. Diese Antwort ist falsch. Dies ist wahrscheinlich auch bei SQLite der Fall.
Albert Hendriks
2
Dies passt jedoch zu dem Fall, in dem wir die gruppierte Spalte und die maximale Spalte auswählen müssen. Dies entspricht nicht der oben genannten Anforderung, wo es zu Ergebnissen führen würde ('Bob', 1, 42), aber das erwartete Ergebnis ist ('Shawn', 1, 42)
Ram Babu S
1
Gut für Postgres
Karol Gasienica
4

Ranking-Methode verwenden.

SELECT @rn :=  CASE WHEN @prev_grp <> groupa THEN 1 ELSE @rn+1 END AS rn,  
   @prev_grp :=groupa,
   person,age,groupa  
FROM   users,(SELECT @rn := 0) r        
HAVING rn=1
ORDER  BY groupa,age DESC,person
sel
quelle
sel - brauche eine Erklärung - ich habe noch nie gesehen :=- was ist das?
Yarin
1
: = ist Zuweisungsoperator. Sie können mehr auf dev.mysql.com/doc/refman/5.0/en/user-variables.html
sel
Ich muss mich damit befassen - ich denke, die Antwort macht unser Szenario zu kompliziert, aber danke, dass Sie mir etwas Neues beigebracht haben.
Yarin
3

Ich bin mir nicht sicher, ob MySQL die Funktion row_number hat. In diesem Fall können Sie das gewünschte Ergebnis erzielen. Unter SQL Server können Sie Folgendes tun:

CREATE TABLE p
(
 person NVARCHAR(10),
 gp INT,
 age INT
);
GO
INSERT  INTO p
VALUES  ('Bob', 1, 32);
INSERT  INTO p
VALUES  ('Jill', 1, 34);
INSERT  INTO p
VALUES  ('Shawn', 1, 42);
INSERT  INTO p
VALUES  ('Jake', 2, 29);
INSERT  INTO p
VALUES  ('Paul', 2, 36);
INSERT  INTO p
VALUES  ('Laura', 2, 39);
GO

SELECT  t.person, t.gp, t.age
FROM    (
         SELECT *,
                ROW_NUMBER() OVER (PARTITION BY gp ORDER BY age DESC) row
         FROM   p
        ) t
WHERE   t.row = 1;
user130268
quelle
1
Seit 8.0.
Ilja Everilä
2

Die Lösung von axiac hat am Ende für mich am besten funktioniert. Ich hatte jedoch eine zusätzliche Komplexität: einen berechneten "Maximalwert", abgeleitet aus zwei Spalten.

Verwenden wir das gleiche Beispiel: Ich möchte die älteste Person in jeder Gruppe. Wenn es Menschen gibt, die gleich alt sind, nehmen Sie die größte Person.

Ich musste den linken Join zweimal ausführen, um dieses Verhalten zu erhalten:

SELECT o1.* WHERE
    (SELECT o.*
    FROM `Persons` o
    LEFT JOIN `Persons` b
    ON o.Group = b.Group AND o.Age < b.Age
    WHERE b.Age is NULL) o1
LEFT JOIN
    (SELECT o.*
    FROM `Persons` o
    LEFT JOIN `Persons` b
    ON o.Group = b.Group AND o.Age < b.Age
    WHERE b.Age is NULL) o2
ON o1.Group = o2.Group AND o1.Height < o2.Height 
WHERE o2.Height is NULL;

Hoffe das hilft! Ich denke, es sollte einen besseren Weg geben, dies zu tun ...

Arthur C.
quelle
2

Meine Lösung funktioniert nur, wenn Sie nur eine Spalte abrufen müssen. Für meine Anforderungen wurde jedoch die beste Lösung in Bezug auf die Leistung gefunden (es wird nur eine einzige Abfrage verwendet!):

SELECT SUBSTRING_INDEX(GROUP_CONCAT(column_x ORDER BY column_y),',',1) AS xyz,
   column_z
FROM table_name
GROUP BY column_z;

Es wird GROUP_CONCAT verwendet, um eine geordnete Concat-Liste zu erstellen, und dann wird nur die erste Teilzeichenfolge erstellt.

Antonio Giovanazzi
quelle
Kann bestätigen, dass Sie mehrere Spalten erhalten können, indem Sie innerhalb von group_concat nach demselben Schlüssel sortieren, müssen jedoch für jede Spalte einen separaten group_concat / index / substring schreiben.
Rasika
Der Vorteil hierbei ist, dass Sie der Sortierung in group_concat mehrere Spalten hinzufügen können. Dadurch werden die Verknüpfungen leicht aufgelöst und es wird nur ein Datensatz pro Gruppe garantiert. Gut gemacht mit der einfachen und effizienten Lösung!
Rasika
2

Ich habe eine einfache Lösung mit WHERE IN

SELECT a.* FROM `mytable` AS a    
WHERE a.age IN( SELECT MAX(b.age) AS age FROM `mytable` AS b GROUP BY b.group )    
ORDER BY a.group ASC, a.person ASC
Khalid Musa Sagar
quelle
1

Verwenden von CTEs - Allgemeine Tabellenausdrücke:

WITH MyCTE(MaxPKID, SomeColumn1)
AS(
SELECT MAX(a.MyTablePKID) AS MaxPKID, a.SomeColumn1
FROM MyTable1 a
GROUP BY a.SomeColumn1
  )
SELECT b.MyTablePKID, b.SomeColumn1, b.SomeColumn2 MAX(b.NumEstado)
FROM MyTable1 b
INNER JOIN MyCTE c ON c.MaxPKID = b.MyTablePKID
GROUP BY b.MyTablePKID, b.SomeColumn1, b.SomeColumn2

--Note: MyTablePKID is the PrimaryKey of MyTable
Marvin
quelle
1

In Oracle unten kann die Abfrage das gewünschte Ergebnis liefern.

SELECT group,person,Age,
  ROWNUMBER() OVER (PARTITION BY group ORDER BY age desc ,person asc) as rankForEachGroup
  FROM tablename where rankForEachGroup=1
kiruba
quelle
0
with CTE as 
(select Person, 
[Group], Age, RN= Row_Number() 
over(partition by [Group] 
order by Age desc) 
from yourtable)`


`select Person, Age from CTE where RN = 1`
Harshad
quelle
0

Sie können es auch versuchen

SELECT * FROM mytable WHERE age IN (SELECT MAX(age) FROM mytable GROUP BY `Group`) ;
Ritwik
quelle
1
Vielen Dank, obwohl dies mehrere Rekorde für ein Alter zurückgibt, in dem es ein Unentschieden gibt
Yarin
Diese Abfrage wäre auch falsch, wenn sich in Gruppe 1 ein 39-Jähriger befindet. In diesem Fall würde auch diese Person ausgewählt, obwohl das Höchstalter in Gruppe 1 höher ist.
Joshua Richardson
0

Ich würde Group nicht als Spaltennamen verwenden, da es sich um ein reserviertes Wort handelt. Das folgende SQL würde jedoch funktionieren.

SELECT a.Person, a.Group, a.Age FROM [TABLE_NAME] a
INNER JOIN 
(
  SELECT `Group`, MAX(Age) AS oldest FROM [TABLE_NAME] 
  GROUP BY `Group`
) b ON a.Group = b.Group AND a.Age = b.oldest
Bae Cheol Shin
quelle
Vielen Dank, obwohl dies mehrere Rekorde für ein Alter zurückgibt, in dem es ein Unentschieden gibt
Yarin
@ Yarin wie würde entscheiden, welche die richtige älteste Person ist? Mehrfachantworten scheinen die richtigste Antwort zu sein, andernfalls verwenden Sie Limit und Reihenfolge
Duncan
0

Diese Methode bietet den Vorteil, dass Sie nach einer anderen Spalte sortieren und die anderen Daten nicht in den Papierkorb werfen können. Dies ist sehr nützlich in Situationen, in denen Sie versuchen, Bestellungen mit einer Spalte für Artikel aufzulisten, wobei die schwersten zuerst aufgelistet werden.

Quelle: http://dev.mysql.com/doc/refman/5.0/de/group-by-functions.html#function_group-concat

SELECT person, group,
    GROUP_CONCAT(
        DISTINCT age
        ORDER BY age DESC SEPARATOR ', follow up: '
    )
FROM sql_table
GROUP BY group;
Ray Foss
quelle
0

Lass den Tabellennamen Menschen sein

select O.*              -- > O for oldest table
from people O , people T
where O.grp = T.grp and 
O.Age = 
(select max(T.age) from people T where O.grp = T.grp
  group by T.grp)
group by O.grp; 
user3475425
quelle
0

Wenn ID (und alle Coulmns) von mytable benötigt wird

SELECT
    *
FROM
    mytable
WHERE
    id NOT IN (
        SELECT
            A.id
        FROM
            mytable AS A
        JOIN mytable AS B ON A. GROUP = B. GROUP
        AND A.age < B.age
    )
Mayank Kumar
quelle
0

Auf diese Weise erhalte ich die N max Zeilen pro Gruppe in MySQL

SELECT co.id, co.person, co.country
FROM person co
WHERE (
SELECT COUNT(*)
FROM person ci
WHERE  co.country = ci.country AND co.id < ci.id
) < 1
;

wie es funktioniert:

  • selbst mit dem Tisch verbinden
  • Gruppen werden von gemacht co.country = ci.country
  • N Elemente pro Gruppe werden ) < 1so für 3 Elemente gesteuert -) <3
  • Max oder Min zu bekommen hängt ab von: co.id < ci.id
    • co.id <ci.id - max
    • co.id> ci.id - min

Vollständiges Beispiel hier:

mysql wähle n max Werte pro Gruppe

Vanko
quelle