doit apparaître dans la clause GROUP BY ou être utilisé dans une fonction d'agrégation

276

J'ai une table qui ressemble à cet appelant «makerar»

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

Et je veux sélectionner la moyenne maximale pour chaque cname.

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

mais j'obtiendrai une erreur,

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;

donc je fais ça

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

cependant, cela ne donnera pas les résultats escomptés et la sortie incorrecte ci-dessous est affichée.

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

Les résultats réels doivent être

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

Comment résoudre ce problème?

Remarque: ce tableau est une VUE créée à partir d'une opération précédente.

Type au hasard
la source
2
EN RELATION
Craig Ringer
Je ne comprends pas. Pourquoi est wmname="usopp"attendu et pas par exemple wmname="luffy"?
AndreKR

Réponses:

226

Oui, il s'agit d'un problème d'agrégation courant. Avant SQL3 (1999) , les champs sélectionnés doivent apparaître dans la GROUP BYclause [*].

Pour contourner ce problème, vous devez calculer l'agrégat dans une sous-requête, puis la joindre à elle-même pour obtenir les colonnes supplémentaires que vous devez afficher:

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

Mais vous pouvez également utiliser des fonctions de fenêtre, ce qui semble plus simple:

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

La seule chose avec cette méthode est qu'elle affichera tous les enregistrements (les fonctions de fenêtre ne se regroupent pas). Mais il affichera le correct (c'est-à-dire au maximum au cnameniveau) MAXpour le pays dans chaque ligne, donc c'est à vous de décider:

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

La solution, sans doute moins élégante, pour montrer les seuls (cname, wmname)tuples correspondant à la valeur maximale, est:

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

[*]: Chose intéressante, même si le type de spécification permet de sélectionner des champs non groupés, les principaux moteurs semblent ne pas vraiment l'aimer. Oracle et SQLServer ne permettent tout simplement pas cela. Mysql le permettait par défaut, mais depuis la version 5.7, l'administrateur doit activer cette option ( ONLY_FULL_GROUP_BY) manuellement dans la configuration du serveur pour que cette fonctionnalité soit prise en charge ...

Sebas
la source
1
Merci la syntaxe est correcte, mais, vous devez comparer les valeurs de mx et moy lors de l'adhésion
RandomGuy
1
Oui, votre syntaxe est correcte et élimine les doublons, mais vous avez finalement besoin de m.avg = t.mx (après avoir écrit JOING) pour obtenir les résultats escomptés
RandomGuy
1
@Sebas Cela peut se faire sans se joindre à MAX(voir la réponse de @ypercube, il y a aussi une autre solution dans ma réponse) mais pas de la façon dont vous le faites. Vérifiez la sortie attendue.
zero323
1
@Sebas Votre solution n'ajoute qu'une colonne (le MAX avgper cname) mais elle ne restreint pas les lignes du résultat (comme le souhaite l'OP). Voir les résultats réels devraient être le paragraphe de la question.
ypercubeᵀᴹ
1
La désactivation ONLY_FULL_GROUP_BY dans MySQL 5.7 n'active pas la façon dont le standard SQL spécifie quand les colonnes peuvent être omises de group by(ou fait en sorte que MySQL se comporte comme Postgres). Il revient simplement à l'ancien comportement où MySQL renvoie des résultats aléatoires (= "indéterminés") à la place.
a_horse_with_no_name
126

Dans Postgres, vous pouvez également utiliser la DISTINCT ON (expression)syntaxe spéciale :

SELECT DISTINCT ON (cname) 
    cname, wmname, avg
FROM 
    makerar 
ORDER BY 
    cname, avg DESC ;
ypercubeᵀᴹ
la source
5
Cela ne fonctionnera pas comme prévu si l'on veut trier des colonnes comme avg
amenzhinsky
@amenzhinsky Que voulez-vous dire? Si l'on veut que le jeu de résultats soit trié dans un ordre différent de celui BY cname?
ypercubeᵀᴹ
@ypercube, En fait psql trie d'abord puis applique DISTINCT. En cas de tri par moyenne, nous obtiendrons des résultats différents pour chaque ligne, des valeurs minimales et maximales en fonction de la direction du tri
amenzhinsky
3
Bien sûr. Si vous n'exécutez pas la requête que j'ai publiée, vous obtiendrez des résultats différents! Ce n'est pas la même chose que "cela ne fonctionnera pas comme prévu" ...
ypercubeᵀᴹ
1
@Batfan thnx. Notez que bien que ce soit assez cool, compact et facile à écrire, ce n'est pas souvent le moyen le plus efficace pour ce type de requêtes.
ypercubeᵀᴹ
27

Le problème avec la spécification de champs non groupés et non agrégés dans les group bysélections est que le moteur n'a aucun moyen de savoir quel champ d'enregistrement il doit retourner dans ce cas. C'est d'abord? C'est la dernière? Il n'y a généralement aucun enregistrement qui correspond naturellement au résultat agrégé ( minet maxsont des exceptions).

Cependant, il existe une solution de contournement: effectuez également l'agrégation du champ requis. En posgres, cela devrait fonctionner:

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

Notez que cela crée un tableau de tous les noms, classés par avg, et renvoie le premier élément (les tableaux en postgres sont basés sur 1).

e-neko
la source
Bon point. Bien qu'il semble possible que la base de données puisse effectuer une jointure externe pour lier les champs non agrégés de chaque ligne au résultat agrégé auquel la ligne a contribué. J'ai souvent été curieux de savoir pourquoi ils n'avaient pas d'option pour cela. Bien que je puisse simplement ignorer cette option :)
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;

