Obtenez des enregistrements avec une valeur maximale pour chaque groupe de résultats SQL groupés

229

Comment obtenez-vous les lignes qui contiennent la valeur maximale pour chaque ensemble groupé?

J'ai vu des variations trop compliquées sur cette question, et aucune avec une bonne réponse. J'ai essayé de rassembler l'exemple le plus simple possible:

Étant donné un tableau comme celui-ci ci-dessous, avec les colonnes personne, groupe et âge, comment obtiendriez-vous la personne la plus âgée dans chaque groupe? (Une égalité au sein d'un groupe devrait donner le premier résultat alphabétique)

Person | Group | Age
---
Bob  | 1     | 32  
Jill | 1     | 34  
Shawn| 1     | 42  
Jake | 2     | 29  
Paul | 2     | 36  
Laura| 2     | 39  

Ensemble de résultats souhaité:

Shawn | 1     | 42    
Laura | 2     | 39  
Yarin
la source
3
Attention: la réponse acceptée a fonctionné en 2012 lorsqu'elle a été rédigée. Cependant, cela ne fonctionne plus pour plusieurs raisons, comme indiqué dans les commentaires.
Rick James

Réponses:

132

Il y a un moyen super simple de le faire dans mysql:

select * 
from (select * from mytable order by `Group`, age desc, Person) x
group by `Group`

Cela fonctionne parce que dans mysql, vous êtes autorisé à ne pas agréger les colonnes non groupées, auquel cas mysql renvoie simplement la première ligne. La solution consiste à d'abord classer les données de telle sorte que pour chaque groupe, la ligne souhaitée soit la première, puis à regrouper en fonction des colonnes pour lesquelles vous souhaitez obtenir la valeur.

Vous évitez les sous-requêtes compliquées qui tentent de trouver max()etc., ainsi que les problèmes de renvoi de plusieurs lignes lorsqu'il y en a plusieurs avec la même valeur maximale (comme le feraient les autres réponses)

Remarque: il s'agit d'une solution réservée à mysql . Toutes les autres bases de données que je connais lèveront une erreur de syntaxe SQL avec le message "les colonnes non agrégées ne sont pas répertoriées dans la clause group by" ou similaire. Parce que cette solution utilise sans papier comportement, plus prudent peut vouloir inclure un test pour affirmer qu'il reste à travailler si une version future de MySQL changer ce comportement.

Mise à jour de la version 5.7:

Depuis la version 5.7, le sql-modeparamètre inclut ONLY_FULL_GROUP_BYpar défaut, donc pour que cela fonctionne, vous ne devez pas avoir cette option (modifiez le fichier d'options du serveur pour supprimer ce paramètre).

Bohème
la source
66
"mysql renvoie juste la première ligne." - c'est peut-être ainsi que cela fonctionne, mais ce n'est pas garanti. La documentation indique: "Le serveur est libre de choisir n'importe quelle valeur de chaque groupe, donc à moins qu'elles ne soient les mêmes, les valeurs choisies sont indéterminées." . Le serveur ne sélectionne pas de lignes mais des valeurs (pas nécessairement de la même ligne) pour chaque colonne ou expression qui apparaît dans la SELECTclause et n'est pas calculée à l'aide d'une fonction d'agrégation.
axiac
16
Ce comportement a changé sur MySQL 5.7.5 et par défaut, il rejette cette requête car les colonnes de la SELECTclause ne dépendent pas fonctionnellement des GROUP BYcolonnes. S'il est configuré pour l'accepter (`ONLY_FULL_GROUP_BY` est désactivé), il fonctionne comme les versions précédentes (c'est-à-dire que les valeurs de ces colonnes sont indéterminées).
axiac
17
Je suis surpris que cette réponse ait reçu autant de votes positifs. C'est faux et c'est mauvais. Cette requête n'est pas garantie de fonctionner. Les données d'une sous-requête sont un ensemble non ordonné malgré la clause order by. MySQL peut vraiment ordonner les enregistrements maintenant et garder cet ordre, mais il ne briserait aucune règle s'il cessait de le faire dans une future version. Ensuite, le GROUP BYcondensé en un seul enregistrement, mais tous les champs seront arbitrairement choisis dans les enregistrements. Il se peut que MySQL sélectionne toujours simplement la première ligne, mais il pourrait tout aussi bien choisir n'importe quelle autre ligne ou même des valeurs de différentes lignes dans une future version.
Thorsten Kettner du
9
D'accord, nous ne sommes pas d'accord ici. Je n'utilise pas de fonctionnalités non documentées qui fonctionnent actuellement et je me fie à certains tests qui, je l'espère, couvriront cela. Vous savez que vous êtes juste chanceux que l'implémentation actuelle vous obtienne le premier enregistrement complet où les documents indiquent clairement que vous pourriez avoir des valeurs indéterminées à la place, mais vous l'utilisez toujours. Certains paramètres de session ou de base de données simples peuvent changer cela à tout moment. Je considérerais cela trop risqué.
Thorsten Kettner
3
Cette réponse semble fausse. Selon la doc , le serveur est libre de choisir n'importe quelle valeur de chaque groupe ... De plus, la sélection des valeurs de chaque groupe ne peut pas être influencée par l'ajout d'une clause ORDER BY. Le tri de l'ensemble de résultats se produit après que les valeurs ont été choisies et ORDER BY n'affecte pas la valeur dans chaque groupe que le serveur choisit.
Tgr
298

La bonne solution est:

SELECT o.*
FROM `Persons` o                    # 'o' from 'oldest person in group'
  LEFT JOIN `Persons` b             # 'b' from 'bigger age'
      ON o.Group = b.Group AND o.Age < b.Age
WHERE b.Age is NULL                 # bigger age not found

Comment ça fonctionne:

Il fait correspondre chaque ligne de oavec toutes les lignes bayant la même valeur dans la colonne Groupet une plus grande valeur dans la colonne Age. Toute ligne one contenant pas la valeur maximale de son groupe dans la colonne Agecorrespondra à une ou plusieurs lignes de b.

Le LEFT JOINfait correspondre la personne la plus âgée du groupe (y compris les personnes seules dans leur groupe) avec une rangée pleine de NULLs de b(«pas de plus grand âge dans le groupe»).
L'utilisation INNER JOINrend ces lignes non identiques et elles sont ignorées.

La WHEREclause ne conserve que les lignes ayant NULLs dans les champs extraits de b. Ce sont les personnes les plus âgées de chaque groupe.

Lectures complémentaires

Cette solution et bien d'autres sont expliquées dans le livre SQL Antipatterns: éviter les pièges de la programmation de base de données

axiaque
la source
43
BTW cela peut renvoyer deux lignes ou plus pour un même groupe si o.Age = b.Age, par exemple si Paul du groupe 2 est sur 39 comme Laura. Cependant, si nous ne voulons pas d'un tel comportement, nous pouvons le faire:ON o.Group = b.Group AND (o.Age < b.Age or (o.Age = b.Age and o.id < b.id))
Todor
8
Incroyable! Pour 20 millions d'enregistrements, c'est 50 fois plus rapide que l'algorithme "naïf" (se joindre à une sous-requête avec max ())
user2706534
3
Fonctionne parfaitement avec les commentaires @Todor. J'ajouterais que s'il existe d'autres conditions de requête, elles doivent être ajoutées dans le FROM et le LEFT JOIN. Quelque chose COMME: DE (SÉLECTIONNER * DE LA PERSONNE
OERE L'ÂGE
1
@AlainZelink ces "autres conditions de requête" ne sont-elles pas mieux placées dans la liste finale des conditions WHERE, afin de ne pas introduire de sous-requêtes - qui n'étaient pas nécessaires dans la réponse @ axiac originale?
tarilabs
5
Cette solution a fonctionné; cependant, il a commencé à être signalé dans le journal des requêtes lentes lors d'une tentative avec plus de 10 000 lignes partageant le même ID. Joining on indexed column. Un cas rare, mais il vaut la peine d'être mentionné.
chaseisabelle
50

Vous pouvez vous joindre à une sous-requête qui tire le MAX(Group)et Age. Cette méthode est portable sur la plupart des SGBDR.

SELECT t1.*
FROM yourTable t1
INNER JOIN
(
    SELECT `Group`, MAX(Age) AS max_age
    FROM yourTable
    GROUP BY `Group`
) t2
    ON t1.`Group` = t2.`Group` AND t1.Age = t2.max_age;
Michael Berkowski
la source
Michael, merci pour cela, mais avez-vous une réponse au problème du retour de plusieurs lignes sur les liens, selon les commentaires de Bohemian?
Yarin
1
@Yarin S'il y avait 2 lignes, par exemple où Group = 2, Age = 20, la sous-requête renverrait l'une d'entre elles, mais la ONclause de jointure correspondrait aux deux , donc vous obtiendrez 2 lignes avec le même groupe / âge, bien que des valeurs différentes pour les autres colonnes, plutôt qu'un.
Michael Berkowski
Sommes-nous donc en train de dire qu'il est impossible de limiter les résultats à un par groupe à moins que nous n'utilisions la voie Bohemians MySQL uniquement?
Yarin
@Yarin n'est pas impossible, nécessite simplement plus de travail s'il y a des colonnes supplémentaires - éventuellement une autre sous-requête imbriquée pour extraire l'id associé maximal pour chaque paire de groupe / âge similaire, puis joignez-vous à cela pour obtenir le reste de la ligne en fonction de l'ID.
Michael Berkowski
Cela devrait être la réponse acceptée (la réponse actuellement acceptée échouera sur la plupart des autres SGBDR et, en fait, échouerait même sur de nombreuses versions de MySQL).
Tim Biegeleisen
28

Ma solution simple pour SQLite (et probablement MySQL):

SELECT *, MAX(age) FROM mytable GROUP BY `Group`;

Cependant, cela ne fonctionne pas dans PostgreSQL et peut-être sur d'autres plateformes.

Dans PostgreSQL, vous pouvez utiliser la clause DISTINCT ON :

SELECT DISTINCT ON ("group") * FROM "mytable" ORDER BY "group", "age" DESC;
Igor Kulagin
la source
@Bohemian désolé, je le sais, ceci est uniquement MySQL car il inclut des colonnes non agrégées
Cec
2
@IgorKulagin - Ne fonctionne pas dans Postgres - Message d'erreur: la colonne "mytable.id" doit apparaître dans la clause GROUP BY ou être utilisée dans une fonction d'agrégation
Yarin
13
La requête MySQL ne peut fonctionner que par accident à de nombreuses reprises. Le "SELECT *" peut renvoyer des informations qui ne correspondent pas au MAX (âge) correspondant. Cette réponse est fausse. C'est probablement aussi le cas pour SQLite.
Albert Hendriks
2
Mais cela correspond au cas où nous devons sélectionner la colonne groupée et la colonne max. Cela ne correspond pas à l'exigence ci-dessus où il en résulterait ('Bob', 1, 42) mais le résultat attendu est ('Shawn', 1, 42)
Ram Babu S
1
Bon pour les postgres
Karol Gasienica
4

Utilisation de la méthode de classement.

SELECT @rn :=  CASE WHEN @prev_grp <> groupa THEN 1 ELSE @rn+1 END AS rn,  
   @prev_grp :=groupa,
   person,age,groupa  
FROM   users,(SELECT @rn := 0) r        
HAVING rn=1
ORDER  BY groupa,age DESC,person
sel
la source
sel - besoin d'explication - je n'ai jamais vu :=auparavant - qu'est-ce que c'est?
Yarin
1
: = est l'opérateur d'affectation. Vous pouvez en savoir plus sur dev.mysql.com/doc/refman/5.0/en/user-variables.html
sel
Je vais devoir creuser cela - je pense que la réponse complique notre scénario, mais merci de m'avoir
appris
3

Je ne sais pas si MySQL a la fonction row_number. Si c'est le cas, vous pouvez l'utiliser pour obtenir le résultat souhaité. Sur SQL Server, vous pouvez faire quelque chose de similaire à:

CREATE TABLE p
(
 person NVARCHAR(10),
 gp INT,
 age INT
);
GO
INSERT  INTO p
VALUES  ('Bob', 1, 32);
INSERT  INTO p
VALUES  ('Jill', 1, 34);
INSERT  INTO p
VALUES  ('Shawn', 1, 42);
INSERT  INTO p
VALUES  ('Jake', 2, 29);
INSERT  INTO p
VALUES  ('Paul', 2, 36);
INSERT  INTO p
VALUES  ('Laura', 2, 39);
GO

SELECT  t.person, t.gp, t.age
FROM    (
         SELECT *,
                ROW_NUMBER() OVER (PARTITION BY gp ORDER BY age DESC) row
         FROM   p
        ) t
WHERE   t.row = 1;
user130268
la source
1
C'est le cas depuis la version 8.0.
Ilja Everilä
2

La solution d'Axiac est finalement celle qui a le mieux fonctionné pour moi. J'avais cependant une complexité supplémentaire: une "valeur max" calculée, dérivée de deux colonnes.

Prenons le même exemple: je voudrais la personne la plus âgée de chaque groupe. S'il y a des gens qui sont tout aussi âgés, prenez la personne la plus grande.

J'ai dû effectuer la jointure gauche deux fois pour obtenir ce comportement:

SELECT o1.* WHERE
    (SELECT o.*
    FROM `Persons` o
    LEFT JOIN `Persons` b
    ON o.Group = b.Group AND o.Age < b.Age
    WHERE b.Age is NULL) o1
LEFT JOIN
    (SELECT o.*
    FROM `Persons` o
    LEFT JOIN `Persons` b
    ON o.Group = b.Group AND o.Age < b.Age
    WHERE b.Age is NULL) o2
ON o1.Group = o2.Group AND o1.Height < o2.Height 
WHERE o2.Height is NULL;

J'espère que cela t'aides! Je suppose qu'il devrait y avoir une meilleure façon de le faire ...

Arthur C
la source
2

Ma solution ne fonctionne que si vous avez besoin de récupérer une seule colonne, mais pour mes besoins était la meilleure solution trouvée en termes de performances (elle n'utilise qu'une seule requête!):

SELECT SUBSTRING_INDEX(GROUP_CONCAT(column_x ORDER BY column_y),',',1) AS xyz,
   column_z
FROM table_name
GROUP BY column_z;

Il utilise GROUP_CONCAT afin de créer une liste de concaturation ordonnée puis je sous-chaîne à la première seulement.

Antonio Giovanazzi
la source
Peut confirmer que vous pouvez obtenir plusieurs colonnes en triant sur la même clé à l'intérieur du group_concat, mais devez écrire un group_concat / index / substring distinct pour chaque colonne.
Rasika
Le bonus ici est que vous pouvez ajouter plusieurs colonnes au tri dans group_concat et cela résoudrait les liens facilement et garantirait un seul enregistrement par groupe. Bravo pour la solution simple et efficace!
Rasika
2

J'ai une solution simple en utilisant WHERE IN

SELECT a.* FROM `mytable` AS a    
WHERE a.age IN( SELECT MAX(b.age) AS age FROM `mytable` AS b GROUP BY b.group )    
ORDER BY a.group ASC, a.person ASC
Khalid Musa Sagar
la source
1

Utilisation des CTE - Expressions de table communes:

WITH MyCTE(MaxPKID, SomeColumn1)
AS(
SELECT MAX(a.MyTablePKID) AS MaxPKID, a.SomeColumn1
FROM MyTable1 a
GROUP BY a.SomeColumn1
  )
SELECT b.MyTablePKID, b.SomeColumn1, b.SomeColumn2 MAX(b.NumEstado)
FROM MyTable1 b
INNER JOIN MyCTE c ON c.MaxPKID = b.MyTablePKID
GROUP BY b.MyTablePKID, b.SomeColumn1, b.SomeColumn2

--Note: MyTablePKID is the PrimaryKey of MyTable
Marvin
la source
1

Dans Oracle ci-dessous, la requête peut donner le résultat souhaité.

SELECT group,person,Age,
  ROWNUMBER() OVER (PARTITION BY group ORDER BY age desc ,person asc) as rankForEachGroup
  FROM tablename where rankForEachGroup=1
Kiruba
la source
0
with CTE as 
(select Person, 
[Group], Age, RN= Row_Number() 
over(partition by [Group] 
order by Age desc) 
from yourtable)`


`select Person, Age from CTE where RN = 1`
Harshad
la source
0

Vous pouvez aussi essayer

SELECT * FROM mytable WHERE age IN (SELECT MAX(age) FROM mytable GROUP BY `Group`) ;
Ritwik
la source
1
Merci, bien que cela renvoie plusieurs enregistrements pour un âge où il y a égalité
Yarin
En outre, cette requête serait incorrecte dans le cas où il y a un homme de 39 ans dans le groupe 1. Dans ce cas, cette personne serait également sélectionnée, même si l'âge maximum dans le groupe 1 est plus élevé.
Joshua Richardson
0

Je n'utiliserais pas Group comme nom de colonne car c'est un mot réservé. Cependant, suivre SQL fonctionnerait.

SELECT a.Person, a.Group, a.Age FROM [TABLE_NAME] a
INNER JOIN 
(
  SELECT `Group`, MAX(Age) AS oldest FROM [TABLE_NAME] 
  GROUP BY `Group`
) b ON a.Group = b.Group AND a.Age = b.oldest
Bae Cheol Shin
la source
Merci, bien que cela renvoie plusieurs enregistrements pour un âge où il y a égalité
Yarin
@Yarin comment déciderait quelle est la bonne personne âgée? Les réponses multiples semblent être la bonne réponse, sinon utilisez la limite et l'ordre
Duncan
0

Cette méthode a l'avantage de vous permettre de classer par une colonne différente et de ne pas jeter les autres données. C'est très utile dans une situation où vous essayez de répertorier les commandes avec une colonne d'articles, en répertoriant les plus lourdes en premier.

Source: http://dev.mysql.com/doc/refman/5.0/en/group-by-functions.html#function_group-concat

SELECT person, group,
    GROUP_CONCAT(
        DISTINCT age
        ORDER BY age DESC SEPARATOR ', follow up: '
    )
FROM sql_table
GROUP BY group;
Ray Foss
la source
0

que le nom de la table soit des personnes

select O.*              -- > O for oldest table
from people O , people T
where O.grp = T.grp and 
O.Age = 
(select max(T.age) from people T where O.grp = T.grp
  group by T.grp)
group by O.grp; 
user3475425
la source
0

Si l'ID (et tous les coulmns) est nécessaire à partir de mytable

SELECT
    *
FROM
    mytable
WHERE
    id NOT IN (
        SELECT
            A.id
        FROM
            mytable AS A
        JOIN mytable AS B ON A. GROUP = B. GROUP
        AND A.age < B.age
    )
mayank kumar
la source
0

Voici comment j'obtiens les N max lignes par groupe dans mysql

SELECT co.id, co.person, co.country
FROM person co
WHERE (
SELECT COUNT(*)
FROM person ci
WHERE  co.country = ci.country AND co.id < ci.id
) < 1
;

Comment ça fonctionne:

  • se joindre à la table
  • les groupes se font par co.country = ci.country
  • N éléments par groupe sont contrôlés par ) < 1 donc pour 3 éléments -) <3
  • pour obtenir max ou min dépend de: co.id < ci.id
    • co.id <ci.id - max
    • co.id> ci.id - min

Exemple complet ici:

mysql sélectionner n valeurs max par groupe

Vanko
la source