web-dev-qa-db-fra.com

MySQL classer par avant groupe par

Il y a beaucoup de questions similaires à trouver ici, mais je ne pense pas que quelqu'un y réponde correctement.

Je vais continuer à partir de l'actuel numéro le plus populaire question et utiliser leur exemple si cela vous convient.

Dans ce cas, la tâche consiste à obtenir le dernier message pour chaque auteur de la base de données.

L'exemple de requête produit des résultats inutilisables, car ce n'est pas toujours le dernier message renvoyé.

SELECT wp_posts.* FROM wp_posts
    WHERE wp_posts.post_status='publish'
    AND wp_posts.post_type='post'
    GROUP BY wp_posts.post_author           
    ORDER BY wp_posts.post_date DESC

La réponse acceptée actuellement est

SELECT
    wp_posts.*
FROM wp_posts
WHERE
    wp_posts.post_status='publish'
    AND wp_posts.post_type='post'
GROUP BY wp_posts.post_author
HAVING wp_posts.post_date = MAX(wp_posts.post_date) <- ONLY THE LAST POST FOR EACH AUTHOR
ORDER BY wp_posts.post_date DESC

Malheureusement, cette réponse est fausse et simple et, dans de nombreux cas, produit des résultats moins stables que la requête initiale.

Ma meilleure solution consiste à utiliser une sous-requête de la forme

SELECT wp_posts.* FROM 
(
    SELECT * 
    FROM wp_posts
    ORDER BY wp_posts.post_date DESC
) AS wp_posts
WHERE wp_posts.post_status='publish'
AND wp_posts.post_type='post'
GROUP BY wp_posts.post_author 

Ma question est simple alors: Est-il possible de commander des lignes avant de les regrouper sans recourir à une sous-requête?

Modifier : Cette question était la continuation d'une autre question et les spécificités de ma situation sont légèrement différentes. Vous pouvez (et devriez) supposer qu'il existe également un wp_posts.id qui est un identificateur unique pour ce message particulier.

219
Rob Forrest

L'utilisation d'un ORDER BY dans une sous-requête n'est pas la meilleure solution à ce problème.

La meilleure solution pour obtenir la max(post_date) par auteur consiste à utiliser une sous-requête pour renvoyer la date maximale, puis la joindre à votre table à la fois sur le post_author et sur la date maximale.

La solution devrait être:

SELECT p1.* 
FROM wp_posts p1
INNER JOIN
(
    SELECT max(post_date) MaxPostDate, post_author
    FROM wp_posts
    WHERE post_status='publish'
       AND post_type='post'
    GROUP BY post_author
) p2
  ON p1.post_author = p2.post_author
  AND p1.post_date = p2.MaxPostDate
WHERE p1.post_status='publish'
  AND p1.post_type='post'
order by p1.post_date desc

Si vous disposez des exemples de données suivants:

CREATE TABLE wp_posts
    (`id` int, `title` varchar(6), `post_date` datetime, `post_author` varchar(3))
;

INSERT INTO wp_posts
    (`id`, `title`, `post_date`, `post_author`)
VALUES
    (1, 'Title1', '2013-01-01 00:00:00', 'Jim'),
    (2, 'Title2', '2013-02-01 00:00:00', 'Jim')
;

La sous-requête va renvoyer la date maximale et l'auteur de:

MaxPostDate | Author
2/1/2013    | Jim

Puis, puisque vous rejoignez ce tableau, vous renverrez les détails complets de cet article pour les deux valeurs.

Voir SQL Fiddle avec Demo .

Pour développer mes commentaires sur l'utilisation d'une sous-requête afin de renvoyer ces données avec précision.

MySQL ne vous oblige pas à GROUP BY chaque colonne que vous incluez dans la liste SELECT. Par conséquent, si vous ne modifiez que GROUP BY une colonne mais renvoyez 10 colonnes au total, rien ne garantit que les valeurs des autres colonnes appartenant au post_author renvoyé. Si la colonne ne se trouve pas dans un GROUP BY, MySQL choisit quelle valeur doit être renvoyée.

L'utilisation de la sous-requête avec la fonction d'agrégat garantit que l'auteur et la publication corrects sont renvoyés à chaque fois.

