muss in der GROUP BY-Klausel erscheinen oder in einer Aggregatfunktion verwendet werden

276

Ich habe einen Tisch, der aussieht wie dieser Anrufer 'Makerar'

 cname  | wmname |          avg           
--------+-------------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | luffy  | 1.00000000000000000000
 spain  | usopp  |     5.0000000000000000

Und ich möchte den maximalen Durchschnitt für jeden c-Namen auswählen.

SELECT cname, wmname, MAX(avg)  FROM makerar GROUP BY cname;

aber ich werde einen Fehler bekommen,

ERROR:  column "makerar.wmname" must appear in the GROUP BY clause or be used in an   aggregate function 
LINE 1: SELECT cname, wmname, MAX(avg)  FROM makerar GROUP BY cname;

Also mache ich das

SELECT cname, wmname, MAX(avg)  FROM makerar GROUP BY cname, wmname;

Dies führt jedoch nicht zu den beabsichtigten Ergebnissen, und die folgende falsche Ausgabe wird angezeigt.

 cname  | wmname |          max           
--------+--------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | luffy  | 1.00000000000000000000
 spain  | usopp  |     5.0000000000000000

Tatsächliche Ergebnisse sollten sein

 cname  | wmname |          max           
--------+--------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | usopp  |     5.0000000000000000

Wie kann ich dieses Problem beheben?

Hinweis: Diese Tabelle ist eine ANSICHT, die aus einer vorherigen Operation erstellt wurde.

Zufälliger Typ
quelle
2
Siehe auch
Craig Ringer
Ich verstehe nicht. Warum wird wmname="usopp"erwartet und nicht zum Beispiel wmname="luffy"?
AndreKR

Antworten:

226

Ja, dies ist ein häufiges Aggregationsproblem. Vor SQL3 (1999) müssen die ausgewählten Felder in der GROUP BYKlausel [*] erscheinen.

Um dieses Problem zu umgehen, müssen Sie das Aggregat in einer Unterabfrage berechnen und dann mit sich selbst verknüpfen, um die zusätzlichen Spalten zu erhalten, die Sie anzeigen müssen:

SELECT m.cname, m.wmname, t.mx
FROM (
    SELECT cname, MAX(avg) AS mx
    FROM makerar
    GROUP BY cname
    ) t JOIN makerar m ON m.cname = t.cname AND t.mx = m.avg
;

 cname  | wmname |          mx           
--------+--------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | usopp  |     5.0000000000000000

Sie können aber auch Fensterfunktionen verwenden, die einfacher aussehen:

SELECT cname, wmname, MAX(avg) OVER (PARTITION BY cname) AS mx
FROM makerar
;

Das einzige, was mit dieser Methode gemacht wird, ist, dass alle Datensätze angezeigt werden (Fensterfunktionen gruppieren sich nicht). Aber es wird das Richtige angezeigt (dh auf cnameStufe maximal )MAX in jeder Zeile für das Land , also liegt es an Ihnen:

 cname  | wmname |          mx           
--------+--------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | luffy  |     5.0000000000000000
 spain  | usopp  |     5.0000000000000000

Die wohl weniger elegante Lösung, um die einzigen (cname, wmname)Tupel anzuzeigen, die dem Maximalwert entsprechen, lautet:

SELECT DISTINCT /* distinct here matters, because maybe there are various tuples for the same max value */
    m.cname, m.wmname, t.avg AS mx
FROM (
    SELECT cname, wmname, avg, ROW_NUMBER() OVER (PARTITION BY avg DESC) AS rn 
    FROM makerar
) t JOIN makerar m ON m.cname = t.cname AND m.wmname = t.wmname AND t.rn = 1
;


 cname  | wmname |          mx           
--------+--------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | usopp  |     5.0000000000000000

[*]: Interessanterweise scheinen große Engines es nicht wirklich zu mögen, obwohl die Spezifikation die Auswahl nicht gruppierter Felder erlaubt. Oracle und SQLServer lassen dies überhaupt nicht zu. Früher erlaubte MySQL dies standardmäßig, aber seit 5.7 muss der Administrator diese Option ( ONLY_FULL_GROUP_BY) manuell in der Serverkonfiguration aktivieren, damit diese Funktion unterstützt wird ...

