web-dev-qa-db-fra.com

Obtenez les n premiers enregistrements pour chaque groupe de résultats groupés

Voici l'exemple le plus simple possible, même si toute solution doit pouvoir s'adapter à tous les n résultats souhaités:

Avec un tableau comme celui ci-dessous, avec les colonnes personne, groupe et âge, comment voulez-vous obtenir les 2 personnes les plus âgées de chaque groupe? _ (Les liens au sein des groupes ne devraient pas donner plus de résultats, mais donner les 2 premiers ordre alphabétique)

 + -------- + ------- + ----- + 
 | Personne | Groupe | Âge | 
 + -------- + ------- + ----- + 
 | Bob | 1 | 32 | 
 | Jill | 1 | 34 | 
 | Shawn | 1 | 42 | 
 | Jake | 2 | 29 | 
 | Paul | 2 | 36 | 
 | Laura | 2 | 39 | 
 + -------- + ------- + ----- + 

Résultat souhaité:

 + -------- + ------- + ----- + 
 | Shawn | 1 | 42 | 
 | Jill | 1 | 34 | 
 | Laura | 2 | 39 | 
 | Paul | 2 | 36 | 
 + -------- + ------- + ----- + 

NOTE: Cette question est basée sur une précédente- Obtenir les enregistrements avec une valeur maximale pour chaque groupe de résultats SQL groupés - pour obtenir une seule ligne du haut de chaque groupe et qui a reçu un excellent résultat MySQL- réponse spécifique de @ Bohemian:

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

J'adorerais pouvoir me baser dessus, mais je ne vois pas comment.

123
Yarin

Voici un moyen de le faire, en utilisant UNION ALL (voir SQL Fiddle avec Demo ). Cela fonctionne avec deux groupes, si vous avez plus de deux groupes, vous devrez alors spécifier le nombre group et ajouter des requêtes pour chaque group:

(
  select *
  from mytable 
  where `group` = 1
  order by age desc
  LIMIT 2
)
UNION ALL
(
  select *
  from mytable 
  where `group` = 2
  order by age desc
  LIMIT 2
)

Il existe différentes façons de procéder. Consultez cet article pour déterminer le meilleur itinéraire pour votre situation:

http://www.xaprb.com/blog/2006/12/07/how-to-select-the-firstleastmax-row-per-group-in-sql/

Modifier:

Cela peut également fonctionner pour vous, cela génère un numéro de ligne pour chaque enregistrement. En utilisant un exemple tiré du lien ci-dessus, seuls les enregistrements dont le numéro de ligne est inférieur ou égal à 2 sont renvoyés:

select person, `group`, age
from 
(
   select person, `group`, age,
      (@num:=if(@group = `group`, @num +1, if(@group := `group`, 1, 1))) row_number 
  from test t
  CROSS JOIN (select @num:=0, @group:=null) c
  order by `Group`, Age desc, person
) as x 
where x.row_number <= 2;

Voir Démo

81
Taryn

Dans d'autres bases de données, vous pouvez le faire en utilisant ROW_NUMBER. MySQL ne supporte pas ROW_NUMBER mais vous pouvez utiliser des variables pour l'émuler:

SELECT
    person,
    groupname,
    age
FROM
(
    SELECT
        person,
        groupname,
        age,
        @rn := IF(@prev = groupname, @rn + 1, 1) AS rn,
        @prev := groupname
    FROM mytable
    JOIN (SELECT @prev := NULL, @rn := 0) AS vars
    ORDER BY groupname, age DESC, person
) AS T1
WHERE rn <= 2

Voyez le travail en ligne: sqlfiddle


Edit Je viens de remarquer que bluefeet a posté une réponse très similaire: +1 pour lui. Cependant, cette réponse présente deux petits avantages:

  1. C'est une requête unique. Les variables sont initialisées dans l’instruction SELECT.
  2. Il traite les liens comme décrit dans la question (ordre alphabétique par nom).

Je vais donc le laisser ici au cas où cela pourrait aider quelqu'un.

