web-dev-qa-db-fra.com

MySQL - Sélection d'une colonne ne figurant pas dans Grouper par

J'essaie d'ajouter des fonctionnalités à une application préexistante et je suis tombé sur une vue MySQL qui ressemble à ceci:

SELECT
     AVG(table_name.col1),
     AVG(table_name.col2),
     AVG(table_name.col3),
     table_name.personID,
     table_name.col4
FROM table_name
GROUP BY table_name.personID;

OK, donc il y a quelques fonctions d'agrégat. Vous pouvez sélectionner personID parce que vous le regroupez. Mais il sélectionne également une colonne qui ne fait pas partie d'une fonction d'agrégat et ne fait pas partie de la clause GROUP BY. Comment est-ce possible??? Choisit-il simplement une valeur aléatoire parce que les valeurs ne sont certainement pas uniques par groupe?

D'où je viens (serveur MSSQL), c'est une erreur. Quelqu'un peut-il m'expliquer ce comportement et pourquoi il est autorisé dans MySQL?

43
colithium

Il est vrai que cette fonctionnalité autorise certaines requêtes ambiguës et renvoie en mode silencieux un jeu de résultats avec une valeur arbitraire choisie dans cette colonne. En pratique, il s'agit généralement de la valeur de la ligne du groupe qui est stockée physiquement en premier.

Ces requêtes ne sont pas ambiguës si vous ne choisissez que des colonnes fonctionnellement dépendantes de la (des) colonne (s) du critère GROUP BY. En d'autres termes, s'il ne peut y avoir qu'une seule valeur distincte de la colonne "ambiguë" par valeur qui définit le groupe, il n'y a pas de problème. Cette requête serait illégale dans Microsoft SQL Server (et ANSI SQL), même si elle ne peut logiquement aboutir à une ambiguïté:

SELECT AVG(table1.col1), table1.personID, persons.col4
FROM table1 JOIN persons ON (table1.personID = persons.id)
GROUP BY table1.personID;

De plus, MySQL a un mode SQL pour le rendre conforme au standard: ONLY_FULL_GROUP_BY

FWIW, SQLite autorise également ces clauses GROUP BY ambiguës, mais il choisit la valeur dans la ligne last du groupe.


Au moins dans la version que j'ai testée. Cela signifie que arbitraire est que MySQL ou SQLite pourraient modifier leur implémentation à l’avenir et adopter un comportement différent. Par conséquent, vous ne devez pas vous fier au comportement qui prévaut dans les cas ambigus comme celui-ci. Il est préférable de réécrire vos requêtes de manière déterministe et non ambiguë. C'est pourquoi MySQL 5.7 active désormais ONLY_FULL_GROUP_BY par défaut.

47
Bill Karwin

J'aurais dû googler un peu plus longtemps ... Il semble que j'ai trouvé ma réponse .

MySQL étend l'utilisation de GROUP BY ainsi que vous pouvez utiliser des colonnes non agrégées ou des calculs dans la liste SELECT qui n'apparaissent pas dans le GROUP BY clause. Vous pouvez utiliser cette fonctionnalité pour obtenir de meilleures performances en évitant tri inutile des colonnes et regroupement. Par exemple, vous n’avez pas besoin de grouper sur customer.name dans le fichier requête suivante

En SQL standard, vous devez ajouter customer.name à la clause GROUP BY . Dans MySQL, le nom est redondant.

Pourtant, cela semble juste ... faux.

11
colithium

Disons que vous avez une requête comme celle-ci:

SELECT g, v 
FROM t
GROUP BY g;

Dans ce cas, pour chaque valeur possible de g, mysql choisit l'une des valeurs correspondantes de v.

Cependant, lequel est choisi, dépend de certaines circonstances. 

J'ai lu quelque part que pour chaque groupe de g, la première valeur de v est conservée, dans l'ordre dans lequel les enregistrements ont été insérés dans la table t

