Sélectionnez la ligne avec la date la plus récente par utilisateur

125

J'ai un tableau ("lms_attendance") des heures d'arrivée et de départ des utilisateurs qui ressemble à ceci:

id  user    time    io (enum)
1   9   1370931202  out
2   9   1370931664  out
3   6   1370932128  out
4   12  1370932128  out
5   12  1370933037  in

J'essaie de créer une vue de cette table qui afficherait uniquement l'enregistrement le plus récent par identifiant d'utilisateur, tout en me donnant la valeur «in» ou «out», donc quelque chose comme:

id  user    time    io
2   9   1370931664  out
3   6   1370932128  out
5   12  1370933037  in

Je suis assez proche jusqu'à présent, mais j'ai réalisé que les vues n'accepteraient pas les sous-requêtes, ce qui rend les choses beaucoup plus difficiles. La requête la plus proche que j'ai reçue était:

select 
    `lms_attendance`.`id` AS `id`,
    `lms_attendance`.`user` AS `user`,
    max(`lms_attendance`.`time`) AS `time`,
    `lms_attendance`.`io` AS `io` 
from `lms_attendance` 
group by 
    `lms_attendance`.`user`, 
    `lms_attendance`.`io`

Mais ce que j'obtiens c'est:

id  user    time    io
3   6   1370932128  out
1   9   1370931664  out
5   12  1370933037  in
4   12  1370932128  out

Ce qui est proche, mais pas parfait. Je sais que le dernier groupe de by ne devrait pas être là, mais sans lui, il renvoie l'heure la plus récente, mais pas avec sa valeur IO relative.

Des idées? Merci!

Keith
la source
Revenez au manuel. Vous verrez qu'il offre des solutions à ce problème avec et sans sous-requêtes (corrélées et non corrélées).
Strawberry
@Barmar, techniquement, comme je l'ai souligné dans ma réponse, il s'agit d'une copie des 700 questions avec la balise la plus grande par groupe .
TMS du
@Prodikl, qu'est-ce que 'io (enum)'?
Monica Heddneck
J'avais une colonne appelée "IO" qui signifie "in or out", c'était un type enum avec des valeurs possibles "in" ou "out". Cela était utilisé pour suivre les dates d'arrivée et de sortie des cours.
Keith

Réponses:

199

Requete:

SQLFIDDLEExample

SELECT t1.*
FROM lms_attendance t1
WHERE t1.time = (SELECT MAX(t2.time)
                 FROM lms_attendance t2
                 WHERE t2.user = t1.user)

Résultat:

| ID | USER |       TIME |  IO |
--------------------------------
|  2 |    9 | 1370931664 | out |
|  3 |    6 | 1370932128 | out |
|  5 |   12 | 1370933037 |  in |

Solution qui fonctionnera à chaque fois:

SQLFIDDLEExample

SELECT t1.*
FROM lms_attendance t1
WHERE t1.id = (SELECT t2.id
                 FROM lms_attendance t2
                 WHERE t2.user = t1.user            
                 ORDER BY t2.id DESC
                 LIMIT 1)
Justin
la source
2
Hou la la! non seulement ce travail, j'ai été autorisé à créer une vue avec cette requête même si elle contient des sous-requêtes. avant, quand j'essayais de créer une vue contenant des sous-requêtes, cela ne me permettait pas. existe-t-il des règles expliquant pourquoi cela est autorisé, mais une autre ne l'est pas?
Keith
très étrange. Merci beaucoup! c'était peut-être parce que ma sous-requête était une pseudo table que je sélectionnais FROM, où dans cet exemple elle était utilisée dans la clause WHERE.
Keith
4
Pas besoin de sous-requêtes! De plus, cette solution ne fonctionne pas s'il y a deux enregistrements avec exactement la même heure . Il n'est pas nécessaire d'essayer de réinventer la roue à chaque fois, car c'est un problème courant - au lieu de cela, optez pour des solutions déjà testées et optimisées - @Prodikl voir ma réponse.
TMS
ah, merci pour la perspicacité! J'essaierai le nouveau code quand je serai au bureau demain.
Keith
3
@TMS Cette solution fonctionne si les enregistrements ont exactement la même heure, car la requête recherche l'enregistrement avec le plus grand ID. Cela implique que l'heure dans le tableau est l'heure d'insertion, ce qui peut ne pas être une bonne hypothèse. Votre solution compare plutôt les horodatages et, lorsque deux horodatages sont identiques, vous renvoyez également la ligne avec le plus grand identifiant. Par conséquent, votre solution suppose également que l'horodatage de ce tableau est lié à l'ordre d'insertion, qui est le plus grand défaut avec vos deux requêtes.
WebWanderer
73

