web-dev-qa-db-fra.com

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

Comment obtenez-vous les lignes contenant 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 créer l'exemple le plus simple possible:

Avec un tableau comme celui ci-dessous, avec les colonnes personne, groupe et âge, comment voulez-vous obtenir la personne la plus âgée de 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  

Résultat souhaité: 

Shawn | 1     | 42    
Laura | 2     | 39  
177
Yarin

Il existe un moyen très simple de faire cela 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é à pas agréger des colonnes non groupées, auquel cas mysql ne fait que renvoyer la première ligne. La solution consiste à classer d'abord les données de manière à ce que la rangée souhaitée soit la première pour chaque groupe, puis à les regrouper en fonction des colonnes pour lesquelles vous souhaitez obtenir la valeur.

Vous évitez les sous-requêtes compliquées qui tentent de trouver la 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).

Note: Ceci est une solution mysql uniquement. Toutes les autres bases de données que je connais généreront 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. Étant donné que cette solution utilise le comportement non documenté, les plus prudents voudront peut-être inclure un test pour affirmer qu'ils fonctionnent reste si une version ultérieure de MySQL change ce comportement.

Mise à jour de la version 5.7:

Depuis la version 5.7, le paramètre sql-mode inclut ONLY_FULL_GROUP_BY par défaut; pour que cela fonctionne, vous devez pas disposer de cette option (modifiez le fichier d'options permettant au serveur de supprimer ce paramètre). .

125
Bohemian

La solution correcte 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 marche:

Il fait correspondre chaque ligne de o avec toutes les lignes de b ayant la même valeur dans la colonne Group et une valeur supérieure dans la colonne Age. Toute ligne de o n'ayant pas la valeur maximale de son groupe dans la colonne Age correspondra à une ou plusieurs lignes de b.

Le LEFT JOIN fait correspondre la personne la plus âgée du groupe (y compris les personnes seules dans leur groupe) à une rangée pleine de NULLs de b ('aucun âge maximum dans le groupe').
Utiliser INNER JOIN rend ces lignes non concordantes et elles sont ignorées.

La clause WHERE 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 supplémentaires

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

235
axiac

Vous pouvez rejoindre une sous-requête qui tire les 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;
31
Michael Berkowski

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 plates-formes.

Dans PostgreSQL, vous pouvez utiliser DISTINCT ON clause:

SELECT DISTINCT ON ("group") * FROM "mytable" ORDER BY "group", "age" DESC;
27
Igor Kulagin

En utilisant 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
3
sel

Pas sûr que MySQL ait 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;
2
user130268

la solution d'Axiac est ce qui a le mieux fonctionné pour moi au final. J'avais cependant une complexité supplémentaire: une "valeur maximale" 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 personnes qui sont également âgées, 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! J'imagine qu'il devrait y avoir une meilleure façon de faire cela cependant ...

2
Arthur C

Ma solution ne fonctionne que si vous n'avez besoin que de récupérer une colonne. Toutefois, pour mes besoins, c'était la meilleure solution en termes de performances (elle utilise 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 concaténation ordonnée, puis une sous-chaîne uniquement pour la première.

1
Antonio Giovanazzi

Utilisation de 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
1
Marvin

Je n'utiliserais pas Groupe comme nom de colonne puisqu'il s'agit d'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
0
Bae Cheol Shin

Voici comment je reçois les N lignes maximum 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 marche:

  • auto rejoindre à la table
  • les groupes sont faits 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électionne n valeurs max par groupe

0
Vanko
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`
0
Harshad

Si un ID (et toutes les colonnes) 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
    )
0
mayank kumar

Cette méthode présente l'avantage de vous permettre de classer dans une colonne différente et de ne pas supprimer les autres données. C'est très utile lorsque vous essayez de lister les commandes avec une colonne pour les articles, en commençant par le plus lourd.

Source: http://dev.mysql.com/doc/refman/5.0/fr/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;
0
Ray Foss

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
0
Khalid Musa Sagar

laisser le nom de la table être des gens

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; 
0
user3475425

Vous pouvez aussi essayer

SELECT * FROM mytable WHERE age IN (SELECT MAX(age) FROM mytable GROUP BY `Group`) ;
0
Ritwik