musi pojawić się w klauzuli GROUP BY lub być użyty w funkcji agregującej

276

Mam stolik, który wygląda jak ten wywołujący „makerar”

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

I chcę wybrać maksymalną średnią dla każdej nazwy.

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

ale dostanę błąd,

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;

więc to robię

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

nie przyniesie to jednak zamierzonych rezultatów i pokazano niepoprawne dane wyjściowe poniżej.

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

Rzeczywiste wyniki powinny być

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

Jak mogę rozwiązać ten problem?

Uwaga: Ta tabela to WIDOK utworzony z poprzedniej operacji.

Losowa osoba
źródło
2
Powiązane: stackoverflow.com/q/18061285/398670
Craig Ringer
Nie rozumiem. Dlaczego jest wmname="usopp"oczekiwany, a nie na przykład wmname="luffy"?
AndreKR

Odpowiedzi:

226

Tak, jest to częsty problem z agregacją. Przed SQL3 (1999) wybrane pola muszą pojawić się w GROUP BYklauzuli [*].

Aby obejść ten problem, musisz obliczyć agregację w zapytaniu podrzędnym, a następnie połączyć ją ze sobą, aby uzyskać dodatkowe kolumny, które musisz pokazać:

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

Ale możesz także użyć funkcji okna, która wygląda na prostszą:

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

Jedyne w tej metodzie jest to, że pokaże wszystkie rekordy (funkcje okien nie grupują się). Ale pokaże poprawne (tj. Maksymalne na cnamepoziomie) MAXdla kraju w każdym rzędzie, więc to zależy od ciebie:

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

Rozwiązaniem, prawdopodobnie mniej eleganckim, aby pokazać jedyne (cname, wmname)krotki pasujące do wartości maksymalnej, jest:

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

[*]: Co ciekawe, chociaż specyfikacja pozwala na wybranie niezgrupowanych pól, wydaje się, że głównym silnikom się to nie podoba. Oracle i SQLServer po prostu w ogóle na to nie pozwalają. Mysql domyślnie pozwalał na to, ale teraz od 5.7 administrator musi włączyć tę opcję ( ONLY_FULL_GROUP_BY) ręcznie w konfiguracji serwera, aby ta funkcja była obsługiwana ...

Sebas
źródło
1
Dzięki składni jest corect, ale musisz dołączyć wartości mx i avg podczas dołączania
RandomGuy
1
Tak, twoja składnia jest poprawna i eliminuje duplikaty, jednak w końcu potrzebujesz m.avg = t.mx (po napisaniu JOING), aby uzyskać zamierzone wyniki
RandomGuy
1
@Sebas Można tego dokonać bez dołączania MAX(patrz odpowiedź @ypercube, w mojej odpowiedzi jest też inne rozwiązanie), ale nie w sposób, w jaki to robisz. Sprawdź oczekiwany wynik.
zero323
1
@Sebas Twoje rozwiązanie dodaje tylko kolumnę (MAX avgna cname), ale nie ogranicza wierszy wyniku (jak chce OP). Zobacz Rzeczywiste wyniki powinny być akapitem w pytaniu.
ypercubeᵀᴹ
1
Obracając się ONLY_FULL_GROUP_BY w MySQL 5.7 nie aktywuje drogę SQL standard określa, kiedy kolumny mogą być pominięte w group by(albo zachowują się jak czyni MySQL PostgreSQL). Po prostu przywraca stare zachowanie, w którym MySQL zwraca losowe (= „nieokreślone”) wyniki.
a_horse_w_no_name
126

W Postgres możesz także użyć specjalnej DISTINCT ON (expression)składni:

SELECT DISTINCT ON (cname) 
    cname, wmname, avg
FROM 
    makerar 
ORDER BY 
    cname, avg DESC ;
ypercubeᵀᴹ
źródło
5
Nie zadziała tak, jak się spodziewano, jeśli chcemy posortować kolumny takie jak avg
amenzhinsky,
@amenzhinsky Co masz na myśli? Jeśli ktoś chce posortować zestaw wyników w innej kolejności niż BY cname?
ypercubeᵀᴹ
@ypercube, Właściwie psql najpierw sortuje, a następnie stosuje DISTINCT. W przypadku sortowania według średniej otrzymamy różne wyniki dla każdego wiersza minimalne i maksymalne wartości w zależności od kierunku sortowania
amenzhinsky
3
Oczywiście. Jeśli nie uruchomisz wysłanego przeze mnie zapytania, otrzymasz inne wyniki! To nie to samo, co „nie będzie działać zgodnie z oczekiwaniami” ...
ypercubeᵀᴹ
1
@Batfan thnx. Pamiętaj, że chociaż jest to dość fajne, kompaktowe i łatwe do napisania, nie jest to często najbardziej wydajny sposób na tego rodzaju zapytania.
ypercubeᵀᴹ
27

