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.
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.
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).
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
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 .
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)
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.
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;
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