web-dev-qa-db-fra.com

Récupère la ligne qui a la valeur Max pour une colonne

Table: 

UserId, Value, Date.

Je veux obtenir le UserId, valeur pour le max (Date) pour chaque UserId. C'est-à-dire, la valeur pour chaque ID utilisateur ayant la dernière date. Y at-il un moyen de faire cela simplement en SQL? (Oracle de préférence)

Mise à jour: / Toutes mes excuses pour toute ambiguïté: je dois obtenir TOUS les UserIds. Mais pour chaque ID utilisateur, seule cette ligne où cet utilisateur a la date la plus récente.

521
Umang

Cela permettra d'extraire toutes les lignes pour lesquelles la valeur de la colonne my_date est égale à la valeur maximale de my_date pour cet ID utilisateur. Cela peut récupérer plusieurs lignes pour l'ID utilisateur lorsque la date maximale est sur plusieurs lignes.

select userid,
       my_date,
       ...
from
(
select userid,
       my_Date,
       ...
       max(my_date) over (partition by userid) max_my_date
from   users
)
where my_date = max_my_date

"Rock fonctions analytiques"

Edit: En ce qui concerne le premier commentaire ...

"utiliser des requêtes analytiques et une auto-jointure annule le but des requêtes analytiques"

Il n'y a pas d'auto-jointure dans ce code. Il y a plutôt un prédicat placé sur le résultat de la vue en ligne qui contient la fonction analytique - un sujet très différent et une pratique tout à fait standard.

"La fenêtre par défaut dans Oracle va de la première ligne de la partition à celle en cours"

La clause de fenêtrage n'est applicable qu'en présence de la clause order by. En l'absence de clause order by, aucune clause de fenêtrage n'est appliquée par défaut et aucune ne peut être spécifiée explicitement.

Le code fonctionne.

366
David Aldridge

Je vois que beaucoup de gens utilisent des sous-requêtes ou des fonctionnalités spécifiques à un fournisseur, mais je fais souvent ce type de requête sans sous-requêtes de la manière suivante. Il utilise SQL simple et standard, il devrait donc fonctionner dans toutes les marques de SGBDR.

SELECT t1.*
FROM mytable t1
  LEFT OUTER JOIN mytable t2
    ON (t1.UserId = t2.UserId AND t1."Date" < t2."Date")
WHERE t2.UserId IS NULL;

En d'autres termes: récupérez la ligne à partir de t1 où aucune autre ligne n'existe avec la même UserId et une date supérieure.