En remarque, si MySQL vous permet d’utiliser un ORDER BY dans une sous-requête et vous permet d’appliquer un GROUP BY à toutes les colonnes de la liste SELECT, ce comportement n’est pas autorisé dans d’autres. bases de données, y compris SQL Server.

342
Taryn

Votre solution utilise une clause extension to GROUP BY qui permet de regrouper certains champs (dans ce cas, juste post_author):

GROUP BY wp_posts.post_author

et sélectionnez les colonnes non agrégées:

SELECT wp_posts.*

qui ne sont pas répertoriés dans la clause group by ou qui ne sont pas utilisés dans une fonction d'agrégation (MIN, MAX, COUNT, etc.).

Utilisation correcte de l'extension de la clause GROUP BY

Ceci est utile lorsque toutes les valeurs des colonnes non agrégées sont égales pour chaque ligne.

Par exemple, supposons que vous ayez une table GardensFlowers (name du jardin, flower qui pousse dans le jardin):

INSERT INTO GardensFlowers VALUES
('Central Park',       'Magnolia'),
('Hyde Park',          'Tulip'),
('Gardens By The Bay', 'Peony'),
('Gardens By The Bay', 'Cherry Blossom');

et vous voulez extraire toutes les fleurs qui poussent dans un jardin, où plusieurs fleurs poussent. Ensuite, vous devez utiliser une sous-requête, par exemple, vous pouvez utiliser ceci:

SELECT GardensFlowers.*
FROM   GardensFlowers
WHERE  name IN (SELECT   name
                FROM     GardensFlowers
                GROUP BY name
                HAVING   COUNT(DISTINCT flower)>1);

Si vous avez plutôt besoin d'extraire toutes les fleurs qui sont les seules fleurs du garder, vous pouvez simplement changer la condition HAVING en HAVING COUNT(DISTINCT flower)=1, mais MySql vous permet également de l'utiliser:

SELECT   GardensFlowers.*
FROM     GardensFlowers
GROUP BY name
HAVING   COUNT(DISTINCT flower)=1;

pas de sous-requête, pas de SQL standard, mais plus simple.

Utilisation incorrecte de l'extension de la clause GROUP BY

Mais que se passe-t-il si vous sélectionnez des colonnes non agrégées qui ne sont pas égales pour toutes les lignes? Quelle est la valeur que MySql choisit pour cette colonne?

Il semble que MySql choisisse toujours la valeur FIRST qu'elle rencontre.

Pour vous assurer que la première valeur rencontrée correspond exactement à la valeur souhaitée, vous devez appliquer un GROUP BY à une requête ordonnée, d'où la nécessité d'utiliser une sous-requête. Vous ne pouvez pas le faire autrement.

En supposant que MySql choisisse toujours la première ligne qu'il rencontre, vous triez correctement les lignes avant GROUP BY. Mais malheureusement, si vous lisez attentivement la documentation, vous remarquerez que cette hypothèse est fausse.

Lors de la sélection de colonnes non agrégées qui ne sont pas toujours identiques, MySql est libre de choisir n'importe quelle valeur, de sorte que la valeur résultante affichée est indéterminée .

Je vois que cette astuce pour obtenir la première valeur d'une colonne non agrégée est beaucoup utilisée, et cela fonctionne généralement/presque toujours, je l'utilise aussi parfois (à mes risques et périls). Mais comme ce n'est pas documenté, vous ne pouvez pas vous fier à ce comportement.

Ce lien (merci ypercube!) L'astuce GROUP BY a été optimisée montre une situation dans laquelle la même requête renvoie des résultats différents entre MySql et MariaDB, probablement en raison d'un moteur d'optimisation différent.

Donc, si cette astuce fonctionne, c'est simplement une question de chance.

Le réponse acceptée sur l'autre question me semble faux:

HAVING wp_posts.post_date = MAX(wp_posts.post_date)

wp_posts.post_date est une colonne non agrégée et sa valeur sera officiellement indéterminée, mais il s'agira probablement du premier post_date rencontré. Mais puisque l'astuce GROUP BY est appliquée à une table non ordonnée, il n'est pas certain de savoir quel est le premier post_date rencontré.

