web-dev-qa-db-fra.com

SQL sélectionne uniquement les lignes avec la valeur maximale sur une colonne

J'ai ce tableau pour les documents (version simplifiée ici):

+------+-------+--------------------------------------+
| id   | rev   | content                              |
+------+-------+--------------------------------------+
| 1    | 1     | ...                                  |
| 2    | 1     | ...                                  |
| 1    | 2     | ...                                  |
| 1    | 3     | ...                                  |
+------+-------+--------------------------------------+

Comment sélectionner une ligne par identifiant et uniquement le plus grand nombre de tours?
Avec les données ci-dessus, le résultat doit contenir deux lignes: [1, 3, ...] et [2, 1, ..]. J'utilise MySQL.

Actuellement, j'utilise des contrôles dans la boucle while pour détecter et écraser les anciennes révs du jeu de résultats. Mais est-ce la seule méthode pour obtenir le résultat? N'y a-t-il pas une solution SQL?

Mettre à jour
Comme le suggèrent les réponses, il y a est une solution SQL et ici, une démonstration de sqlfiddle

Mise à jour 2
J'ai remarqué qu'après avoir ajouté le sqlfiddle ci-dessus, le taux de vote favorable de la question a dépassé le taux de vote positif des réponses. Cela n'a pas été l'intention! Le violon est basé sur les réponses, en particulier sur la réponse acceptée.

994
Majid Fouladpour

À première vue ...

Tout ce dont vous avez besoin est une clause GROUP BY avec la fonction d'agrégation MAX:

SELECT id, MAX(rev)
FROM YourTable
GROUP BY id

Ce n'est jamais aussi simple, n'est-ce pas?

Je viens de remarquer que vous avez également besoin de la colonne content.

C'est une question très courante en SQL: trouver toutes les données de la ligne avec une valeur maximale dans une colonne par identificateur de groupe. J'ai beaucoup entendu parler de ça au cours de ma carrière. En fait, c’était l’une des questions auxquelles j’ai répondu lors de l’entretien technique de mon emploi actuel. 

En fait, il est si courant que la communauté StackOverflow ait créé une seule étiquette pour traiter des questions comme celle-ci: greatest-n-per-group .

En gros, vous avez deux approches pour résoudre ce problème:

Jointure avec une simple requête group-identifier, max-value-in-group

Dans cette approche, vous trouvez d’abord le group-identifier, max-value-in-group (déjà résolu ci-dessus) dans une sous-requête. Ensuite, vous joignez votre table à la sous-requête avec une égalité à la fois sur group-identifier et max-value-in-group:

SELECT a.id, a.rev, a.contents
FROM YourTable a
INNER JOIN (
    SELECT id, MAX(rev) rev
    FROM YourTable
    GROUP BY id
) b ON a.id = b.id AND a.rev = b.rev

Left Joining with self, ajustement des conditions de jointure et des filtres

Dans cette approche, vous avez quitté rejoindre la table avec lui-même. L'égalité, bien sûr, va dans le group-identifier. Ensuite, 2 mouvements intelligents: 

  1. La deuxième condition de jointure a une valeur du côté gauche inférieure à la valeur de droite
  2. Lorsque vous effectuez l'étape 1, la ou les lignes qui ont réellement la valeur maximale auront NULL dans la partie droite (c'est un LEFT JOIN, vous vous souvenez?). Ensuite, nous filtrons le résultat joint en affichant uniquement les lignes où le côté droit est NULL.

Donc vous vous retrouvez avec:

SELECT a.*
FROM YourTable a
LEFT OUTER JOIN YourTable b
    ON a.id = b.id AND a.rev < b.rev
WHERE b.id IS NULL;

Conclusion

Les deux approches donnent exactement le même résultat. 

Si vous avez deux lignes avec max-value-in-group pour group-identifier, les deux lignes seront dans le résultat dans les deux approches.

Les deux approches sont compatibles SQL ANSI et fonctionneront donc avec votre SGBDR préféré, quelle que soit sa "saveur".

