MySQL "Gruppieren nach" und "Bestellen nach"

96

Ich möchte in der Lage sein, eine Reihe von Zeilen aus einer Tabelle von E-Mails auszuwählen und sie nach dem Absender zu gruppieren. Meine Anfrage sieht folgendermaßen aus:

SELECT 
    `timestamp`, `fromEmail`, `subject`
FROM `incomingEmails` 
GROUP BY LOWER(`fromEmail`) 
ORDER BY `timestamp` DESC

Die Abfrage funktioniert fast so, wie ich es möchte - sie wählt Datensätze aus, die per E-Mail gruppiert sind. Das Problem ist, dass Betreff und Zeitstempel nicht dem neuesten Datensatz für eine bestimmte E-Mail-Adresse entsprechen.

Zum Beispiel könnte es zurückgeben:

fromEmail: john@example.com, subject: hello
fromEmail: mark@example.com, subject: welcome

Wenn die Datensätze in der Datenbank sind:

fromEmail: john@example.com, subject: hello
fromEmail: john@example.com, subject: programming question
fromEmail: mark@example.com, subject: welcome

Wenn das Thema "Programmierfrage" das aktuellste ist, wie kann ich MySQL veranlassen, diesen Datensatz beim Gruppieren der E-Mails auszuwählen?

John Kurlak
quelle

Antworten:

141

Eine einfache Lösung ist die Abfrage in eine subselect mit der ORDER - Anweisung wickeln ersten und Anwenden der GROUP BY später :

SELECT * FROM ( 
    SELECT `timestamp`, `fromEmail`, `subject`
    FROM `incomingEmails` 
    ORDER BY `timestamp` DESC
) AS tmp_table GROUP BY LOWER(`fromEmail`)

Dies ähnelt der Verwendung des Joins, sieht jedoch viel besser aus.

Die Verwendung nicht aggregierter Spalten in einem SELECT mit einer GROUP BY-Klausel ist nicht Standard. MySQL gibt im Allgemeinen die Werte der ersten gefundenen Zeile zurück und verwirft den Rest. Alle ORDER BY-Klauseln gelten nur für den zurückgegebenen Spaltenwert, nicht für die verworfenen.

WICHTIGES UPDATE Auswahl nicht aggregierter Spalten, die in der Praxis verwendet werden, auf die man sich jedoch nicht verlassen sollte. Gemäß der MySQL-Dokumentation "ist dies vor allem dann nützlich, wenn alle Werte in jeder nicht aggregierten Spalte, die nicht in GROUP BY benannt sind, für jede Gruppe gleich sind. Der Server kann aus jeder Gruppe einen beliebigen Wert auswählen . Wenn sie also nicht gleich sind, sind die Werte gleich Auserwählte sind unbestimmt . "

Ab 5.7.5 ist ONLY_FULL_GROUP_BY standardmäßig aktiviert, sodass nicht aggregierte Spalten Abfragefehler verursachen (ER_WRONG_FIELD_WITH_GROUP).

Wie @mikep weiter unten ausführt, besteht die Lösung darin, ANY_VALUE () ab 5.7 zu verwenden

Siehe http://www.cafewebmaster.com/mysql-order-sort-group https://dev.mysql.com/doc/refman/5.6/en/group-by-handling.html https: //dev.mysql .com / doc / refman / 5.7 / de / group-by-handle.html https://dev.mysql.com/doc/refman/5.7/de/miscellaneous-functions.html#function_any-value