Inutile d'essayer de réinventer la roue, car il s'agit du problème le plus fréquent par groupe . Une très belle solution est présentée .

Je préfère la solution la plus simpliste ( voir SQLFiddle, mise à jour de Justin ) sans sous-requêtes (donc facile à utiliser dans les vues):

SELECT t1.*
FROM lms_attendance AS t1
LEFT OUTER JOIN lms_attendance AS t2
  ON t1.user = t2.user 
        AND (t1.time < t2.time 
         OR (t1.time = t2.time AND t1.Id < t2.Id))
WHERE t2.user IS NULL

Cela fonctionne également dans le cas où il y a deux enregistrements différents avec la même plus grande valeur dans le même groupe - grâce à l'astuce avec (t1.time = t2.time AND t1.Id < t2.Id). Tout ce que je fais ici est d'assurer que dans le cas où deux enregistrements du même utilisateur ont la même heure, un seul est choisi. Peu importe si le critère est Idou quelque chose d'autre - fondamentalement, tout critère garanti unique ferait le travail ici.

TMS
la source
1
Le max utilise t1.time < t2.timeet le min serait t1.time > t2.timequi est le contraire de mon intuition initiale.
Aucun
1
@ J.Money car il y a une négation implicite cachée: vous sélectionnez tous les enregistrements de t1 qui n'ont pas d' enregistrement correspondant de t2 où la t1.time < t2.timecondition s'applique :-)
TMS
4
WHERE t2.user IS NULLest un peu étrange. Quel rôle joue cette ligne?
tumultous_rooster
1
La réponse acceptée, publiée par Justin, peut être plus optimale. La réponse acceptée utilise un balayage d'index vers l'arrière sur la clé primaire de la table, suivi d'une limite, suivi d'un balayage de séquence de la table. Par conséquent, la réponse acceptée peut être grandement optimisée avec un index supplémentaire. Cette requête pourrait également être optimisée par un index, car elle effectue deux balayages de séquence, mais comprend également un hachage et un "hachage-anti-jointure" des résultats du balayage de séquence et du hachage de l'autre balayage de séquence. Je serais intéressé par une explication de quelle approche est vraiment la plus optimale.
WebWanderer
@TMS pourriez-vous clarifier la OR (t1.time = t2.time AND t1.Id < t2.Id))section?
Oleg Kuts
6

Basé sur la réponse @TMS, je l'aime bien car il n'y a pas besoin de sous-requêtes mais je pense que l'omission de la 'OR'partie sera suffisante et beaucoup plus simple à comprendre et à lire.

SELECT t1.*
FROM lms_attendance AS t1
LEFT JOIN lms_attendance AS t2
  ON t1.user = t2.user 
        AND t1.time < t2.time
WHERE t2.user IS NULL

si vous n'êtes pas intéressé par les lignes avec des temps nuls, vous pouvez les filtrer dans la WHEREclause:

SELECT t1.*
FROM lms_attendance AS t1
LEFT JOIN lms_attendance AS t2
  ON t1.user = t2.user 
        AND t1.time < t2.time
WHERE t2.user IS NULL and t1.time IS NOT NULL
user1792210
la source
Omettre la ORpartie est une très mauvaise idée si deux enregistrements peuvent avoir la même chose time.
TMS du
J'éviterais cette solution pour des raisons de performances. Comme @OlegKuts l'a mentionné, cela devient très lent sur les ensembles de données de taille moyenne à grande.
Peter Meadley
4

Déjà résolu, mais juste pour mémoire, une autre approche serait de créer deux vues ...

CREATE TABLE lms_attendance
(id int, user int, time int, io varchar(3));

CREATE VIEW latest_all AS
SELECT la.user, max(la.time) time
FROM lms_attendance la 
GROUP BY la.user;

CREATE VIEW latest_io AS
SELECT la.* 
FROM lms_attendance la
JOIN latest_all lall 
    ON lall.user = la.user
    AND lall.time = la.time;