Les deux approches favorisent également les performances, mais votre kilométrage peut varier (SGBDR, structure de base de données, index, etc.). Donc, lorsque vous choisissez une approche plutôt que l'autre, benchmark. Et assurez-vous de choisir celui qui vous convient le mieux.

1579
Adrian Carneiro

Ma préférence est d'utiliser le moins de code possible ...

Vous pouvez le faire en utilisant IN Essayez ceci:

SELECT * 
FROM t1 WHERE (id,rev) IN 
( SELECT id, MAX(rev)
  FROM t1
  GROUP BY id
)

à mon avis, c'est moins compliqué ... plus facile à lire et à entretenir.

204
Kevin Burton

Une autre solution consiste à utiliser une sous-requête corrélée:

select yt.id, yt.rev, yt.contents
    from YourTable yt
    where rev = 
        (select max(rev) from YourTable st where yt.id=st.id)

Avoir un index sur (id, rev) rend la sous-requête presque comme une simple recherche ...

Voici des comparaisons avec les solutions de la réponse de @ AdrianCarneiro (sous-requête, leftjoin), basées sur des mesures MySQL avec une table InnoDB d'environ 1 million d'enregistrements, la taille du groupe étant: 1-3.

Alors que pour les balayages de table complets, les sous-requêtes/leftjoin/corrélations se rapportent les 6/8/9, lorsqu'il s'agit de recherches directes ou de batch (id in (1,2,3)), la sous-requête est beaucoup plus lente que les autres (en raison de la réexécution de la sous-requête). Cependant, je ne pouvais pas faire la différence entre les solutions de gauche et corrélées en termes de rapidité.

Une dernière note, comme leftjoin crée n * (n + 1)/2 jointures dans des groupes, ses performances peuvent être fortement affectées par la taille des groupes ...

66
Vajk Hermecz

Je suis abasourdi par le fait qu'aucune réponse ne soit proposée dans la solution de la fonction de fenêtre SQL:

SELECT a.id, a.rev, a.contents
  FROM (SELECT id, rev, contents,
               ROW_NUMBER() OVER (PARTITION BY id ORDER BY rev DESC) rank
          FROM YourTable) a
 WHERE a.rank = 1 

Ajoutées dans la norme SQL ANSI/ISO Standard SQL: 2003 et ultérieurement étendue à la norme ANSI/ISO Standard SQL: 2008, les fonctions de fenêtre (ou de fenêtrage) sont désormais disponibles chez tous les principaux fournisseurs. Il existe plus de types de fonctions de classement disponibles pour traiter un problème d'égalité: RANK, DENSE_RANK, PERSENT_RANK.

54
topchef

Je ne peux pas garantir les performances, mais voici une astuce inspirée par les limitations de Microsoft Excel. Il a quelques bonnes caractéristiques

BON PRODUIT

  • Il devrait forcer le retour d'un seul "max record" même s'il y a une égalité (parfois utile)
  • Il ne nécessite pas de rejoindre

APPROCHE

C'est un peu moche et nécessite que vous sachiez quelque chose sur la plage de valeurs valides de la colonnerev. Supposons que nous savons que la colonnerevest un nombre compris entre 0,00 et 999, décimales comprises, mais qu’il n’y aura jamais que deux chiffres à droite du séparateur décimal (par exemple, 34.17 serait une valeur valide ).

En résumé, vous créez une colonne synthétique unique en concaténant/encapsidant le champ de comparaison principal avec les données souhaitées. De cette manière, vous pouvez forcer la fonction d'agrégation MAX () de SQL à renvoyer toutes les données (car elles ont été regroupées dans une seule colonne). Ensuite, vous devez décompresser les données.

Voici à quoi ça ressemble avec l'exemple ci-dessus, écrit en SQL

SELECT id, 
       CAST(SUBSTRING(max(packed_col) FROM 2 FOR 6) AS float) as max_rev,
       SUBSTRING(max(packed_col) FROM 11) AS content_for_max_rev 
