deve apparire nella clausola GROUP BY o essere utilizzato in una funzione aggregata


276

Ho un tavolo che assomiglia a questo creatore "makerar"

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

E voglio selezionare la media massima per ogni cname.

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

ma avrò un errore,

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;

quindi faccio questo

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

tuttavia ciò non darà i risultati intenzionali e verrà mostrato l'output errato di seguito.

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

I risultati effettivi dovrebbero essere

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

Come posso risolvere il problema?

Nota: questa tabella è una VISTA creata da un'operazione precedente.



Non capisco. Perché è wmname="usopp"previsto e non per esempio wmname="luffy"?
AndreKR

Risposte:


226

Sì, questo è un problema di aggregazione comune. Prima di SQL3 (1999) , i campi selezionati devono apparire nella GROUP BYclausola [*].

Per ovviare a questo problema, è necessario calcolare l'aggregato in una sottoquery e quindi unirlo con se stesso per ottenere le colonne aggiuntive che è necessario mostrare:

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

Ma puoi anche usare le funzioni della finestra, che sembrano più semplici:

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

L'unica cosa con questo metodo è che mostrerà tutti i record (le funzioni della finestra non si raggruppano). Ma mostrerà il corretto (cioè massimo a cnamelivello) MAXper il paese in ogni riga, quindi dipende da te:

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

La soluzione, probabilmente meno elegante, per mostrare le uniche (cname, wmname)tuple corrispondenti al valore massimo, è:

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

[*]: Abbastanza interessante, anche se il tipo di specifica consente di selezionare campi non raggruppati, i motori principali sembrano non apprezzarlo davvero. Oracle e SQLServer non lo consentono affatto. Mysql lo usava per impostazione predefinita, ma ora dal 5.7 l'amministratore deve abilitare questa opzione ( ONLY_FULL_GROUP_BY) manualmente nella configurazione del server per supportare questa funzione ...


1
Grazie sintassi è corretto, ma è necessario confrontare i valori di mx e avg quando ci si unisce
RandomGuy

1
Sì, la sintassi è corretta ed elimina i duplicati, tuttavia alla fine è necessario m.avg = t.mx (dopo aver scritto JOING) per ottenere i risultati previsti
RandomGuy

1
@Sebas Può essere fatto senza aderire MAX(vedi risposta di @ypercube, c'è anche un'altra soluzione nella mia risposta) ma non nel modo in cui lo fai. Controlla l'output previsto.
zero323,

1
@Sebas La tua soluzione aggiunge solo una colonna (il MAX avgper cname) ma non limita le righe del risultato (come vuole l'OP). Vedere i risultati effettivi dovrebbe essere il paragrafo nella domanda.
ypercubeᵀᴹ

1
La disattivazione ONLY_FULL_GROUP_BY in MySQL 5.7 non attiva il modo in cui lo standard SQL specifica quando le colonne possono essere omesse dal group by(o fa sì che MySQL si comporti come Postgres). Torna semplicemente al vecchio comportamento in cui MySQL restituisce invece risultati casuali (= "indeterminati").
a_horse_with_no_name

126

In Postgres puoi anche usare la DISTINCT ON (expression)sintassi speciale :

SELECT DISTINCT ON (cname) 
    cname, wmname, avg
FROM 
    makerar 
ORDER BY 
    cname, avg DESC ;

5
Non funzionerà come previsto se si vogliono ordinare colonne come avg
amenzhinsky il

@amenzhinsky Che vuoi dire? Se si desidera che il set di risultati sia ordinato con un ordine diverso da BY cname?
ypercubeᵀᴹ

@ypercube, in realtà psql ordina prima e poi applica DISTINCT. In caso di ordinamento per avg otterremo risultati diversi per ogni riga valori minimi e massimi a seconda della direzione
dell'ordinamento

3
Ovviamente. Se non esegui la query che ho pubblicato, otterrai risultati diversi! Non è lo stesso di "non funzionerà come previsto" ...
ypercubeᵀᴹ

1
@Batfan thnx. Si noti che, sebbene sia piuttosto interessante, compatto e facile da scrivere, spesso non è il modo più efficiente per questo tipo di query.
ypercubeᵀᴹ

27

Il problema con la specifica di campi non raggruppati e non aggregati in group byselects è che il motore non ha modo di sapere quale campo del record dovrebbe restituire in questo caso. È il primo? È ultimo? Di solito non esiste alcun record che corrisponde naturalmente al risultato aggregato ( mine maxsono eccezioni).

Tuttavia, esiste una soluzione alternativa: aggregare anche il campo richiesto. In posgres, questo dovrebbe funzionare:

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

Si noti che questo crea una matrice di tutti i wname, ordinati per avg, e restituisce il primo elemento (le matrici in postgres sono basate su 1).


Buon punto. Tuttavia, sembra possibile che il DB possa eseguire un join esterno per collegare i campi non aggregati di ciascuna riga al risultato aggregato a cui la riga ha contribuito. Sono stato spesso curioso di sapere perché non hanno un'opzione per questo. Anche se potrei semplicemente ignorare questa opzione :)
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;

Utilizzando la rank() funzione finestra :

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

Nota

Uno dei due manterrà più valori massimi per gruppo. Se vuoi un solo record per gruppo anche se c'è più di un record con avg uguale a max dovresti controllare la risposta di @ ypercube.


16

Per me, non si tratta di un "problema di aggregazione comune", ma solo di una query SQL errata. La sola risposta corretta per "seleziona il massimo avg per ogni cname ..." è

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

Il risultato sarà:

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

Questo risultato in generale risponde alla domanda "Qual è il risultato migliore per ciascun gruppo?" . Vediamo che il miglior risultato per la Spagna è 5 e per il Canada il miglior risultato è 2. È vero, e non ci sono errori. Se dobbiamo visualizzare anche wmname , dobbiamo rispondere alla domanda: "Qual è la REGOLA per scegliere wmname dal set risultante?" Cambiamo un po 'i dati di input per chiarire l'errore:

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

Quale risultato ti aspetti su runnig questa query: SELECT cname, wmname, MAX(avg) FROM makerar GROUP BY cname;? Dovrebbe essere spain+luffyo spain+usopp? Perché? Nella query non è determinato come scegliere wmname "migliore" se diversi sono adatti, quindi anche il risultato non è determinato. Ecco perché l'interprete SQL restituisce un errore: la query non è corretta.

In altre parole, non esiste una risposta corretta alla domanda "Chi è il migliore nel spaingruppo?" . Rufy non è meglio di usopp, perché usopp ha lo stesso "punteggio".


Questa soluzione ha funzionato anche per me. Ho avuto problemi di query perché il mio ORM includeva anche la chiave primaria associata, risultante nella seguente query errata :, SELECT cname, id, MAX(avg) FROM makerar GROUP BY cname;che ha dato questo errore fuorviante.
Roberto

1

Anche questo sembra funzionare

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

0

Di recente ho case whenriscontrato questo problema, quando ho provato a contare usando , e ho scoperto che cambiando l'ordine delle istruzioni whiche si countrisolve il problema:

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

Invece di usare - in quest'ultimo, dove ho riscontrato errori che mele e arance dovrebbero apparire in funzioni aggregate

CASE WHEN ((apples = 'TRUE' OR oranges 'TRUE') THEN COUNT(*) END) END AS fruit_counter

1
La whichdichiarazione?
Hillary Sanders,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.