Sebas
quelle
1
Dank Syntax ist richtig, aber Sie müssen Werte von mx und
avg
1
Ja, Ihre Syntax ist korrekt und eliminiert Duplikate. Sie benötigen jedoch am Ende m.avg = t.mx (nachdem Sie JOING geschrieben haben), um die beabsichtigten Ergebnisse zu erhalten
RandomGuy
1
@Sebas Es kann gemacht werden, ohne sich MAXanzumelden (siehe Antwort von @ypercube, es gibt auch eine andere Lösung in meiner Antwort), aber nicht so, wie Sie es tun. Überprüfen Sie die erwartete Ausgabe.
Null323
1
@Sebas Ihre Lösung fügt nur eine Spalte hinzu (MAX avgper cname), schränkt jedoch die Zeilen des Ergebnisses nicht ein (wie vom OP gewünscht). Siehe die tatsächlichen Ergebnisse sollte Absatz in der Frage sein.
Ypercubeᵀᴹ
1
Turning off ONLY_FULL_GROUP_BY in MySQL 5.7 nicht die Art , wie die SQL - Standard legt fest , zu aktivieren , wenn Spalten aus der weggelassen werden kann group by(oder macht MySQL verhalten sich wie Postgres). Es wird nur auf das alte Verhalten zurückgegriffen, bei dem MySQL stattdessen zufällige (= "unbestimmte") Ergebnisse zurückgibt.
a_horse_with_no_name
126

In Postgres können Sie auch die spezielle DISTINCT ON (expression)Syntax verwenden:

SELECT DISTINCT ON (cname) 
    cname, wmname, avg
FROM 
    makerar 
ORDER BY 
    cname, avg DESC ;
ypercubeᵀᴹ
quelle
5
Es wird nicht wie erwartet funktionieren, wenn man Spalten wie avg
amenzhinsky
@amenzhinsky Was meinst du? Wenn man die Ergebnismenge in einer anderen Reihenfolge sortieren lassen möchte als BY cname?
Ypercubeᵀᴹ
@ypercube, Eigentlich sortiert psql zuerst und wendet dann DISTINCT an. Im Falle einer Sortierung nach Durchschnitt erhalten wir je nach Sortierrichtung unterschiedliche Ergebnisse für jede Zeile mit minimalen und maximalen Werten
amenzhinsky
3
Natürlich. Wenn Sie die von mir gepostete Abfrage nicht ausführen, erhalten Sie unterschiedliche Ergebnisse! Das ist nicht dasselbe wie "es wird nicht wie erwartet funktionieren" ...
ypercubeᵀᴹ
1
@ Batfan Danke. Beachten Sie, dass dies zwar ziemlich cool, kompakt und einfach zu schreiben ist, aber für diese Art von Abfragen oft nicht die effizienteste Methode ist.
Ypercubeᵀᴹ
27

Das Problem bei der Angabe nicht gruppierter und nicht aggregierter Felder in group byAuswahlen besteht darin, dass die Engine nicht wissen kann, welches Datensatzfeld in diesem Fall zurückgegeben werden soll. Ist es zuerst? Ist es das letzte? Es gibt normalerweise keine Aufzeichnung, die natürlich dem aggregierten Ergebnis entspricht ( minundmax Ausnahmen sind).

Es gibt jedoch eine Problemumgehung: Machen Sie das erforderliche Feld ebenfalls aggregiert. In posgres sollte dies funktionieren:

SELECT cname, (array_agg(wmname ORDER BY avg DESC))[1], MAX(avg)
FROM makerar GROUP BY cname;

Beachten Sie, dass dadurch ein Array aller nach Durchschnitt geordneten Namen erstellt wird und das erste Element zurückgegeben wird (Arrays in Postgres basieren auf 1).

e-neko
quelle
Guter Punkt. Es scheint jedoch möglich, dass die Datenbank einen äußeren Join ausführt, um die nicht aggregierten Felder aus jeder Zeile mit dem aggregierten Ergebnis zu verknüpfen, zu dem die Zeile beigetragen hat. Ich war oft neugierig, warum sie dafür keine Option haben. Obwohl ich diese Option einfach nicht kennen könnte :)
Ben Simmons
16
SELECT t1.cname, t1.wmname, t2.max
FROM makerar t1 JOIN (
    SELECT cname, MAX(avg) max
    FROM makerar
    GROUP BY cname ) t2