FROM  (SELECT id, 
       CAST(1000 + rev + .001 as CHAR) || '---' || CAST(content AS char) AS packed_col
       FROM yourtable
      ) 
GROUP BY id

Le compactage commence par forcer la colonnerevà être un nombre de longueurs de caractères connues quelle que soit la valeur derevafin que, par exemple,

  • 3.2 devient 1003.201
  • 57 devient 1057.001
  • 923.88 devient 1923.881

Si vous le faites bien, la comparaison de chaînes de deux nombres devrait donner le même "max" que la comparaison numérique des deux nombres et il est facile de reconvertir le nombre en utilisant la fonction de sous-chaîne (disponible sous une forme ou une autre partout).

44
David Foster

Je pense que c'est la solution la plus simple:

SELECT *
FROM
    (SELECT *
    FROM Employee
    ORDER BY Salary DESC)
AS employeesub
GROUP BY employeesub.Salary;
  • SELECT *: Renvoie tous les champs.
  • FROM Employee: Table recherchée sur.
  • (SELECT * ...) sous-requête: Renvoie toutes les personnes, triées par Salaire.
  • GROUP BY employeesub.Salary:: force le résultat renvoyé à la ligne Salaire triée en premier de chaque employé.

Si vous n'avez besoin que d'une seule ligne, c'est encore plus simple:

SELECT *
FROM Employee
ORDER BY Employee.Salary DESC
LIMIT 1

Je pense également qu'il est le plus facile de décomposer, de comprendre et de modifier d'autres objectifs:

  • ORDER BY Employee.Salary DESC: Triez les résultats en fonction du salaire, en commençant par les salaires les plus élevés.
  • LIMITE 1: Renvoie un seul résultat.

Comprendre cette approche, résoudre l'un de ces problèmes similaires devient trivial: recruter l'employé avec le salaire le plus bas (changer DESC en ASC), obtenir le top 10 des employés gagnant (changer LIMIT 1 en LIMIT 10), trier à l'aide d'un autre champ (modifier Employee.Salary to ORDER BY Employee.Commission), etc.

25
HoldOffHunger

Quelque chose comme ça?

SELECT yourtable.id, rev, content
FROM yourtable
INNER JOIN (
    SELECT id, max(rev) as maxrev FROM yourtable
    WHERE yourtable
    GROUP BY id
) AS child ON (yourtable.id = child.id) AND (yourtable.rev = maxrev)
18
Marc B

Comme il s’agit de la question la plus populaire concernant ce problème, je vais publier une autre réponse ici aussi:

Il semble y avoir un moyen plus simple de faire cela (mais uniquement dans MySQL):

select *
from (select * from mytable order by id, rev desc ) x
group by id

Merci de citer la réponse de l'utilisateur Bohemian dans cette question pour avoir fourni une réponse aussi concise et élégante à ce problème.

EDIT: bien que cette solution fonctionne pour de nombreuses personnes, elle risque de ne pas être stable à long terme, car MySQL ne garantit pas que l'instruction GROUP BY renvoie des valeurs significatives pour les colonnes ne figurant pas dans la liste GROUP BY. Alors utilisez cette solution à vos risques et périls

6
Yura

J'aime utiliser une solution basée sur NOT EXIST pour résoudre ce problème:

SELECT id, rev
FROM YourTable t
WHERE NOT EXISTS (
   SELECT * FROM YourTable t WHERE t.id = id AND rev > t.rev
)
6
Bulat

Une troisième solution que je vois rarement mentionnée est spécifique à MySQL et ressemble à ceci:

SELECT id, MAX(rev) AS rev
 , 0+SUBSTRING_INDEX(GROUP_CONCAT(numeric_content ORDER BY rev DESC), ',', 1) AS numeric_content
FROM t1
GROUP BY id

Oui, c'est affreux (conversion en chaîne, etc.), mais d'après mon expérience, c'est généralement plus rapide que les autres solutions. Peut-être que ce n'est que pour mes cas d'utilisation, mais je l'ai utilisé sur des tables avec des millions d'enregistrements et de nombreux identifiants uniques. Peut-être est-ce dû au fait que MySQL optimise assez mal les autres solutions (du moins dans les 5.0 jours où j'ai proposé cette solution).