INSERT INTO lms_attendance 
VALUES
(1, 9, 1370931202, 'out'),
(2, 9, 1370931664, 'out'),
(3, 6, 1370932128, 'out'),
(4, 12, 1370932128, 'out'),
(5, 12, 1370933037, 'in');

SELECT * FROM latest_io;

Cliquez ici pour le voir en action chez SQL Fiddle

Davmos
la source
1
Merci pour le suivi! oui, j'allais créer plusieurs vues s'il n'y avait pas de moyen plus simple. merci encore
Keith
0
select b.* from 

    (select 
        `lms_attendance`.`user` AS `user`,
        max(`lms_attendance`.`time`) AS `time`
    from `lms_attendance` 
    group by 
        `lms_attendance`.`user`) a

join

    (select * 
    from `lms_attendance` ) b

on a.user = b.user
and a.time = b.time
chétan
la source
Merci. Je sais que je peux le faire en utilisant une sous-requête, mais j'espérais transformer cela en une vue, et cela n'autorisera pas les sous-requêtes dans les vues AFAIK. Dois-je transformer chaque sous-requête en vue, etc.?
Keith
join (select * from lms_attendance ) b= join lms_attendance b
azerafati
0
 select result from (
     select vorsteuerid as result, count(*) as anzahl from kreditorenrechnung where kundeid = 7148
     group by vorsteuerid
 ) a order by anzahl desc limit 0,1
Konstantin XFlash Stratigenas
la source
0

Si vous êtes sur MySQL 8.0 ou supérieur, vous pouvez utiliser les fonctions Windows :

Requete:

DBFiddleExample

SELECT DISTINCT
FIRST_VALUE(ID) OVER (PARTITION BY lms_attendance.USER ORDER BY lms_attendance.TIME DESC) AS ID,
FIRST_VALUE(USER) OVER (PARTITION BY lms_attendance.USER ORDER BY lms_attendance.TIME DESC) AS USER,
FIRST_VALUE(TIME) OVER (PARTITION BY lms_attendance.USER ORDER BY lms_attendance.TIME DESC) AS TIME,
FIRST_VALUE(IO) OVER (PARTITION BY lms_attendance.USER ORDER BY lms_attendance.TIME DESC) AS IO
FROM lms_attendance;

Résultat:

| ID | USER |       TIME |  IO |
--------------------------------
|  2 |    9 | 1370931664 | out |
|  3 |    6 | 1370932128 | out |
|  5 |   12 | 1370933037 |  in |

L'avantage que je vois par rapport à l'utilisation de la solution proposée par Justin est qu'elle vous permet de sélectionner la ligne avec les données les plus récentes par utilisateur (ou par identifiant, ou par autre) même à partir de sous-requêtes sans avoir besoin d'une vue ou d'un tableau intermédiaire.

Et si vous exécutez un HANA, il est également ~ 7 fois plus rapide: D

Nicolas Brauer
la source
-1

Ok, cela peut être un piratage ou une source d'erreur, mais d'une manière ou d'une autre, cela fonctionne aussi bien-

SELECT id, MAX(user) as user, MAX(time) as time, MAX(io) as io FROM lms_attendance GROUP BY id;
kev
la source
-2

Essayez cette requête:

  select id,user, max(time), io 
  FROM lms_attendance group by user;
Sugan
la source
Essayez d'en faire un SQLFiddle. Vous trouverez probablement cela idet ce iosont des colonnes non agrégées, qui ne peuvent pas être utilisées dans un fichier group by.
Dewi Morgan
1
il n'y a pas de garantie que l'id sera l'id avec max (time), il pourrait s'agir de n'importe lequel des identifiants du groupe. c'est le problème que je suis venu ici pour résoudre, toujours à la recherche
robisrob
-3

Vous pouvez éventuellement faire un groupe par utilisateur, puis classer par ordre chronologique. Quelque chose comme ci-dessous

  SELECT * FROM lms_attendance group by user order by time desc;
user2365199
la source
-3

Cela a fonctionné pour moi:

SELECT user, time FROM 
(
    SELECT user, time FROM lms_attendance --where clause
) AS T 
WHERE (SELECT COUNT(0) FROM table WHERE user = T.user AND time > T.time) = 0
ORDER BY user ASC, time DESC
Alvaro Sifuentes
la source