b7kich
quelle
7
Ich habe vor ein paar Jahren die gleiche Lösung gefunden, und es ist eine großartige Lösung. ein großes Lob an b7kich. Zwei Probleme hierbei ... GROUP BY unterscheidet nicht zwischen Groß- und Kleinschreibung, sodass LOWER () nicht erforderlich ist. Zweitens scheint $ userID eine Variable direkt aus PHP zu sein. Ihr Code ist möglicherweise anfällig für SQL-Injection, wenn $ userID vom Benutzer bereitgestellt und nicht erzwungen wird eine ganze Zahl sein.
Velcrow
Das WICHTIGE UPDATE gilt auch für MariaDB: mariadb.com/kb/en/mariadb/…
Arthur Shipkowski
1
As of 5.7.5 ONLY_FULL_GROUP_BY is enabled by default, i.e. it's impossible to use non-aggregate columns.Der SQL-Modus kann zur Laufzeit ohne Administratorrechte geändert werden, sodass ONLY_FULL_GROUP_BY sehr einfach deaktiviert werden kann. Zum Beispiel : SET SESSION sql_mode = '';. Demo: db-fiddle.com/f/esww483qFQXbXzJmkHZ8VT/3
Mikep
1
Oder eine andere Alternative zum aktivierten Bypass ONLY_FULL_GROUP_BY ist die Verwendung von ANY_VALUE (). Weitere Informationen finden
Sie unter
42

Hier ist ein Ansatz:

SELECT cur.textID, cur.fromEmail, cur.subject, 
     cur.timestamp, cur.read
FROM incomingEmails cur
LEFT JOIN incomingEmails next
    on cur.fromEmail = next.fromEmail
    and cur.timestamp < next.timestamp
WHERE next.timestamp is null
and cur.toUserID = '$userID' 
ORDER BY LOWER(cur.fromEmail)

Grundsätzlich verbinden Sie die Tabelle selbst und suchen nach späteren Zeilen. In der where-Klausel geben Sie an, dass es keine späteren Zeilen geben darf. Dies gibt Ihnen nur die letzte Zeile.

Wenn mehrere E-Mails mit demselben Zeitstempel vorhanden sein können, muss diese Abfrage verfeinert werden. Wenn die E-Mail-Tabelle eine inkrementelle ID-Spalte enthält, ändern Sie den JOIN wie folgt:

LEFT JOIN incomingEmails next
    on cur.fromEmail = next.fromEmail
    and cur.id < next.id
Andomar
quelle
Sagte, das textIDsei mehrdeutig = /
John Kurlak
1
Entfernen Sie dann die Mehrdeutigkeit und stellen Sie dem Tabellennamen wie cur.textID den Präfix voran. Auch die Antwort wurde geändert.
Andomar
Dies ist die einzige Lösung, die mit Doctrine DQL möglich ist.
VisioN
Dies funktioniert nicht, wenn Sie versuchen, mehrere Spalten so gut selbst zu verbinden. IE, wenn Sie versuchen, die neueste E-Mail-Adresse und den neuesten Benutzernamen zu finden, und Sie mehrere Self-Left-Joins benötigen, um diesen Vorgang in einer einzigen Abfrage auszuführen.
Loveen Dyall
Wenn Sie mit vergangenen und zukünftigen Zeitstempeln / Daten arbeiten, müssen Sie den LEFT JOINKriterien eine weitere Bedingung hinzufügen, um die Ergebnismenge auf nicht zukünftige Daten zu beschränkenAND next.timestamp <= UNIX_TIMESTAMP()
fyrye
31

Wie bereits in einer Antwort erwähnt, ist die aktuelle Antwort falsch, da GROUP BY den Datensatz willkürlich aus dem Fenster auswählt.

Wenn man MySQL 5.6 oder MySQL 5.7 mit verwendet ONLY_FULL_GROUP_BY, lautet die richtige (deterministische) Abfrage:

SELECT incomingEmails.*
  FROM (
    SELECT fromEmail, MAX(timestamp) `timestamp`
    FROM incomingEmails
    GROUP BY fromEmail
  ) filtered_incomingEmails
  JOIN incomingEmails USING (fromEmail, timestamp)
GROUP BY fromEmail, timestamp

Damit die Abfrage effizient ausgeführt werden kann, ist eine ordnungsgemäße Indizierung erforderlich.