Utilisation de la rank() fonction fenêtre :

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

Remarque

L'un ou l'autre conservera plusieurs valeurs maximales par groupe. Si vous ne voulez qu'un seul enregistrement par groupe, même s'il y a plus d'un enregistrement avec une moyenne égale à max, vous devriez vérifier la réponse de @ ypercube.

zero323
la source
16

Pour moi, il ne s'agit pas d'un "problème d'agrégation courant", mais simplement d'une requête SQL incorrecte. La seule bonne réponse pour "sélectionner la moyenne maximale pour chaque nom de domaine ..." est

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

Le résultat sera:

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

Ce résultat répond en général à la question "Quel est le meilleur résultat pour chaque groupe?" . Nous voyons que le meilleur résultat pour l'Espagne est 5 et pour le Canada le meilleur résultat est 2. C'est vrai, et il n'y a pas d'erreur. Si nous devons afficher wmname , nous devons aussi répondre à la question: « Quelle est la Règle ? À wmname choisir résultant ensemble » Modifions un peu les données d'entrée pour clarifier l'erreur:

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

Quel est le résultat attendez-vous runnig cette requête: SELECT cname, wmname, MAX(avg) FROM makerar GROUP BY cname;? Devrait-il être spain+luffyou spain+usopp? Pourquoi? Il n'est pas déterminé dans la requête comment choisir "mieux" wmname si plusieurs conviennent, donc le résultat n'est pas non plus déterminé. C'est pourquoi l'interpréteur SQL renvoie une erreur - la requête n'est pas correcte.

En d'autres termes, il n'y a pas de bonne réponse à la question "Qui est le meilleur du spaingroupe?" . Luffy n'est pas meilleur qu'usopp, car usopp a le même "score".

ox160d05d
la source
Cette solution a également fonctionné pour moi. J'ai eu des problèmes de requête car mon ORM comprenait également la clé primaire associée, ce qui a entraîné la requête incorrecte suivante :,SELECT cname, id, MAX(avg) FROM makerar GROUP BY cname; qui a donné cette erreur trompeuse.
Roberto
1

Cela semble fonctionner aussi

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

J'ai récemment rencontré ce problème, en essayant de compter en utilisant case when, et j'ai constaté que la modification de l'ordre des instructions whichet countrésout le problème:

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

Au lieu d'utiliser - dans ce dernier, où j'ai eu des erreurs que les pommes et les oranges devraient apparaître dans les fonctions d'agrégation

CASE WHEN ((apples = 'TRUE' OR oranges 'TRUE') THEN COUNT(*) END) END AS fruit_counter
Rachel Windzberg
la source
1
La whichdéclaration?
Hillary Sanders du