Une chose importante est que GROUP_CONCAT a une longueur maximale pour la chaîne qu'il peut construire. Vous voudrez probablement augmenter cette limite en définissant la variable group_concat_max_len. Et gardez à l’esprit que la mise à l’échelle sera limitée si vous avez un grand nombre de lignes.

Quoi qu'il en soit, ce qui précède ne fonctionne pas directement si votre champ de contenu est déjà du texte. Dans ce cas, vous voudrez probablement utiliser un séparateur différent, comme\0 peut-être. Vous rencontrerez également la limite group_concat_max_len plus rapidement.

5
Jannes

Je pense que tu veux ça?

select * from docs where (id, rev) IN (select id, max(rev) as rev from docs group by id order by id)  

SQL Fiddle: À vérifier ici

4
Abhishek Rana

Si vous avez plusieurs champs dans l'instruction select et que vous souhaitez obtenir la dernière valeur pour tous ces champs via un code optimisé:

select * from
(select * from table_name
order by id,rev desc) temp
group by id 
4
seahawk

NOT mySQL, mais pour les autres personnes trouvant cette question et utilisant SQL, un autre moyen de résoudre le problème le plus grand nombre par groupe consiste à utiliser Cross Apply dans MS SQL.

WITH DocIds AS (SELECT DISTINCT id FROM docs)

SELECT d2.id, d2.rev, d2.content
FROM DocIds d1
CROSS APPLY (
  SELECT Top 1 * FROM docs d
  WHERE d.id = d1.id
  ORDER BY rev DESC
) d2

_ { Voici un exemple dans SqlFiddle } _

4
KyleMit

Une autre façon de faire le travail consiste à utiliser la fonction analytique MAX() dans la clause OVER PARTITION

SELECT t.*
  FROM
    (
    SELECT id
          ,rev
          ,contents
          ,MAX(rev) OVER (PARTITION BY id) as max_rev
      FROM YourTable
    ) t
  WHERE t.rev = t.max_rev 

L’autre solution ROW_NUMBER() OVER PARTITION déjà décrite dans ce message est

SELECT t.*
  FROM
    (
    SELECT id
          ,rev
          ,contents
          ,ROW_NUMBER() OVER (PARTITION BY id ORDER BY rev DESC) rank
      FROM YourTable
    ) t
  WHERE t.rank = 1 

Ce 2 SELECT fonctionne bien sur Oracle 10g.

MAX () solution fonctionne certainement PLUS RAPIDEMENT que ROW_NUMBER() solution parce que MAX() complexité est O(n) tandis que ROW_NUMBER() complexité est au minimum O(n.log(n))n représente le nombre d'enregistrements dans la table!

3
schlebe

Je voudrais utiliser ceci:

select t.*
from test as t
join
   (select max(rev) as rev
    from test
    group by id) as o
on o.rev = t.rev

La sous-requête SELECT n'est peut-être pas trop efficace, mais la clause JOIN semble utilisable. Je ne suis pas un expert en optimisation de requêtes, mais j'ai déjà essayé avec MySQL, PostgreSQL, FireBird et cela fonctionne très bien.

Vous pouvez utiliser ce schéma dans plusieurs jointures et avec la clause WHERE. Voici mon exemple de travail (résoudre le même problème que le vôtre avec le tableau "firmy"):

select *
from platnosci as p
join firmy as f
on p.id_rel_firmy = f.id_rel
join (select max(id_obj) as id_obj
      from firmy
      group by id_rel) as o
on o.id_obj = f.id_obj and p.od > '2014-03-01'

Il est posé sur des tables ayant des dizaines d’adolescents, et il faut moins de 0,01 seconde sur une machine vraiment pas trop forte.

