Seleziona la riga con la data più recente per utente


125

Ho una tabella ("lms_attendance") dei tempi di check-in e out degli utenti che assomiglia a questo:

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

Sto provando a creare una vista di questa tabella che produrrebbe solo il record più recente per ID utente, dandomi il valore "in" o "out", quindi qualcosa del tipo:

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

Finora sono abbastanza vicino, ma mi sono reso conto che le viste non accetteranno subquerys, il che lo sta rendendo molto più difficile. La domanda più vicina che ho ricevuto è stata:

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`

Ma quello che ottengo è:

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

Che è vicino, ma non perfetto. So che l'ultimo gruppo di non dovrebbe essere lì, ma senza di esso, restituisce l'ora più recente, ma non con il suo valore IO relativo.

Qualche idea? Grazie!



Torna al manuale. Vedrai che offre soluzioni a questo problema sia con che senza subquery (correlate e non correlate).
Fragola,

@Barmar, tecnicamente, come ho sottolineato nella mia risposta, questo è un duplicato di tutte le 700 domande con il tag più grande per gruppo .
TMS,

@Prodikl, che cos'è 'io (enum)'?
Monica Heddneck,

Avevo una colonna chiamata "IO" che sta per "dentro o fuori", era un tipo enum con possibili valori "dentro" o "fuori". Questo è stato usato per tenere traccia di quando le persone hanno effettuato il check-in e out da una lezione.
Keith,

Risposte:


199

Query:

SQLFIDDLEExample

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

Risultato:

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

Soluzione che funzionerà ogni volta:

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)

2
Wow! non solo ha funzionato, ma mi è stato permesso di creare una vista con questa query anche se contiene sottoquery. prima, quando ho provato a creare una vista contenente sottoquery, non me lo ha permesso. ci sono delle regole sul perché questo è permesso ma un altro no?
Keith l'

molto strano. grazie mille! forse è stato perché la mia subquery era una pseudo tabella che stavo selezionando DA, dove in questo esempio è stato usato nella clausola WHERE.
Keith l'

4
Non c'è bisogno di subquery! Inoltre, questa soluzione non funziona se ci sono due record esattamente allo stesso tempo . Non è necessario provare a reinventare la ruota ogni volta, poiché questo è un problema comune - invece, scegli soluzioni già testate e ottimizzate - @Prodikl vedi la mia risposta.
TMS,

ah, grazie per la comprensione! proverò il nuovo codice quando sarò in ufficio domani.
Keith l'

3
@TMS Questa soluzione funziona se i record hanno lo stesso orario esatto, poiché la query sta individuando il record con l'ID più grande. Ciò implica che il tempo nella tabella è il tempo di inserimento, che potrebbe non essere una buona ipotesi. La soluzione confronta invece i timestamp e, quando due timestamp sono identici, si restituisce la riga con l'ID più grande. Pertanto, la soluzione presuppone anche che il timestamp in questa tabella sia correlato all'ordine di inserimento, che è il più grande difetto di entrambe le query.
WebWanderer,

73

Non è necessario provare a reinventare la ruota, in quanto si tratta del problema più comune per gruppo . Viene presentata una soluzione molto bella .

Preferisco la soluzione più semplicistica ( vedi SQLFiddle, aggiornamento di Justin ) senza subquery (quindi facile da usare nelle viste):

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

Questo funziona anche nel caso in cui ci siano due record diversi con lo stesso valore più grande all'interno dello stesso gruppo, grazie al trucco con (t1.time = t2.time AND t1.Id < t2.Id). Tutto quello che sto facendo qui è assicurarmi che nel caso in cui due record dello stesso utente abbiano lo stesso tempo ne venga scelto solo uno. In realtà non importa se i criteri sono Ido qualcos'altro - fondamentalmente qualsiasi criterio che è garantito per essere unico renderebbe il lavoro qui.


1
Il massimo usa t1.time < t2.timee il minimo sarebbe t1.time > t2.timel'opposto della mia intuizione iniziale.
Nessuno

1
@ J.Money perché c'è una negazione implicita nascosta: selezioni tutti i record da t1 che non hanno record corrispondenti da t2 dove t1.time < t2.timesi applica la condizione :-)
TMS

4
WHERE t2.user IS NULLè un po 'strano. Che ruolo gioca questa linea?
tumultous_rooster il

1
La risposta accettata, pubblicata da Justin, potrebbe essere più ottimale. La risposta accettata utilizza una scansione indice all'indietro sulla chiave primaria della tabella, seguita da un limite, seguita da una scansione sequenziale della tabella. Pertanto, la risposta accettata può essere notevolmente ottimizzata con un indice aggiuntivo. Questa query può essere ottimizzata anche da un indice, poiché esegue due scansioni di sequenza, ma include anche un hash e un "hash-anti-join" dei risultati della scansione di sequenza e dell'hash dell'altra scansione di sequenza. Sarei interessato a una spiegazione di quale approccio sia davvero più ottimale.
WebWanderer,

@TMS potresti chiarire la OR (t1.time = t2.time AND t1.Id < t2.Id))sezione?
Oleg Kuts,

6

Basato sulla risposta di @TMS, mi piace perché non sono necessarie sottoquery ma penso che ommettere la 'OR'parte sarà sufficiente e molto più semplice da capire e leggere.

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

se non sei interessato alle righe con tempi nulli puoi filtrarle nella WHEREclausola:

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

Omettere la ORparte è una pessima idea se due dischi possono avere lo stesso time.
TMS

Eviterei questa soluzione per motivi di prestazioni. Come accennato da @OlegKuts, questo diventa molto lento su set di dati medio-grandi.
Peter Meadley,

4

Già risolto, ma solo per la cronaca, un altro approccio sarebbe quello di creare due viste ...

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;

Fai clic qui per vederlo in azione su SQL Fiddle


1
grazie per il seguito! sì, avrei creato più viste se non ci fosse un modo più semplice. grazie ancora
Keith l'

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

Grazie. so di poterlo fare usando una sottoquery, ma speravo di trasformarlo in una vista e non consentirò sottoquery nelle viste AFAIK. dovrei trasformare ogni sottointerrogazione in una vista, ecc.?
Keith l'

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

0

Se usi MySQL 8.0 o versioni successive puoi utilizzare le funzioni di Windows :

Query:

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;

Risultato:

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

Il vantaggio che vedo rispetto all'utilizzo della soluzione proposta da Justin è che ti consente di selezionare la riga con i dati più recenti per utente (o per ID o per qualsiasi cosa) anche da sottoquery senza la necessità di una vista o tabella intermedia.

E nel caso in cui corri un HANA è anche ~ 7 volte più veloce: D


-1

Ok, potrebbe trattarsi di un hack o soggetto a errori, ma in qualche modo funziona anche-

SELECT id, MAX(user) as user, MAX(time) as time, MAX(io) as io FROM lms_attendance GROUP BY id;

-2

Prova questa query:

  select id,user, max(time), io 
  FROM lms_attendance group by user;

Prova a creare un SQLFiddle di questo. Probabilmente lo troverai ide iosono colonne non aggregate, che non possono essere utilizzate in a group by.
Dewi Morgan,

1
non esiste alcuna garanzia che l'id sia l'id con max (tempo), potrebbe essere uno qualsiasi degli id ​​all'interno del gruppo. questo è il problema che sono venuto qui per risolvere, cercando ancora
robisrob,

-3

Probabilmente puoi fare il raggruppamento per utente e poi ordinare per tempo desc. Qualcosa come il seguente

  SELECT * FROM lms_attendance group by user order by time desc;

-3

Questo ha funzionato per me:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.