Seleccionar fila con la fecha más reciente por usuario

125

Tengo una tabla ("lms_attendance") de los horarios de entrada y salida de los usuarios que se ve así:

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

Estoy tratando de crear una vista de esta tabla que solo muestre el registro más reciente por ID de usuario, al tiempo que me da el valor de "entrada" o "salida", así que algo como:

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

Hasta ahora estoy bastante cerca, pero me di cuenta de que las vistas no aceptarán subconsultas, lo que lo hace mucho más difícil. La consulta más cercana que recibí fue:

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`

Pero lo que obtengo es:

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

Lo cual está cerca, pero no es perfecto. Sé que el último grupo no debería estar allí, pero sin él, devuelve la última vez, pero no con su valor relativo de IO.

¿Algunas ideas? ¡Gracias!

Keith
fuente
Regrese al manual. Verá que ofrece soluciones a este problema con y sin subconsultas (correlacionadas y no relacionadas).
Fresa
@Barmar, técnicamente, como señalé en mi respuesta, este es un duplicado de las 700 preguntas con la etiqueta más grande de n por grupo .
TMS
@Prodikl, ¿qué es 'io (enum)'?
Monica Heddneck
Tenía una columna llamada "IO" que significa "dentro o fuera", era un tipo de enumeración con posibles valores "dentro" o "fuera". Esto se usó para realizar un seguimiento de cuándo las personas ingresaban y salían de una clase.
Keith

Respuestas:

199

Consulta:

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 |

Solución que funcionará siempre:

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
fuente
2
¡Guauu! Esto no solo funcionó, sino que se me permitió crear una vista con esta consulta a pesar de que contiene subconsultas. antes, cuando intenté crear una vista que contenía subconsultas, no me lo permitió. ¿existen reglas sobre por qué esto está permitido pero otro no?
Keith
muy raro. ¡gracias una tonelada! tal vez fue porque mi subconsulta era una pseudo tabla que estaba seleccionando FROM, donde en este ejemplo se usa en la cláusula WHERE.
Keith
44
No hay necesidad de subconsultas! Además, esta solución no funciona si hay dos registros con exactamente el mismo tiempo . No es necesario intentar reinventar la rueda cada vez, ya que este es un problema común; en cambio, busque soluciones ya probadas y optimizadas. @Prodikl vea mi respuesta.
TMS
ah, gracias por la idea! Probaré el nuevo código cuando esté en la oficina mañana.
Keith
3
@TMS Esta solución funciona si los registros tienen exactamente el mismo tiempo, ya que la consulta está localizando el registro con la mayor identificación. Esto implica que el tiempo en la tabla es el tiempo de inserción, que puede no ser una buena suposición. En cambio, su solución compara las marcas de tiempo y, cuando dos marcas de tiempo son idénticas, también devuelve la fila con la identificación más grande. Por lo tanto, su solución también supone que la marca de tiempo en esta tabla está relacionada con el orden de inserción, que es la falla más grande con ambas consultas.
WebWanderer
73

No es necesario intentar reinventar la rueda, ya que este es el problema más grande de n-por-grupo . Se presenta una muy buena solución .

Prefiero la solución más simplista ( vea SQLFiddle, Justin actualizado ) sin subconsultas (por lo tanto, fácil de usar en las vistas):

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

Esto también funciona en un caso en el que hay dos registros diferentes con el mismo mayor valor dentro del mismo grupo, gracias al truco con (t1.time = t2.time AND t1.Id < t2.Id). Todo lo que estoy haciendo aquí es asegurar que en caso de que dos registros del mismo usuario tengan el mismo tiempo, solo se elija uno. En realidad, no importa si el criterio es Idu otra cosa, básicamente cualquier criterio que se garantice que sea único haría el trabajo aquí.

TMS
fuente
1
El uso máximo t1.time < t2.timey el mínimo serían t1.time > t2.timelo opuesto a mi intuición inicial.
Ninguno
1
@ J.Money porque hay una negación implícita oculta: selecciona todos los registros de t1 que no tienen el registro correspondiente de t2 donde se t1.time < t2.timeaplica la condición :-)
TMS
44
WHERE t2.user IS NULLEs un poco extraño. ¿Qué papel juega esta línea?
tumultous_rooster
1
La respuesta aceptada, publicada por Justin, puede ser más óptima. La respuesta aceptada utiliza una exploración de índice hacia atrás en la clave principal de la tabla, seguida de un límite, seguido de una exploración de secuencia de la tabla. Por lo tanto, la respuesta aceptada se puede optimizar en gran medida con un índice adicional. Esta consulta también podría optimizarse mediante un índice, ya que realiza dos exploraciones de secuencia, pero también incluye un hash y un "hash-anti-join" de los resultados de la exploración de secuencia y el hash de la otra exploración de secuencia. Me interesaría una explicación de qué enfoque es realmente más óptimo.
WebWanderer
@TMS ¿podría aclarar la OR (t1.time = t2.time AND t1.Id < t2.Id))sección?
Oleg Kuts
6

Basado en la respuesta de @TMS, me gusta porque no hay necesidad de subconsultas, pero creo que omitir la 'OR'parte será suficiente y mucho más fácil de entender y leer.

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 no está interesado en filas con tiempos nulos, puede filtrarlas en la 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
usuario1792210
fuente
Omitir la ORparte es una muy mala idea si dos registros pueden tener lo mismo time.
TMS
Evitaría esta solución por el bien del rendimiento. Como mencionó @OlegKuts, esto se vuelve muy lento en conjuntos de datos de medianos a grandes.
Peter Meadley
4

Ya resuelto, pero solo para el registro, otro enfoque sería crear dos vistas ...

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;

Haga clic aquí para verlo en acción en SQL Fiddle

davmos
fuente
1
¡Gracias por el seguimiento! Sí, iba a crear múltiples vistas si no hubiera una manera más fácil. gracias de nuevo
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
fuente
Gracias. Sé que puedo hacerlo usando una subconsulta, pero esperaba convertir esto en una vista, y no permitirá subconsultas en vistas AFAIK. ¿tendría que convertir cada subconsulta en una vista, 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
fuente
0

Si está en MySQL 8.0 o superior, puede usar las funciones de Windows :

Consulta:

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 |

La ventaja que veo sobre el uso de la solución propuesta por Justin es que le permite seleccionar la fila con los datos más recientes por usuario (o por id, o por lo que sea) incluso de subconsultas sin la necesidad de una vista o tabla intermedia.

Y en caso de que ejecute una HANA, también es ~ 7 veces más rápido: D

Nicolas Brauer
fuente
-1

Ok, esto podría ser un hack o propenso a errores, pero de alguna manera esto también funciona.

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

Prueba esta consulta:

  select id,user, max(time), io 
  FROM lms_attendance group by user;
Sugan
fuente
Intenta hacer un SQLFiddle de esto. Probablemente encontrará eso idy ioson columnas no agregadas, que no se pueden usar en a group by.
Dewi Morgan
1
no hay garantía de identificación será la identificación con max (tiempo), podría ser cualquiera de los identificadores dentro del grupo. este es el problema que vine aquí para resolver, sin dejar de mirar
robisrob
-3

Posiblemente puede agrupar por usuario y luego ordenar por tiempo desc. Algo así como abajo

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

Esto funcionó para mí:

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
fuente