52
Mark Byers

Essaye ça:

SELECT a.person, a.group, a.age FROM person AS a WHERE 
(SELECT COUNT(*) FROM person AS b 
WHERE b.group = a.group AND b.age >= a.age) <= 2 
ORDER BY a.group ASC, a.age DESC

D&EACUTE;MO

34
snuffn

Que diriez-vous d'utiliser l'auto-adhésion:

CREATE TABLE mytable (person, groupname, age);
INSERT INTO mytable VALUES('Bob',1,32);
INSERT INTO mytable VALUES('Jill',1,34);
INSERT INTO mytable VALUES('Shawn',1,42);
INSERT INTO mytable VALUES('Jake',2,29);
INSERT INTO mytable VALUES('Paul',2,36);
INSERT INTO mytable VALUES('Laura',2,39);

SELECT a.* FROM mytable AS a
  LEFT JOIN mytable AS a2 
    ON a.groupname = a2.groupname AND a.age <= a2.age
GROUP BY a.person
HAVING COUNT(*) <= 2
ORDER BY a.groupname, a.age DESC;

donne moi:

a.person    a.groupname  a.age     
----------  -----------  ----------
Shawn       1            42        
Jill        1            34        
Laura       2            39        
Paul        2            36      

La réponse de Bill Karwin à Sélectionne les 10 meilleurs disques de chaque catégorie

De plus, j'utilise SQLite, mais cela devrait fonctionner sous MySQL.

Autre chose: dans ce qui précède, j'ai remplacé la colonne group par une colonne groupname pour plus de commodité.

Modifier:

Pour donner suite au commentaire du PO concernant les résultats manquants, j'ai incrémenté la réponse de snuffin pour montrer tous les liens. Cela signifie que si les derniers sont des liens, plus de 2 lignes peuvent être retournées, comme indiqué ci-dessous:

.headers on
.mode column

CREATE TABLE foo (person, groupname, age);
INSERT INTO foo VALUES('Paul',2,36);
INSERT INTO foo VALUES('Laura',2,39);
INSERT INTO foo VALUES('Joe',2,36);
INSERT INTO foo VALUES('Bob',1,32);
INSERT INTO foo VALUES('Jill',1,34);
INSERT INTO foo VALUES('Shawn',1,42);
INSERT INTO foo VALUES('Jake',2,29);
INSERT INTO foo VALUES('James',2,15);
INSERT INTO foo VALUES('Fred',1,12);
INSERT INTO foo VALUES('Chuck',3,112);


SELECT a.person, a.groupname, a.age 
FROM foo AS a 
WHERE a.age >= (SELECT MIN(b.age)
                FROM foo AS b 
                WHERE (SELECT COUNT(*)
                       FROM foo AS c
                       WHERE c.groupname = b.groupname AND c.age >= b.age) <= 2
                GROUP BY b.groupname)
ORDER BY a.groupname ASC, a.age DESC;

donne moi:

person      groupname   age       
----------  ----------  ----------
Shawn       1           42        
Jill        1           34        
Laura       2           39        
Paul        2           36        
Joe         2           36        
Chuck       3           112      
30
user610650

Regarde ça:

SELECT
  p.Person,
  p.`Group`,
  p.Age
FROM
  people p
  INNER JOIN
  (
    SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`
    UNION
    SELECT MAX(p3.Age) AS Age, p3.`Group` FROM people p3 INNER JOIN (SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`) p4 ON p3.Age < p4.Age AND p3.`Group` = p4.`Group` GROUP BY `Group`
  ) p2 ON p.Age = p2.Age AND p.`Group` = p2.`Group`
ORDER BY
  `Group`,
  Age DESC,
  Person;

Fiddle SQL: http://sqlfiddle.com/#!2/cdbb6/15

10
Travesty3

