Selecionar linha com a data mais recente por usuário

125

Eu tenho uma tabela ("lms_attendance") dos horários de check-in e check-out dos usuários que se parece com isso:

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

Estou tentando criar uma exibição dessa tabela que produziria apenas o registro mais recente por ID de usuário, enquanto me fornecia o valor "in" ou "out", algo como:

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

Estou bem perto até agora, mas percebi que as visualizações não aceitam subconsultas, o que está tornando muito mais difícil. A consulta mais próxima que recebi foi:

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`

Mas o que eu recebo é:

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

O que é próximo, mas não perfeito. Eu sei que o último grupo de não deveria estar lá, mas sem ele, ele retorna o tempo mais recente, mas não com seu valor relativo de IO.

Alguma ideia? Obrigado!

Keith
fonte
Volte ao manual. Você verá que ele oferece soluções para esse problema com e sem subconsultas (correlacionadas e não correlacionadas).
Strawberry
@Barmar, tecnicamente, como apontei na minha resposta, esta é uma duplicata de todas as 700 perguntas com a maior tag n por grupo .
TMS
@Prodikl, o que é 'io (enum)'?
Monica Heddneck
Eu tinha uma coluna chamada "IO", que significa "in ou out", era um tipo de enumeração com valores possíveis "in" ou "out". Isso era usado para acompanhar quando as pessoas faziam check-in e fora de uma classe.
Keith

Respostas:

199

Inquerir:

SQLFIDDLEExample

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

Resultado:

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

Solução que funciona sempre:

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
fonte
2
Uau! não só funcionou, como foi permitido criar uma exibição com essa consulta, mesmo que ela contenha subconsultas. Antes, quando tentei criar uma exibição contendo subconsultas, ela não me deixou. existem regras sobre por que isso é permitido, mas outro não?
Keith
muito estranho. muito obrigado! talvez fosse porque minha subconsulta era uma pseudo tabela que eu estava selecionando FROM, onde neste exemplo é usado na cláusula WHERE.
Keith
4
Não há necessidade de subconsultas! Além disso, esta solução não funciona se houver dois registros exatamente ao mesmo tempo . Não há necessidade de tentar reinventar a roda toda vez, pois esse é um problema comum - em vez disso, procure soluções já testadas e otimizadas - @Prodikl, veja minha resposta.
TMS
ah, obrigado pela compreensão! vou tentar o novo código quando estiver no escritório amanhã.
Keith
3
@TMS Esta solução funciona se os registros tiverem exatamente o mesmo horário, pois a consulta está localizando o registro com o maior ID. Isso implica que o tempo na tabela é o tempo de inserção, o que pode não ser uma boa suposição. Sua solução compara os carimbos de data e hora e, quando dois carimbos de data e hora são idênticos, você retorna a linha com o maior ID também. Portanto, sua solução também pressupõe que o registro de data e hora nesta tabela esteja relacionado à ordem de inserção, que é a maior falha nas duas consultas.
WebWanderer
73

Não é necessário tentar reinventar a roda, pois esse é o maior problema comum por grupo . Solução muito boa é apresentada .

Prefiro a solução mais simplista ( consulte SQLFiddle, atualizado de Justin ) sem subconsultas (portanto, fácil de usar nas visualizações):

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

Isso também funciona em um caso em que existem dois registros diferentes com o mesmo maior valor dentro do mesmo grupo - graças ao truque com (t1.time = t2.time AND t1.Id < t2.Id). Tudo o que estou fazendo aqui é garantir que, quando dois registros do mesmo usuário tiverem o mesmo horário, apenas um seja escolhido. Na verdade, não importa se o critério é Idou algo mais - basicamente qualquer critério que seja garantido como único faria o trabalho aqui.

TMS
fonte
1
O max usa t1.time < t2.timee o min seria t1.time > t2.timeo oposto da minha intuição inicial.
Nenhum
1
@ J.Money porque não há negação implícita escondida: você seleciona todos os registros de t1 que não tem correspondente registro de t2 onde a t1.time < t2.timecondição se aplica :-)
TMS
4
WHERE t2.user IS NULLé um pouco estranho. Que papel essa linha desempenha?
tumultous_rooster
1
A resposta aceita, postada por Justin, pode ser mais ideal. A resposta aceita usa uma varredura de índice reversa na chave primária da tabela, seguida por um limite, seguido por uma varredura de sequência da tabela. Portanto, a resposta aceita pode ser bastante otimizada com um índice adicional. Essa consulta também pode ser otimizada por um índice, pois realiza duas varreduras de sequência, mas também inclui um hash e uma "anti-junção de hash" dos resultados da varredura de sequência e o hash da outra varredura de sequência. Eu estaria interessado em uma explicação de qual abordagem é realmente mais ideal.
WebWanderer
@TMS, você poderia esclarecer a OR (t1.time = t2.time AND t1.Id < t2.Id))seção?
Oleg Kuts
6

Baseado na resposta do @TMS, gosto porque não há necessidade de subconsultas, mas acho que omitir a 'OR'peça será suficiente e muito mais simples de entender e ler.

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 você não estiver interessado em linhas com tempos nulos, poderá filtrá-las na WHEREcláusula:

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
fonte
Omitir a ORpeça é uma péssima idéia se dois registros puderem ter o mesmo time.
TMS
Eu evitaria esta solução por uma questão de desempenho. Como o @OlegKuts mencionou, isso fica muito lento nos conjuntos de dados de médio a grande porte.
Peter Meadley
4

Já resolvido, mas apenas para constar, outra abordagem seria criar duas visualizações ...

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;

Clique aqui para vê-lo em ação no SQL Fiddle

davmos
fonte
1
Obrigado pelo seguimento! Sim, eu criaria várias visualizações se não houvesse uma maneira mais fácil. obrigado novamente
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
chetan
fonte
obrigado. Eu sei que posso fazê-lo usando uma subconsulta, mas esperava transformar isso em uma exibição, e isso não permitirá subconsultas na exibição AFAIK. eu teria que transformar cada subconsulta em uma visualização, 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
fonte
0

Se você está no MySQL 8.0 ou superior, pode usar as funções do Windows :

Inquerir:

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;

Resultado:

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

A vantagem que vejo ao usar a solução proposta por Justin é que ela permite selecionar a linha com os dados mais recentes por usuário (ou por ID ou por qualquer outra coisa), mesmo de subconsultas sem a necessidade de uma exibição ou tabela intermediária.

E, caso você esteja executando um HANA, também é ~ 7 vezes mais rápido: D

Nicolas Brauer
fonte
-1

Ok, isso pode ser um hack ou propenso a erros, mas de alguma forma isso está funcionando também -

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

Tente esta consulta:

  select id,user, max(time), io 
  FROM lms_attendance group by user;
Sugan
fonte
Tente fazer um SQLFiddle disso. Você provavelmente encontrará isso ide iosão colunas não agregadas, que não podem ser usadas em a group by.
Dewi Morgan
1
não há garantia de identificação será a id com max (time), pode ser qualquer um dos ids do grupo. este é o problema que eu vim aqui para resolver, ainda olhando
robisrob
-3

Possivelmente você pode agrupar por usuário e depois ordenar por tempo desc. Algo como abaixo

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

Isso funcionou para mim:

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
fonte