ON t1.cname = t2.cname AND t1.avg = t2.max;

Mit rank() Fensterfunktion :

SELECT cname, wmname, avg
FROM (
    SELECT cname, wmname, avg, rank() 
    OVER (PARTITION BY cname ORDER BY avg DESC)
    FROM makerar) t
WHERE rank = 1;

Hinweis

In beiden Fällen werden mehrere Maximalwerte pro Gruppe beibehalten. Wenn Sie nur einen Datensatz pro Gruppe möchten, auch wenn es mehr als einen Datensatz mit einem Durchschnitt von max gibt, sollten Sie die Antwort von @ ypercube überprüfen.

null323
quelle
16

Für mich geht es nicht um ein "allgemeines Aggregationsproblem", sondern nur um eine falsche SQL-Abfrage. Die einzige richtige Antwort für "Wählen Sie den maximalen Durchschnitt für jeden C-Namen ..." lautet

SELECT cname, MAX(avg) FROM makerar GROUP BY cname;

Das Ergebnis wird sein:

 cname  |      MAX(avg)
--------+---------------------
 canada | 2.0000000000000000
 spain  | 5.0000000000000000

Dieses Ergebnis beantwortet im Allgemeinen die Frage "Was ist das beste Ergebnis für jede Gruppe?" . Wir sehen, dass das beste Ergebnis für Spanien 5 und für Kanada das beste Ergebnis 2 ist. Es ist wahr und es gibt keinen Fehler. Wenn wir auch wmname anzeigen müssen , müssen wir die Frage beantworten: "Was ist die REGEL , um wmname aus der resultierenden Menge auszuwählen?" Lassen Sie uns die Eingabedaten ein wenig ändern, um den Fehler zu klären:

  cname | wmname |        avg           
--------+--------+-----------------------
 spain  | zoro   |  1.0000000000000000
 spain  | luffy  |  5.0000000000000000
 spain  | usopp  |  5.0000000000000000

Welches Ergebnis erwarten Sie beim Ausführen dieser Abfrage : SELECT cname, wmname, MAX(avg) FROM makerar GROUP BY cname;? Sollte es sein spain+luffyoder spain+usopp? Warum? In der Abfrage wird nicht festgelegt, wie "besserer" WM-Name ausgewählt werden soll wenn mehrere geeignet sind. Daher wird das Ergebnis auch nicht ermittelt. Aus diesem Grund gibt der SQL-Interpreter einen Fehler zurück - die Abfrage ist nicht korrekt.

Mit anderen Worten, es gibt keine richtige Antwort auf die Frage "Wer ist der Beste in der spainGruppe?" . Ruffy ist nicht besser als Lysop, weil Lysop die gleiche "Punktzahl" hat.

ox160d05d
quelle
Diese Lösung hat auch bei mir funktioniert. Ich hatte Abfrageprobleme, weil mein ORM auch den zugehörigen Primärschlüssel enthielt, was zu der folgenden falschen Abfrage führte : SELECT cname, id, MAX(avg) FROM makerar GROUP BY cname;, die diesen irreführenden Fehler verursachte.
Roberto
1

Dies scheint auch zu funktionieren

SELECT *
FROM makerar m1
WHERE m1.avg = (SELECT MAX(avg)
                FROM makerar m2
                WHERE m1.cname = m2.cname
               )
daintym0sh
quelle
0

Ich bin kürzlich auf dieses Problem gestoßen, als ich versucht habe, mit zu zählen case when, und habe festgestellt, dass das Ändern der Reihenfolge der Anweisungen whichund countdas Problem behebt:

SELECT date(dateday) as pick_day,
COUNT(CASE WHEN (apples = 'TRUE' OR oranges 'TRUE') THEN fruit END)  AS fruit_counter

FROM pickings

GROUP BY 1

Anstatt zu verwenden - in letzterem, wo ich Fehler bekam, dass Äpfel und Orangen in aggregierten Funktionen erscheinen sollten

CASE WHEN ((apples = 'TRUE' OR oranges 'TRUE') THEN COUNT(*) END) END AS fruit_counter
Rachel Windzberg
quelle
1
Die whichAussage?
Hillary Sanders