La solution Snuffin semble assez lente à exécuter lorsque vous avez beaucoup de lignes et que Mark Byers/Rick James et les solutions Bluefeet ne fonctionnent pas sur mon environnement (MySQL 5.6) car order by est appliqué après l'exécution de select, voici donc une variante solutions de Marc Byers/Rick James pour résoudre ce problème (avec une sélection extra imbriquée):

select person, groupname, age
from
(
    select person, groupname, age,
    (@rn:=if(@prev = groupname, @rn +1, 1)) as rownumb,
    @prev:= groupname 
    from 
    (
        select person, groupname, age
        from persons 
        order by groupname ,  age desc, person
    )   as sortedlist
    JOIN (select @prev:=NULL, @rn :=0) as vars
) as groupedlist 
where rownumb<=2
order by groupname ,  age desc, person;

J'ai essayé une requête similaire sur une table ayant 5 millions de lignes et elle renvoie le résultat en moins de 3 secondes

7
Laurent PELE

Si les autres réponses ne sont pas assez rapides, donnez ce code a essayez:

SELECT
        province, n, city, population
    FROM
      ( SELECT  @prev := '', @n := 0 ) init
    JOIN
      ( SELECT  @n := if(province != @prev, 1, @n + 1) AS n,
                @prev := province,
                province, city, population
            FROM  Canada
            ORDER BY
                province   ASC,
                population DESC
      ) x
    WHERE  n <= 3
    ORDER BY  province, n;

Sortie:

+---------------------------+------+------------------+------------+
| province                  | n    | city             | population |
+---------------------------+------+------------------+------------+
| Alberta                   |    1 | Calgary          |     968475 |
| Alberta                   |    2 | Edmonton         |     822319 |
| Alberta                   |    3 | Red Deer         |      73595 |
| British Columbia          |    1 | Vancouver        |    1837970 |
| British Columbia          |    2 | Victoria         |     289625 |
| British Columbia          |    3 | Abbotsford       |     151685 |
| Manitoba                  |    1 | ...
5
Rick James

Je voulais partager cela parce que j'ai passé beaucoup de temps à chercher un moyen simple de l'implémenter dans un programme Java sur lequel je travaille. Cela ne donne pas tout à fait la sortie que vous recherchez mais sa fin. La fonction dans mysql appelée GROUP_CONCAT() fonctionnait très bien pour spécifier le nombre de résultats à renvoyer dans chaque groupe. L'utilisation de LIMIT ou de l'un des autres moyens sophistiqués d'essayer de le faire avec COUNT n'a pas fonctionné pour moi. Donc, si vous êtes prêt à accepter une sortie modifiée, c'est une excellente solution. Disons que j'ai une table appelée 'student' avec les identifiants d'étudiant, leur sexe et leur gpa. Disons que je veux top 5 gpas pour chaque sexe. Ensuite, je peux écrire la requête comme ceci

SELECT sex, SUBSTRING_INDEX(GROUP_CONCAT(cast(gpa AS char ) ORDER BY gpa desc), ',',5) 
AS subcategories FROM student GROUP BY sex;

Notez que le paramètre '5' indique le nombre d'entrées à concaténer dans chaque ligne.

Et la sortie ressemblerait à quelque chose comme 

+--------+----------------+
| Male   | 4,4,4,4,3.9    |
| Female | 4,4,3.9,3.9,3.8|
+--------+----------------+

Vous pouvez également modifier la variable ORDER BY et les classer différemment. Donc, si j'avais l'âge de l'étudiant, je pourrais remplacer le "gpa desc" par "age desc" et cela fonctionnera! Vous pouvez également ajouter des variables à l'instruction group by pour obtenir plus de colonnes dans la sortie. Donc, c’est une des solutions que j’ai trouvées, qui est assez flexible et qui fonctionne bien si vous n’êtes pas mal à lister les résultats. 

2
Jon Bown

Dans SQL Server, row_numer() est une fonction puissante qui permet d’obtenir facilement les résultats décrits ci-dessous. 

select Person,[group],age
from
(
select * ,row_number() over(partition by [group] order by age desc) rn
from mytable
) t
where rn <= 2
0
Prakash