Problem z określaniem niezgrupowanych i niezagregowanych pól w group byselekcjach polega na tym, że silnik nie ma możliwości sprawdzenia, które pole rekordu powinno w tym przypadku zwrócić. Czy to pierwszy? Czy to jest ostatnie Zwykle nie ma zapisu, który naturalnie odpowiadałby wynikowi zagregowanemu ( mini maxsą wyjątkami).

Istnieje jednak obejście: należy również agregować wymagane pole. W posgresie powinno to działać:

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

Zauważ, że to tworzy tablicę wszystkich wnamesów, uporządkowaną według avg, i zwraca pierwszy element (tablice w postgresie są oparte na 1).

e-neko
źródło
Słuszna uwaga. Chociaż wydaje się możliwe, że baza danych może wykonać zewnętrzne połączenie, aby połączyć pola nieskumulowane z każdego wiersza z zagregowanym wynikiem, do którego przyczynił się wiersz. Często byłem ciekawy, dlaczego nie mają na to opcji. Choć mógłbym po prostu nie wiedzieć o tej opcji :)
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;

Korzystanie z rank() funkcji okna :

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

Uwaga

Każda z nich zachowa wiele maksymalnych wartości na grupę. Jeśli chcesz mieć tylko jeden rekord na grupę, nawet jeśli jest więcej niż jeden rekord o średniej równej maksimum, powinieneś sprawdzić odpowiedź @ ypercube.

zero323
źródło
16

Dla mnie nie chodzi o „powszechny problem agregacji”, ale o niepoprawne zapytanie SQL. Jedną poprawną odpowiedzią dla „wybierz maksymalną średnią dla każdej nazwy ...” jest

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

Wynik będzie:

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

Ten wynik ogólnie odpowiada na pytanie „Jaki jest najlepszy wynik dla każdej grupy?” . Widzimy, że najlepszy wynik dla Hiszpanii to 5, a dla Kanady najlepszy wynik to 2. To prawda i nie ma błędu. Jeśli musimy również wyświetlić nazwę wmname , musimy odpowiedzieć na pytanie: „Jaka jest ZASADA wyboru wmname z wynikowego zestawu?” Zmieńmy trochę dane wejściowe, aby wyjaśnić błąd:

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

Które wynikają można się spodziewać na runnig to zapytanie: SELECT cname, wmname, MAX(avg) FROM makerar GROUP BY cname;? Powinien to być spain+luffylub spain+usopp? Czemu? W zapytaniu nie jest określone, jak wybrać „lepszą” nazwę wmname, jeśli kilka jest odpowiednich, więc wynik również nie jest określony. Dlatego interpreter SQL zwraca błąd - zapytanie jest nieprawidłowe.

Innymi słowy, nie ma poprawnej odpowiedzi na pytanie „Kto jest najlepszy w spaingrupie?” . Luffy nie jest lepszy od usopp, ponieważ usopp ma ten sam „wynik”.

ox160d05d
źródło
To rozwiązanie również działało dla mnie. Miałem problemy z zapytaniami, ponieważ moja ORM zawierała również powiązany klucz podstawowy, co spowodowało następujące nieprawidłowe zapytanie:, SELECT cname, id, MAX(avg) FROM makerar GROUP BY cname;które spowodowało ten błąd wprowadzający w błąd.
Roberto
1

To też wydaje się działać

SELECT *
FROM makerar m1
WHERE m1.avg = (SELECT MAX(avg)
                FROM makerar m2
                WHERE m1.cname = m2.cname
               )
daintym0sh
źródło
0

Niedawno napotkałem ten problem, gdy próbowałem liczyć użycie case when, i stwierdziłem, że zmiana kolejności instrukcji whichi countrozwiązuje problem:

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

Zamiast używać - w tym ostatnim, gdzie dostałem błędy, że jabłka i pomarańcze powinny pojawiać się w funkcjach agregujących

CASE WHEN ((apples = 'TRUE' OR oranges 'TRUE') THEN COUNT(*) END) END AS fruit_counter
Rachel Windzberg
źródło
1
whichStwierdzenie?
Hillary Sanders