Beachten Sie, dass ich zur Vereinfachung das entfernt habe LOWER(), was in den meisten Fällen nicht verwendet wird.

Marcus
quelle
2
Dies sollte die richtige Antwort sein. Ich habe gerade einen Fehler auf meiner Website entdeckt, der damit zusammenhängt. Die order byin der Unterauswahl in den anderen Antworten hat überhaupt keine Auswirkung.
Jette
1
OMG, bitte machen Sie dies zur akzeptierten Antwort. Der Akzeptierte verschwendete 5 Stunden meiner Zeit :(
Richard Kersey
29

Führen Sie nach ORDER BY ein GROUP BY durch, indem Sie Ihre Abfrage wie folgt mit GROUP BY umschließen:

SELECT t.* FROM (SELECT * FROM table ORDER BY time DESC) t GROUP BY t.from
11101101b
quelle
1
Also wählt die GROUP BY` automatisch die neueste timeoder die neueste timeoder zufällige aus?
xrDDDD
1
Es wird die neueste Zeit ausgewählt, da wir nach bestellen time DESCund dann die Gruppe nach die erste (späteste).
11101101b
Wenn ich nur JOINS für Unterauswahlen in VIEWS in MySQL 5.1 ausführen könnte. Möglicherweise ist diese Funktion in einer neueren Version enthalten.
IcarusNM
21

Gemäß dem SQL-Standard können Sie keine nicht aggregierten Spalten in der Auswahlliste verwenden. MySQL erlaubt eine solche Verwendung (uless ONLY_FULL_GROUP_BY-Modus verwendet), aber das Ergebnis ist nicht vorhersehbar.

ONLY_FULL_GROUP_BY

Sie sollten zuerst E-Mail, MIN (Lesen) und dann mit der zweiten Abfrage (oder Unterabfrage) - Betreff auswählen.

noonex
quelle
MIN (Lesen) würde den Minimalwert von "Lesen" zurückgeben. Wahrscheinlich sucht er stattdessen nach der "Lese" -Flagge der neuesten E-Mail.
Andomar
2

Ich kämpfte mit diesen beiden Ansätzen um komplexere Abfragen als die gezeigten, weil der Unterabfrageansatz schrecklich unzulänglich war, egal welche Indizes ich anlegte, und weil ich die äußere Selbstverbindung nicht durch den Ruhezustand erreichen konnte

Der beste (und einfachste) Weg, dies zu tun, besteht darin, nach etwas zu gruppieren, das eine Verkettung der benötigten Felder enthält, und sie dann mithilfe von Ausdrücken in der SELECT-Klausel herauszuziehen. Wenn Sie MAX () ausführen müssen, stellen Sie sicher, dass sich das Feld, über das Sie MAX () senden möchten, immer am wichtigsten Ende der verketteten Entität befindet.

Der Schlüssel zum Verständnis ist, dass die Abfrage nur dann sinnvoll sein kann, wenn diese anderen Felder für eine Entität unveränderlich sind, die Max () erfüllt. In Bezug auf die Sortierung können die anderen Teile der Verkettung ignoriert werden. Wie das geht, erfahren Sie ganz unten in diesem Link. http://dev.mysql.com/doc/refman/5.0/en/group-by-hidden-columns.html

Wenn Sie ein Einfüge- / Aktualisierungsereignis (wie einen Auslöser) erhalten, um die Verkettung der Felder vorab zu berechnen, können Sie es indizieren, und die Abfrage ist so schnell, als ob die Gruppe nach nur über dem Feld wäre, das Sie tatsächlich MAX ( ). Sie können es sogar verwenden, um das Maximum aus mehreren Feldern zu erhalten. Ich verwende es, um Abfragen für mehrdimensionale Bäume durchzuführen, die als verschachtelte Mengen ausgedrückt werden.

Mike N.
quelle