(J'ai mis l'identifiant "Date" dans les délimiteurs car c'est un mot réservé SQL.)

Dans le cas où t1."Date" = t2."Date", le doublage apparaît. Les tables ont généralement la touche auto_inc(seq), par exemple id. Pour éviter de doubler, vous pouvez utiliser les méthodes suivantes:

SELECT t1.*
FROM mytable t1
  LEFT OUTER JOIN mytable t2
    ON t1.UserId = t2.UserId AND ((t1."Date" < t2."Date") 
         OR (t1."Date" = t2."Date" AND t1.id < t2.id))
WHERE t2.UserId IS NULL;

Re commentaire de @Farhan:

Voici une explication plus détaillée:

Une jointure externe tente de rejoindre t1 avec t2. Par défaut, tous les résultats de t1 sont renvoyés et si il existe une correspondance dans t2, elle est également renvoyée. S'il n'y a pas de correspondance dans t2 pour une ligne donnée de t1, la requête renvoie toujours la ligne de t1 et utilise NULL comme espace réservé pour toutes les colonnes de t2. C'est comme ça que les jointures externes fonctionnent en général.

Le truc dans cette requête est de concevoir la condition de correspondance de la jointure de telle sorte que t2 doit correspondre au même userid et à un supérieur date. L'idée est que si une ligne existe dans t2 qui a une plus grande date, alors la ligne dans t1 est comparée à ne peut pas être la plus grande date pour cette userid. Mais s'il n'y a pas de correspondance - c'est-à-dire s'il n'y a pas de ligne dans t2 avec une plus grande date que la ligne dans t1 - nous savons que la ligne dans t1 était la rangée avec le plus grand date pour la userid donnée.

Dans ces cas (lorsqu'il n'y a pas de correspondance), les colonnes de t2 seront NULL - même les colonnes spécifiées dans la condition de jointure. C'est pourquoi nous utilisons WHERE t2.UserId IS NULL, car nous recherchons les cas où aucune ligne n'a été trouvée avec une plus grande date pour la userid donnée.

418
Bill Karwin
SELECT userid, MAX(value) KEEP (DENSE_RANK FIRST ORDER BY date DESC)
  FROM table
  GROUP BY userid
150
Dave Costa

Je ne connais pas vos noms de colonnes exacts, mais ce serait quelque chose comme ceci:

 Sélectionnez ID utilisateur, valeur 
 des utilisateurs u1 
 où date = (sélectionnez max (date) 
 parmi les utilisateurs u2 
 où u1.userid = u2.userid) 
47
Steve K

N'étant pas au travail, je n'ai pas Oracle à portée de main, mais il me semble rappeler qu'Oracle autorise la correspondance de plusieurs colonnes dans une clause IN, ce qui devrait au moins éviter les options utilisant une sous-requête corrélée, ce qui est rarement un bon idée.

Quelque chose comme ça, peut-être (je ne me souviens plus si la liste des colonnes doit être entre parenthèses ou non):

SELECT * 
FROM MyTable
WHERE (User, Date) IN
  ( SELECT User, MAX(Date) FROM MyTable GROUP BY User)

EDIT: Juste essayé pour de vrai:

SQL> create table MyTable (usr char(1), dt date);
SQL> insert into mytable values ('A','01-JAN-2009');
SQL> insert into mytable values ('B','01-JAN-2009');
SQL> insert into mytable values ('A', '31-DEC-2008');
SQL> insert into mytable values ('B', '31-DEC-2008');
SQL> select usr, dt from mytable
  2  where (usr, dt) in 
  3  ( select usr, max(dt) from mytable group by usr)
  4  /

U DT
- ---------
A 01-JAN-09
B 01-JAN-09

Cela fonctionne donc, bien que certains des nouveaux trucs mentionnés ailleurs puissent être plus performants.

35
Mike Woodhouse

Je sais que vous avez demandé Oracle, mais dans SQL 2005, nous utilisons maintenant ceci:


-- Single Value
;WITH ByDate
AS (
SELECT UserId, Value, ROW_NUMBER() OVER (PARTITION BY UserId ORDER BY Date DESC) RowNum
FROM UserDates
)
SELECT UserId, Value
FROM ByDate
WHERE RowNum = 1

-- Multiple values where dates match
;WITH ByDate
AS (
SELECT UserId, Value, RANK() OVER (PARTITION BY UserId ORDER BY Date DESC) Rnk
FROM UserDates
)
SELECT UserId, Value
FROM ByDate
WHERE Rnk = 1
13
mancaus

Je n'ai pas Oracle pour le tester, mais la solution la plus efficace consiste à utiliser des requêtes analytiques. Ça devrait ressembler a quelque chose comme ca:

SELECT DISTINCT
    UserId
  , MaxValue
FROM (
    SELECT UserId
      , FIRST (Value) Over (
          PARTITION BY UserId
          ORDER BY Date DESC
        ) MaxValue
    FROM SomeTable
  )

Je suppose que vous pouvez vous débarrasser de la requête externe et mettre distincte sur la requête interne, mais je ne suis pas sûr. En attendant, je sais que celui-ci fonctionne.

Si vous souhaitez en savoir plus sur les requêtes analytiques, je vous suggère de lire http://www.orafaq.com/node/55 et http://www.akadia.com/services/ora_analytic_functions.html. Voici le court résumé.

Sous le capot, les requêtes analytiques trient l'ensemble du jeu de données, puis le traitent de manière séquentielle. Au fur et à mesure que vous le traitez, vous partitionnez le jeu de données en fonction de certains critères, puis, pour chaque ligne, examine une fenêtre (la valeur par défaut de la première valeur de la partition correspond à la ligne actuelle - cette valeur est également la plus efficace) et vous pouvez calculer des valeurs à l'aide d'un nombre de fonctions analytiques (dont la liste est très similaire aux fonctions agrégées).

Dans ce cas, voici ce que fait la requête interne. L'ensemble de données entier est trié par UserId puis Date DESC. Ensuite, il le traite en un seul passage. Pour chaque ligne, vous renvoyez l'identifiant utilisateur et la première date vue pour cet identifiant utilisateur (puisque les dates sont triées par DESC, c'est la date maximale). Cela vous donne votre réponse avec des lignes dupliquées. Ensuite, l’extérieur de DISTINCT écrase les doublons.

Ce n’est pas un exemple particulièrement spectaculaire de requêtes analytiques. Pour gagner beaucoup plus, envisagez de dresser un tableau des recettes financières et de calculer, pour chaque utilisateur et chaque reçu, le total cumulé de leurs paiements. Les requêtes analytiques résolvent cela efficacement. D'autres solutions sont moins efficaces. C'est pourquoi ils font partie du standard SQL 2003. (Malheureusement, Postgres ne les a pas encore. Grrr ...)

6
user11318

Une clause QUALIFY ne serait-elle pas à la fois la plus simple et la meilleure?

select userid, my_date, ...
from users
qualify rank() over (partition by userid order by my_date desc) = 1

Pour le contexte, ici, sur Teradata, un test de taille décente s'exécute en 17 avec cette version de QUALIFY et en 23 avec la solution 'inline view'/Aldridge # 1.

6
wcw

Avec PostgreSQL 8.4 ou ultérieur, vous pouvez utiliser ceci:

select user_id, user_value_1, user_value_2
  from (select user_id, user_value_1, user_value_2, row_number()
          over (partition by user_id order by user_date desc) 
        from users) as r
  where r.row_number=1
5
Cito

DansOracle 12c+, vous pouvez utiliser les requêtes Top n avec la fonction analytique rank pour y parvenir très précisément sans sous-requêtes:

select *
from your_table
order by rank() over (partition by user_id order by my_date desc)
fetch first 1 row with ties;

Ce qui précède renvoie toutes les lignes avec un maximum de my_date par utilisateur. 

Si vous souhaitez une seule ligne avec une date maximale, remplacez la rank par row_number:

select *
from your_table
order by row_number() over (partition by user_id order by my_date desc)
fetch first 1 row with ties; 
5
Gurwinder Singh
select VALUE from TABLE1 where TIME = 
   (select max(TIME) from TABLE1 where DATE= 
   (select max(DATE) from TABLE1 where CRITERIA=CRITERIA))
3
nouky

Utilisez ROW_NUMBER() pour attribuer un classement unique sur Date décroissant pour chaque UserId, puis filtrez vers la première ligne pour chaque UserId (c'est-à-dire, ROW_NUMBER = 1).

SELECT UserId, Value, Date
FROM (SELECT UserId, Value, Date,
        ROW_NUMBER() OVER (PARTITION BY UserId ORDER BY Date DESC) rn
      FROM users) u
WHERE rn = 1;
3
markusk

Il me suffisait d'écrire un exemple "en direct" au travail :)

Celui-ci supporte plusieurs valeurs pour UserId le même date.

Colonnes: UserId, Value, Date

SELECT
   DISTINCT UserId,
   MAX(Date) OVER (PARTITION BY UserId ORDER BY Date DESC),
   MAX(Values) OVER (PARTITION BY UserId ORDER BY Date DESC)
FROM
(
   SELECT UserId, Date, SUM(Value) As Values
   FROM <<table_name>>
   GROUP BY UserId, Date
)

Vous pouvez utiliser FIRST_VALUE au lieu de MAX et le rechercher dans le plan Expliquer. Je n'ai pas eu le temps de jouer avec.

Bien sûr, si vous effectuez une recherche dans d'énormes tables, il est probablement préférable d'utiliser des indications complètes dans votre requête.

3
Truper
Select  
   UserID,  
   Value,  
   Date  
From  
   Table,  
   (  
      Select  
          UserID,  
          Max(Date) as MDate  
      From  
          Table  
      Group by  
          UserID  
    ) as subQuery  
Where  
   Table.UserID = subQuery.UserID and  
   Table.Date = subQuery.mDate  
3
Aheho

La réponse ici est Oracle uniquement. Voici une réponse un peu plus sophistiquée dans tout le langage SQL:

Qui a le meilleur résultat global pour les devoirs (somme maximale de points de devoirs)?

SELECT FIRST, LAST, SUM(POINTS) AS TOTAL
FROM STUDENTS S, RESULTS R
WHERE S.SID = R.SID AND R.CAT = 'H'
GROUP BY S.SID, FIRST, LAST
HAVING SUM(POINTS) >= ALL (SELECT SUM (POINTS)
FROM RESULTS
WHERE CAT = 'H'
GROUP BY SID)

Et un exemple plus difficile, qui nécessite quelques explications, pour lequel je n'ai pas le temps nécessaire:

Donnez le livre (ISBN et titre) le plus populaire en 2008, c’est-à-dire le plus emprunté en 2008.

SELECT X.ISBN, X.title, X.loans
FROM (SELECT Book.ISBN, Book.title, count(Loan.dateTimeOut) AS loans
FROM CatalogEntry Book
LEFT JOIN BookOnShelf Copy
ON Book.bookId = Copy.bookId
LEFT JOIN (SELECT * FROM Loan WHERE YEAR(Loan.dateTimeOut) = 2008) Loan 
ON Copy.copyId = Loan.copyId
GROUP BY Book.title) X
HAVING loans >= ALL (SELECT count(Loan.dateTimeOut) AS loans
FROM CatalogEntry Book
LEFT JOIN BookOnShelf Copy
ON Book.bookId = Copy.bookId
LEFT JOIN (SELECT * FROM Loan WHERE YEAR(Loan.dateTimeOut) = 2008) Loan 
ON Copy.copyId = Loan.copyId
GROUP BY Book.title);

J'espère que cela aide (n'importe qui) .. :)

Cordialement, Guus

2
Guus

Je pense quelque chose comme ça. (Pardonnez-moi les erreurs de syntaxe; je suis habitué à utiliser HQL à ce stade!)

EDIT: Également mal interprété la question! Corrigé la requête ...

SELECT UserId, Value
FROM Users AS user
WHERE Date = (
    SELECT MAX(Date)
    FROM Users AS maxtest
    WHERE maxtest.UserId = user.UserId
)
2
jdmichal

(T-SQL) Obtenez d’abord tous les utilisateurs et leur date maximale. Joindre avec la table pour trouver les valeurs correspondantes pour les utilisateurs sur les maxdates.

create table users (userid int , value int , date datetime)
insert into users values (1, 1, '20010101')
insert into users values (1, 2, '20020101')
insert into users values (2, 1, '20010101')
insert into users values (2, 3, '20030101')

select T1.userid, T1.value, T1.date 
    from users T1,
    (select max(date) as maxdate, userid from users group by userid) T2    
    where T1.userid= T2.userid and T1.date = T2.maxdate

résultats:

userid      value       date                                    
----------- ----------- -------------------------- 
2           3           2003-01-01 00:00:00.000
1           2           2002-01-01 00:00:00.000
2
boes

je pense que vous devriez faire cette variante à la requête précédente:

SELECT UserId, Value FROM Users U1 WHERE 
Date = ( SELECT MAX(Date)    FROM Users where UserId = U1.UserId)
2
stefano m

En supposant que Date est unique pour un ID utilisateur donné, voici quelques TSQL:

SELECT 
    UserTest.UserID, UserTest.Value
FROM UserTest
INNER JOIN
(
    SELECT UserID, MAX(Date) MaxDate
    FROM UserTest
    GROUP BY UserID
) Dates
ON UserTest.UserID = Dates.UserID
AND UserTest.Date = Dates.MaxDate 
2
marc

Je suis assez en retard pour le parti, mais le hack suivant surperformera les sous-requêtes corrélées et toute fonction d'analyse, mais comporte une restriction: les valeurs doivent être converties en chaînes. Cela fonctionne donc pour les dates, les nombres et les autres chaînes. Le code n'a pas l'air bien mais le profil d'exécution est génial. 

select
    userid,
    to_number(substr(max(to_char(date,'yyyymmdd') || to_char(value)), 9)) as value,
    max(date) as date
from 
    users
group by
    userid

La raison pour laquelle ce code fonctionne si bien est qu’il n’a besoin que d’analyser la table une fois. Il ne nécessite aucun index et, plus important encore, il ne nécessite pas de trier la table, contrairement à la plupart des fonctions d'analyse. Les index vous seront utiles si vous devez filtrer le résultat pour un seul ID utilisateur.

2
aLevelOfIndirection

Si vous utilisez Postgres, vous pouvez utiliser array_agg comme

SELECT userid,MAX(adate),(array_agg(value ORDER BY adate DESC))[1] as value
FROM YOURTABLE
GROUP BY userid

Je ne connais pas Oracle. C'est ce que je suis venu avec

SELECT 
  userid,
  MAX(adate),
  SUBSTR(
    (LISTAGG(value, ',') WITHIN GROUP (ORDER BY adate DESC)),
    0,
    INSTR((LISTAGG(value, ',') WITHIN GROUP (ORDER BY adate DESC)), ',')-1
  ) as value 
FROM YOURTABLE
GROUP BY userid 

Les deux requêtes renvoient les mêmes résultats que la réponse acceptée. Voir SQLFiddles:

  1. Réponse acceptée
  2. Ma solution avec Postgres
  3. Ma solution avec Oracle
1
Bruno Calza

Cela devrait être aussi simple que:

SELECT UserId, Value
FROM Users u
WHERE Date = (SELECT MAX(Date) FROM Users WHERE UserID = u.UserID)
1
Valerion

Essayez d’abord, j’ai mal interprété la question, en suivant la première réponse, voici un exemple complet avec des résultats corrects:

CREATE TABLE table_name (id int, the_value varchar(2), the_date datetime);

INSERT INTO table_name (id,the_value,the_date) VALUES(1 ,'a','1/1/2000');
INSERT INTO table_name (id,the_value,the_date) VALUES(1 ,'b','2/2/2002');
INSERT INTO table_name (id,the_value,the_date) VALUES(2 ,'c','1/1/2000');
INSERT INTO table_name (id,the_value,the_date) VALUES(2 ,'d','3/3/2003');
INSERT INTO table_name (id,the_value,the_date) VALUES(2 ,'e','3/3/2003');

-

  select id, the_value
      from table_name u1
      where the_date = (select max(the_date)
                     from table_name u2
                     where u1.id = u2.id)

-

id          the_value
----------- ---------
2           d
2           e
1           b

(3 row(s) affected)
1
KyleLanser

Cela s’occupera aussi des doublons (retourne une ligne pour chaque utilisateur)

SELECT *
FROM (
  SELECT u.*, FIRST_VALUE(u.rowid) OVER(PARTITION BY u.user_id ORDER BY u.date DESC) AS last_rowid
  FROM users u
) u2
WHERE u2.rowid = u2.last_rowid
1
na43251

Je viens de tester cela et cela semble fonctionner sur une table de journalisation

select ColumnNames, max(DateColumn) from log  group by ColumnNames order by 1 desc
1
Mauro
select userid, value, date
  from thetable t1 ,
       ( select t2.userid, max(t2.date) date2 
           from thetable t2 
          group by t2.userid ) t3
 where t3.userid t1.userid and
       t3.date2 = t1.date

IMHO cela fonctionne. HTH 

1
Zsolt Botykai

Je pense que cela devrait fonctionner?

Select
T1.UserId,
(Select Top 1 T2.Value From Table T2 Where T2.UserId = T1.UserId Order By Date Desc) As 'Value'
From
Table T1
Group By
T1.UserId
Order By
T1.UserId
1
GateKiller

vérifier ce lien si vos questions semblent similaires à cette page, je vous suggérerais la requête suivante qui vous donnera la solution

select distinct sno,item_name,max(start_date) over(partition by sno),max(end_date) over(partition by sno),max(creation_date) over(partition by sno), max(last_modified_date) over(partition by sno) from uniq_select_records order by sno,item_name asc;

sera donné des résultats précis liés à ce lien

0
Smart003
SELECT a.userid,a.values1,b.mm 
FROM table_name a,(SELECT userid,Max(date1)AS mm FROM table_name GROUP BY userid) b
WHERE a.userid=b.userid AND a.DATE1=b.mm;
0
praveen

Si (UserID, Date) est unique, c’est-à-dire qu’aucune date ne s'affiche deux fois pour le même utilisateur, puis:

select TheTable.UserID, TheTable.Value
from TheTable inner join (select UserID, max([Date]) MaxDate
                          from TheTable
                          group by UserID) UserMaxDate
     on TheTable.UserID = UserMaxDate.UserID
        TheTable.[Date] = UserMaxDate.MaxDate;
0
finnw

Utilisez le code:

select T.UserId,T.dt from (select UserId,max(dt) 
over (partition by UserId) as dt from t_users)T where T.dt=dt;

Cela va récupérer les résultats, indépendamment des valeurs en double pour UserId. Si votre UserId est unique, il devient plus simple:

select UserId,max(dt) from t_users group by UserId;
0
Natty
select   UserId,max(Date) over (partition by UserId) value from users;
0
Amitābha

Solution pour MySQL qui n'a pas les concepts de partition KEEP, DENSE_RANK. 

select userid,
       my_date,
       ...
from
(
select @sno:= case when @pid<>userid then 0
                    else @sno+1
    end as serialnumber, 
    @pid:=userid,
       my_Date,
       ...
from   users order by userid, my_date
) a
where a.serialnumber=0

Référence: http://benincampus.blogspot.com/2013/08/select-rows-which-have-maxmin-value-in.html

0
Ben Lin