Je ne voudrais pas utiliser la clause IN (comme il est mentionné quelque part ci-dessus). IN est donné à utiliser avec des listes courtes de constantes, et non comme un filtre de requête construit sur une sous-requête. C’est parce que la sous-requête dans IN est exécutée pour chaque enregistrement analysé, ce qui peut rendre la requête très longue.

3
Marek Wysmułek

Que dis-tu de ça:

SELECT all_fields.*  
FROM (SELECT id, MAX(rev) FROM yourtable GROUP BY id) AS max_recs  
LEFT OUTER JOIN yourtable AS all_fields 
ON max_recs.id = all_fields.id
3
inor
SELECT *
FROM Employee
where Employee.Salary in (select max(salary) from Employee group by Employe_id)
ORDER BY Employee.Salary
3
guru008

Aucune de ces réponses n'a fonctionné pour moi.

C'est ce qui a fonctionné pour moi.

with score as (select max(score_up) from history)
select history.* from score, history where history.score_up = score.max
2
qaisjp

Trié le champ rev en ordre inverse, puis groupé par id, ce qui a donné la première ligne de chaque groupe, celle avec la valeur rev la plus élevée.

SELECT * FROM (SELECT * FROM table1 ORDER BY id, rev DESC) X GROUP BY X.id;

Testé dans http://sqlfiddle.com/ avec les données suivantes

CREATE TABLE table1
    (`id` int, `rev` int, `content` varchar(11));

INSERT INTO table1
    (`id`, `rev`, `content`)
VALUES
    (1, 1, 'One-One'),
    (1, 2, 'One-Two'),
    (2, 1, 'Two-One'),
    (2, 2, 'Two-Two'),
    (3, 2, 'Three-Two'),
    (3, 1, 'Three-One'),
    (3, 3, 'Three-Three')
;

Cela a donné le résultat suivant dans MySql 5.5 et 5.6 

id  rev content
1   2   One-Two
2   2   Two-Two
3   3   Three-Two
2
blokeish

Voici une bonne façon de le faire

Utilisez le code suivant:

with temp as  ( 
select count(field1) as summ , field1
from table_name
group by field1 )
select * from temp where summ = (select max(summ) from temp)
2
shay

J'aime faire cela en classant les enregistrements par colonne. Dans ce cas, rangez les valeurs rev groupées par id. Les personnes dont la variable rev est élevée auront un classement inférieur. Donc, la plus haute rev aura un rang de 1.

select id, rev, content
from
 (select
    @rowNum := if(@prevValue = id, @rowNum+1, 1) as row_num,
    id, rev, content,
    @prevValue := id
  from
   (select id, rev, content from YOURTABLE order by id asc, rev desc) TEMP,
   (select @rowNum := 1 from DUAL) X,
   (select @prevValue := -1 from DUAL) Y) TEMP
where row_num = 1;

Pas sûr que l'introduction de variables ralentisse le tout. Mais au moins, je ne demande pas YOURTABLE deux fois.

2
user5124980

Voici une autre solution pour récupérer les enregistrements uniquement avec un champ ayant la valeur maximale pour ce champ. Cela fonctionne pour SQL400, la plate-forme sur laquelle je travaille. Dans cet exemple, les enregistrements avec la valeur maximale dans la zone FIELD5 seront récupérés par l'instruction SQL suivante.

SELECT A.KEYFIELD1, A.KEYFIELD2, A.FIELD3, A.FIELD4, A.FIELD5
  FROM MYFILE A
 WHERE RRN(A) IN
   (SELECT RRN(B) 
      FROM MYFILE B
     WHERE B.KEYFIELD1 = A.KEYFIELD1 AND B.KEYFIELD2 = A.KEYFIELD2
     ORDER BY B.FIELD5 DESC
     FETCH FIRST ROW ONLY)
2
Cesar

voici une autre solution espérons que cela aidera quelqu'un 

Select a.id , a.rev, a.content from Table1 a
inner join 
(SELECT id, max(rev) rev FROM Table1 GROUP BY id) x on x.id =a.id and x.rev =a.rev
2
Abdul Samad