Il renverra probablement des publications qui sont les seules publications d'un seul auteur, mais même cela n'est pas toujours certain.

Une solution possible

Je pense que cela pourrait être une solution possible:

SELECT wp_posts.*
FROM   wp_posts
WHERE  id IN (
  SELECT max(id)
  FROM wp_posts
  WHERE (post_author, post_date) = (
    SELECT   post_author, max(post_date)
    FROM     wp_posts
    WHERE    wp_posts.post_status='publish'
             AND wp_posts.post_type='post'
    GROUP BY post_author
  ) AND wp_posts.post_status='publish'
    AND wp_posts.post_type='post'
  GROUP BY post_author
)

Sur la requête interne, je retourne la date de publication maximale pour chaque auteur. Je prends ensuite en considération le fait que le même auteur pourrait théoriquement avoir deux postes en même temps, donc je n’obtiens que l’ID maximum. Et puis je renvoie toutes les lignes qui ont ces ID maximum. Cela pourrait être accéléré en utilisant des jointures au lieu de la clause IN.

(Si vous êtes sûr que ID ne fait qu'augmenter, et si ID1 > ID2 signifie également que post_date1 > post_date2, alors la requête pourrait être beaucoup plus simple, mais je ne suis pas sûr que cela est le cas).

19
fthiella

Ce que vous allez lire est plutôt hacky, alors n'essayez pas cela à la maison!

En général, dans SQL, la réponse à votre question est NON, mais à cause du mode d'assouplissement du GROUP BY (mentionné par @ bluefeet ), la réponse est OUI dans MySQL.

Supposons que vous ayez un index BTREE sur (post_status, post_type, post_author, post_date). A quoi ressemble l'indice sous le capot?

(post_status = 'publier', post_type = 'post', post_author = 'utilisateur A', post_date = '2012-12-01') (post_status = 'publier', post_type = 'post', post_author = 'utilisateur A', post_date = '2012-12-31') (post_status = 'publish', post_type = 'post', post_author = 'utilisateur B', post_date = '2012-10-01') (post_status = 'publish', post_type = ' post ', post_author =' utilisateur B ', post_date =' 2012-12-01 ')

C'est que les données sont triées par tous ces champs dans l'ordre croissant.

Lorsque vous effectuez un GROUP BY par défaut, il trie les données en fonction du champ de regroupement (post_author, dans notre cas; post_status, les types post_type sont requis par la clause WHERE). index, il prend les données pour chaque premier enregistrement dans l'ordre croissant. C’est-à-dire que la requête va chercher ce qui suit (le premier message pour chaque utilisateur):

(post_status = 'publier', post_type = 'post', post_author = 'utilisateur A', post_date = '2012-12-01') (post_status = 'publier', post_type = 'post', post_author = 'utilisateur B', post_date = '2012-10-01')

Mais GROUP BY dans MySQL vous permet de spécifier explicitement cet ordre. Et lorsque vous demandez post_user dans l'ordre décroissant, il parcourra notre index dans l'ordre inverse, en prenant toujours le premier enregistrement de chaque groupe, qui est en réalité le dernier.

C'est

...
WHERE wp_posts.post_status='publish' AND wp_posts.post_type='post'
GROUP BY wp_posts.post_author DESC

va nous donner

(post_status = 'publier', post_type = 'post', post_author = 'utilisateur B', post_date = '2012-12-01') (post_status = 'publier', post_type = 'post', post_author = 'utilisateur A', post_date = '2012-12-31')

Désormais, lorsque vous commandez les résultats du regroupement par post_date, vous obtenez les données souhaitées.

SELECT wp_posts.*
FROM wp_posts
WHERE wp_posts.post_status='publish' AND wp_posts.post_type='post'
GROUP BY wp_posts.post_author DESC
ORDER BY wp_posts.post_date DESC;

NB:

Ce n'est pas ce que je recommanderais pour cette requête particulière. Dans ce cas, je voudrais utiliser une version légèrement modifiée de ce que @ bluefeet suggère. Mais cette technique pourrait être très utile. Regardez ma réponse ici: Récupération du dernier enregistrement de chaque groupe

Pièges : L’inconvénient de cette approche est que

  • le résultat de la requête dépend de l'index, ce qui est contraire à l'esprit du code SQL (les index ne doivent accélérer que les requêtes);
  • index ne sait rien de son influence sur la requête (vous ou quelqu'un d'autre pourrait à l'avenir trouver l'index trop consommateur de ressources et le modifier d'une manière ou d'une autre, annulant ainsi les résultats de la requête, et pas seulement ses performances)
  • si vous ne comprenez pas comment fonctionne la requête, vous oubliez probablement l'explication dans un mois et la requête vous embarrassera, vous et vos collègues.

L'avantage est la performance dans les cas difficiles. Dans ce cas, les performances de la requête doivent être identiques à celles de la requête de @ bluefeet, en raison de la quantité de données impliquées dans le tri (toutes les données sont chargées dans une table temporaire, puis triées; au fait, sa requête requiert le (post_status, post_type, post_author, post_date) index aussi).

Ce que je suggérerais :

Comme je l’ai dit plus tôt, ces requêtes font perdre du temps à MySQL en triant d’énormes quantités de données dans une table temporaire. Si vous avez besoin de la pagination (c'est-à-dire que LIMIT est impliqué), la plupart des données sont même effacées. Ce que je voudrais faire est de minimiser la quantité de données triées: il s'agit de trier et de limiter un minimum de données dans la sous-requête, puis de rejoindre la table entière.

SELECT * 
FROM wp_posts
INNER JOIN
(
  SELECT max(post_date) post_date, post_author
  FROM wp_posts
  WHERE post_status='publish' AND post_type='post'
  GROUP BY post_author
  ORDER BY post_date DESC
  -- LIMIT GOES HERE
) p2 USING (post_author, post_date)
WHERE post_status='publish' AND post_type='post';

La même requête en utilisant l'approche décrite ci-dessus:

SELECT *
FROM (
  SELECT post_id
  FROM wp_posts
  WHERE post_status='publish' AND post_type='post'
  GROUP BY post_author DESC
  ORDER BY post_date DESC
  -- LIMIT GOES HERE
) as ids
JOIN wp_posts USING (post_id);

Toutes ces requêtes avec leurs plans d'exécution sur SQLFiddle .

9
newtover

Essaye celui-là. Obtenez juste la liste des dernières dates de publication de chaque auteur. C'est ça

SELECT wp_posts.* FROM wp_posts WHERE wp_posts.post_status='publish'
AND wp_posts.post_type='post' AND wp_posts.post_date IN(SELECT MAX(wp_posts.post_date) FROM wp_posts GROUP BY wp_posts.post_author) 
8
sanchitkhanna26

Non. Cela n'a aucun sens de classer les enregistrements avant le regroupement, car le regroupement va modifier l'ensemble des résultats. La méthode de sous-requête est la méthode préférée. Si cela vous semble trop lent, vous devrez modifier la structure de votre table, par exemple en enregistrant l'identifiant du dernier article de chaque auteur dans un tableau séparé ou en insérant une colonne booléenne indiquant pour chaque auteur lequel de ses articles est le dernier. un.

3
Dennisch

Pour récapituler, la solution standard utilise une sous-requête non corrélée et ressemble à ceci:

SELECT x.*
  FROM my_table x
  JOIN (SELECT grouping_criteria,MAX(ranking_criterion) max_n FROM my_table GROUP BY grouping_criteria) y
    ON y.grouping_criteria = x.grouping_criteria
   AND y.max_n = x.ranking_criterion;

Si vous utilisez une version ancienne de MySQL ou un ensemble de données relativement petit, vous pouvez utiliser la méthode suivante:

SELECT x.*
  FROM my_table x
  LEFT
  JOIN my_table y
    ON y.joining_criteria = x.joining_criteria
   AND y.ranking_criteria < x.ranking_criteria
 WHERE y.some_non_null_column IS NULL;  
0
Strawberry

Il suffit d'utiliser la fonction max et la fonction group

    select max(taskhistory.id) as id from taskhistory
            group by taskhistory.taskid
            order by taskhistory.datum desc