C'est assez moche parce que les enregistrements dans une table doivent être traités comme un set où l'ordre des éléments ne devrait pas avoir d'importance. C'est tellement "mysql-ish" ...

Si vous voulez déterminer quelle valeur pour v conserver, vous devez appliquer une sous-sélection pour t comme ceci:

SELECT g, v 
FROM (
    SELECT * 
        FROM t 
        ORDER BY g, v DESC
) q
GROUP BY g;

De cette façon, vous définissez l'ordre dans lequel les enregistrements de la sous-requête sont traités par la requête externe. Vous pouvez donc vous fier à la valeur de v qu'il choisira pour les valeurs individuelles de g

Cependant, si vous avez besoin de conditions WHERE, soyez très prudent. Si vous ajoutez la condition WHERE à la sous-requête, le comportement sera conservé, la valeur attendue sera toujours renvoyée:

SELECT g, v 
FROM (
    SELECT * 
        FROM t 
        WHERE g = '737a8783-110c-447e-b4c2-1cbb7c6b72c9' 
        ORDER BY g, v DESC
) q
GROUP BY g;

C’est ce à quoi vous vous attendez, la sous-sélection filtre et ordonne la table. Il conserve les enregistrements où g a la valeur donnée et la requête externe renvoie g et la première valeur de v

Cependant, si vous ajoutez la même condition WHERE à la requête externe, vous obtenez un résultat non déterministe:

SELECT g, v 
FROM (
    SELECT * 
        FROM t 
        -- WHERE g = '737a8783-110c-447e-b4c2-1cbb7c6b72c9' 
        ORDER BY g, v DESC
) q
WHERE g = '737a8783-110c-447e-b4c2-1cbb7c6b72c9'
GROUP BY g;

Étonnamment, vous pouvez obtenir différentes valeurs pour v lors de l'exécution répétée de la même requête, ce qui est ... étrange. Le comportement attendu consiste à extraire tous les enregistrements de la sous-requête dans le bon ordre, en les filtrant dans la requête externe, puis en sélectionnant la même chose que dans l'exemple précédent. Mais ce n'est pas le cas. 

Il choisit une valeur pour v apparemment aléatoire. La même requête a renvoyé des valeurs différentes pour v si j'ai exécuté plus de temps (~ 20) mais que la distribution n'était pas uniforme. 

Si, au lieu d’ajouter un WHERE externe, vous spécifiez une condition HAVING comme ceci:

SELECT g, v 
FROM (
    SELECT * 
        FROM t1 
        -- WHERE g = '737a8783-110c-447e-b4c2-1cbb7c6b72c9' 
        ORDER BY g, v DESC
) q
-- WHERE g = '737a8783-110c-447e-b4c2-1cbb7c6b72c9'
GROUP BY g
HAVING g = '737a8783-110c-447e-b4c2-1cbb7c6b72c9';

Ensuite, vous obtenez à nouveau un comportement cohérent. 

CONCLUSION: Je suggérerais de ne pas utiliser cette technique du tout. Si vous voulez vraiment/devez alors éviter les conditions WHERE dans la requête externe. Utilisez-le dans la requête interne si vous le pouvez ou utilisez une clause HAVING dans la requête externe. 

Je l'ai testé avec ces données:

CREATE TABLE t1 (
    v INT,
    g VARCHAR(36)
);

INSERT INTO t1 VALUES (1, '737a8783-110c-447e-b4c2-1cbb7c6b72c9');
INSERT INTO t1 VALUES (2, '737a8783-110c-447e-b4c2-1cbb7c6b72c9');

dans mysql 5.6.41. 

Peut-être qu’il s’agit simplement d’un bogue corrigé/corrigé dans les versions les plus récentes. Merci de nous faire part de vos commentaires si vous avez de l’expérience avec les versions les plus récentes. 

0
Csongor Halmai
select * from personel where p_id IN(select
min(dbo.personel.p_id)
FROM
personel
GROUP BY dbo.personel.p_adi)
0
Salih Kiraz