Cette solution n'effectue qu'une seule sélection dans YourTable, elle est donc plus rapide. Cela ne fonctionne que pour MySQL et SQLite (pour SQLite, supprimez DESC) selon le test effectué sur sqlfiddle.com. Peut-être qu’il pourrait être modifié pour travailler sur d’autres langues que je ne connais pas bien.

SELECT *
FROM ( SELECT *
       FROM ( SELECT 1 as id, 1 as rev, 'content1' as content
              UNION
              SELECT 2, 1, 'content2'
              UNION
              SELECT 1, 2, 'content3'
              UNION
              SELECT 1, 3, 'content4'
            ) as YourTable
       ORDER BY id, rev DESC
   ) as YourTable
GROUP BY id
2
plavozont

Explication

Ce n'est pas du SQL pur. Cela utilisera l'ORM SQLAlchemy.

Je suis venu ici pour demander de l'aide à SQLAlchemy. Je vais donc dupliquer la réponse d'Adrian Carneiro avec la version python/SQLAlchemy, plus précisément la jointure externe.

Cette requête répond à la question de: 

"Pouvez-vous me retourner les enregistrements de ce groupe d'enregistrements (basés sur le même identifiant) qui ont le numéro de version le plus élevé".  

Cela me permet de dupliquer l’enregistrement, de le mettre à jour, d’augmenter son numéro de version et d’obtenir la copie de l’ancienne version de manière à pouvoir montrer les changements au fil du temps.

Code

MyTableAlias = aliased(MyTable)
newest_records = appdb.session.query(MyTable).select_from(join(
    MyTable, 
    MyTableAlias, 
    onclause=and_(
        MyTable.id == MyTableAlias.id,
        MyTable.version_int < MyTableAlias.version_int
    ),
    isouter=True
    )
).filter(
    MyTableAlias.id  == None,
).all()

Testé sur une base de données PostgreSQL.

0
Ian A McElhenny

J'ai utilisé le ci-dessous pour résoudre un problème de mon cru. J'ai d'abord créé une table temporaire et inséré la valeur de rév. Max par identifiant unique.

CREATE TABLE #temp1
(
    id varchar(20)
    , rev int
)
INSERT INTO #temp1
SELECT a.id, MAX(a.rev) as rev
FROM 
    (
        SELECT id, content, SUM(rev) as rev
        FROM YourTable
        GROUP BY id, content
    ) as a 
GROUP BY a.id
ORDER BY a.id

J'ai ensuite joint ces valeurs maximales (# temp1) à toutes les combinaisons possibles id/contenu. En faisant cela, je filtre naturellement les combinaisons id/contenu non maximales, et je ne dispose plus que des valeurs maximales de rév.

SELECT a.id, a.rev, content
FROM #temp1 as a
LEFT JOIN
    (
        SELECT id, content, SUM(rev) as rev
        FROM YourTable
        GROUP BY id, content
    ) as b on a.id = b.id and a.rev = b.rev
GROUP BY a.id, a.rev, b.content
ORDER BY a.id
0
Richard Ball

Vous pouvez effectuer la sélection sans jointure en combinant les rev et id en une valeur maxRevId pour MAX(), puis en le fractionnant aux valeurs d'origine:

SELECT maxRevId & ((1 << 32) - 1) as id, maxRevId >> 32 AS rev
FROM (SELECT MAX(((rev << 32) | id)) AS maxRevId
      FROM YourTable
      GROUP BY id) x;

Ceci est particulièrement rapide lorsqu'il existe une jointure complexe au lieu d'une seule table. Avec les approches traditionnelles, la jointure complexe se ferait deux fois.

La combinaison ci-dessus est simple avec les fonctions de bits lorsque rev et id sont INT UNSIGNED (32 bits) et que la valeur combinée correspond à BIGINT UNSIGNED (64 bits). Lorsque les variables id & rev sont supérieures à 32 bits ou composées de plusieurs colonnes, vous devez combiner la valeur, par exemple. une valeur binaire avec un remplissage approprié pour MAX().

0
zovio