MySQL "Group By" et "Order By"

97

Je veux pouvoir sélectionner un groupe de lignes dans une table d'e-mails et les regrouper par expéditeur. Ma requête ressemble à ceci:

SELECT 
    `timestamp`, `fromEmail`, `subject`
FROM `incomingEmails` 
GROUP BY LOWER(`fromEmail`) 
ORDER BY `timestamp` DESC

La requête fonctionne presque comme je le souhaite - elle sélectionne les enregistrements groupés par e-mail. Le problème est que le sujet et l'horodatage ne correspondent pas à l'enregistrement le plus récent pour une adresse e-mail particulière.

Par exemple, il peut renvoyer:

fromEmail: john@example.com, subject: hello
fromEmail: mark@example.com, subject: welcome

Lorsque les enregistrements de la base de données sont:

fromEmail: john@example.com, subject: hello
fromEmail: john@example.com, subject: programming question
fromEmail: mark@example.com, subject: welcome

Si le sujet "question de programmation" est le plus récent, comment puis-je faire en sorte que MySQL sélectionne cet enregistrement lors du regroupement des e-mails?

John Kurlak
la source

Réponses:

140

Une solution simple consiste à encapsuler la requête dans une sous-sélection avec l'instruction ORDER en premier et en appliquant le GROUP BY plus tard :

SELECT * FROM ( 
    SELECT `timestamp`, `fromEmail`, `subject`
    FROM `incomingEmails` 
    ORDER BY `timestamp` DESC
) AS tmp_table GROUP BY LOWER(`fromEmail`)

Ceci est similaire à l'utilisation de la jointure mais semble beaucoup plus agréable.

L'utilisation de colonnes non agrégées dans un SELECT avec une clause GROUP BY n'est pas standard. MySQL renvoie généralement les valeurs de la première ligne qu'il trouve et supprime le reste. Toutes les clauses ORDER BY ne s'appliqueront qu'à la valeur de colonne retournée, pas à celles rejetées.

MISE À JOUR IMPORTANTE La sélection de colonnes non agrégées fonctionnait dans la pratique mais ne devrait pas être invoquée. Selon la documentation MySQL "cela est utile principalement lorsque toutes les valeurs de chaque colonne non agrégée non nommée dans GROUP BY sont les mêmes pour chaque groupe. Le serveur est libre de choisir n'importe quelle valeur de chaque groupe, donc à moins qu'elles ne soient identiques, les valeurs Chosen sont indéterminés « .

Depuis la version 5.7.5, ONLY_FULL_GROUP_BY est activé par défaut, de sorte que les colonnes non agrégées provoquent des erreurs de requête (ER_WRONG_FIELD_WITH_GROUP)

Comme @mikep le souligne ci-dessous, la solution consiste à utiliser ANY_VALUE () à partir de 5.7 et plus

Voir http://www.cafewebmaster.com/mysql-order-sort-group https://dev.mysql.com/doc/refman/5.6/en/group-by-handling.html https: //dev.mysql .com / doc / refman / 5.7 / fr / group-by-handling.html https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_any-value

b7kich
la source
7
J'ai proposé la même solution il y a quelques années, et c'est une excellente solution. bravo à b7kich. Deux problèmes ici cependant ... GROUP BY est insensible à la casse, donc LOWER () est inutile, et deuxièmement, $ userID semble être une variable directement à partir de PHP, votre code peut être vulnérable à l'injection SQL si $ userID est fourni par l'utilisateur et non forcé être un entier.
velcrow
La MISE À JOUR IMPORTANTE s'applique également à MariaDB: mariadb.com/kb/en/mariadb/…
Arthur Shipkowski
1
As of 5.7.5 ONLY_FULL_GROUP_BY is enabled by default, i.e. it's impossible to use non-aggregate columns.Le mode SQL peut être modifié pendant l'exécution sans privilèges d'administrateur, il est donc très facile de désactiver ONLY_FULL_GROUP_BY. Par exemple: SET SESSION sql_mode = '';. Démo: db-fiddle.com/f/esww483qFQXbXzJmkHZ8VT/3
mikep
1
Ou une autre alternative au contournement activé ONLY_FULL_GROUP_BY est d'utiliser ANY_VALUE (). Voir plus dev.mysql.com/doc/refman/8.0/en/…
mikep
42

Voici une approche:

SELECT cur.textID, cur.fromEmail, cur.subject, 
     cur.timestamp, cur.read
FROM incomingEmails cur
LEFT JOIN incomingEmails next
    on cur.fromEmail = next.fromEmail
    and cur.timestamp < next.timestamp
WHERE next.timestamp is null
and cur.toUserID = '$userID' 
ORDER BY LOWER(cur.fromEmail)

Fondamentalement, vous joignez la table sur elle-même, en recherchant les lignes ultérieures. Dans la clause where, vous indiquez qu'il ne peut pas y avoir de lignes ultérieures. Cela ne vous donne que la dernière ligne.

S'il peut y avoir plusieurs e-mails avec le même horodatage, cette requête doit être affinée. S'il y a une colonne ID incrémentielle dans la table des e-mails, modifiez la jointure comme suit:

LEFT JOIN incomingEmails next
    on cur.fromEmail = next.fromEmail
    and cur.id < next.id
Andomar
la source
Dit que textIDc'était ambigu = /
John Kurlak
1
Supprimez ensuite l'ambiguïté et ajoutez-lui le préfixe du nom de la table, comme cur.textID. Changé dans la réponse également.
Andomar
C'est la seule solution possible à faire avec Doctrine DQL.
VisioN
Cela ne fonctionne pas lorsque vous essayez de vous joindre automatiquement pour plusieurs colonnes. IE lorsque vous essayez de trouver le dernier e-mail et le dernier nom d'utilisateur et que vous avez besoin de plusieurs jointures auto-gauches pour effectuer cette opération en une seule requête.
Loveen Dyall
Lorsque vous travaillez avec des horodatages / dates passés et futurs, pour limiter le jeu de résultats à des dates non futures, vous devez ajouter une autre condition aux LEFT JOINcritèresAND next.timestamp <= UNIX_TIMESTAMP()
fyrye
32

Comme déjà indiqué dans une réponse, la réponse actuelle est fausse, car le GROUP BY sélectionne arbitrairement l'enregistrement dans la fenêtre.

Si l'on utilise MySQL 5.6 ou MySQL 5.7 avec ONLY_FULL_GROUP_BY, la requête correcte (déterministe) est:

SELECT incomingEmails.*
  FROM (
    SELECT fromEmail, MAX(timestamp) `timestamp`
    FROM incomingEmails
    GROUP BY fromEmail
  ) filtered_incomingEmails
  JOIN incomingEmails USING (fromEmail, timestamp)
GROUP BY fromEmail, timestamp

Pour que la requête s'exécute efficacement, une indexation appropriée est requise.

Notez qu'à des fins de simplification, j'ai supprimé le LOWER(), qui dans la plupart des cas, ne sera pas utilisé.

Marcus
la source
2
Cela devrait être la bonne réponse. Je viens de découvrir un bug sur mon site Web lié à cela. Le order bydans la sous-sélection dans les autres réponses, n'a aucun effet.
Jette
1
OMG, veuillez en faire la réponse acceptée. L'accepté a perdu 5 heures de mon temps :(
Richard Kersey
29

Faites un GROUP BY après le ORDER BY en enveloppant votre requête avec le GROUP BY comme ceci:

SELECT t.* FROM (SELECT * FROM table ORDER BY time DESC) t GROUP BY t.from
11101101b
la source
1
Donc, le GROUP BY` sélectionne automatiquement le dernier time, le plus récent timeou aléatoire?
xrDDDD
1
Il sélectionne l'heure la plus récente car nous trions par time DESC, puis le groupe par prend la première (la plus récente).
11101101b
Maintenant, si seulement je pouvais faire JOINS sur les sous-sélections dans VIEWS, dans mysql 5.1. Peut-être que cette fonctionnalité vient dans une version plus récente.
IcarusNM
21

Selon la norme SQL, vous ne pouvez pas utiliser de colonnes non agrégées dans la liste de sélection. MySQL permet une telle utilisation (mode ONLY_FULL_GROUP_BY uless utilisé) mais le résultat n'est pas prévisible.

ONLY_FULL_GROUP_BY

Vous devez d'abord sélectionner fromEmail, MIN (lecture), puis, avec la deuxième requête (ou sous-requête) - Objet.

noonex
la source
MIN (read) renverrait la valeur minimale de "read". Il cherche probablement plutôt le drapeau "lu" du dernier e-mail.
Andomar
2

J'ai eu du mal avec ces deux approches pour des requêtes plus complexes que celles présentées, car l'approche des sous-requêtes était horriblement inefficace, quels que soient les index que j'ai mis, et parce que je ne pouvais pas obtenir l'auto-jointure externe via Hibernate

La meilleure (et la plus simple) façon de faire cela est de grouper par quelque chose qui est construit pour contenir une concaténation des champs dont vous avez besoin, puis de les extraire à l'aide d'expressions dans la clause SELECT. Si vous avez besoin de faire un MAX (), assurez-vous que le champ sur lequel vous voulez MAX () se trouve toujours à l'extrémité la plus significative de l'entité concaténée.

La clé pour comprendre cela est que la requête ne peut avoir de sens que si ces autres champs sont invariants pour toute entité qui satisfait le Max (), donc en termes de tri, les autres éléments de la concaténation peuvent être ignorés. Il explique comment faire cela tout en bas de ce lien. http://dev.mysql.com/doc/refman/5.0/en/group-by-hidden-columns.html

Si vous pouvez obtenir un événement d'insertion / mise à jour (comme un déclencheur) pour précalculer la concaténation des champs, vous pouvez l'indexer et la requête sera aussi rapide que si le groupe par était juste au-dessus du champ que vous vouliez réellement MAX ( ). Vous pouvez même l'utiliser pour obtenir le maximum de plusieurs champs. Je l'utilise pour faire des requêtes sur des arbres multidimensionnels exprimés sous forme d'ensembles imbriqués.

Mike N
la source