2009-08-22 13 views
5

Tengo una tabla con 2 campos: ID único, ID de usuario (clave externa) y fecha y hora. Este es un registro de acceso a un servicio. Trabajo en SQL Server pero agradecería las respuestas agnósticas.SQL: encontrar la brecha de fecha más larga

Me gustaría usar SQL para encontrar para un determinado usuario la ID desde donde comienza la brecha más larga.

Así, por ejemplo, dicen que mis valores son los siguientes (simplificación para un usuario):

ID | User-ID | Time 
---------------------------------- 
1 | 1  | 11-MAR-09, 8:00am 
2 | 1  | 11-MAR-09, 6:00pm 
3 | 1  | 13-MAR-09, 7:00pm 
4 | 1  | 14-MAR-09, 6:00pm 

Si busco la brecha más larga para el usuario 1 voy a entrar ID 2 (que también sería bueno obtener la longitud de la brecha allí mismo, pero mucho menos crítico).

¿Cuál es la forma más eficiente de lograr esto en SQL?

Nota: La identificación no es necesariamente secuencial.

Gracias

+0

Puede aclarar: ¿está buscando la brecha más grande entre los registros * adyacentes * cuando se clasifica por ID y se filtra por usuario, o la brecha más grande entre * dos registros * para el mismo usuario? Para cualquiera, la respuesta es 2 para su caso de prueba. – richardtallent

+0

@richardtellent: Estoy buscando el espacio más largo entre las entradas de usuario "adyacentes", donde "adyacente" significa que no hay ninguna entrada de fecha y hora entre ellas (y no está basada en ID). Espero que eso se aclare. No estoy seguro de haber entendido su segunda explicación, porque la brecha más grande entre dos registros se encuentra entre el primero (1) y el último (4). –

Respuesta

10

Agnóstico de base de datos, algo así como una variante de richardtallent, pero sin las restricciones.

A partir de esta configuración:

create table test(id int, userid int, time datetime) 
insert into test values (1, 1, '2009-03-11 08:00') 
insert into test values (2, 1, '2009-03-11 18:00') 
insert into test values (3, 1, '2009-03-13 19:00') 
insert into test values (4, 1, '2009-03-14 18:00') 

(Estoy aquí SQL Server 2008, pero no debe importar)

ejecución de esta consulta:

select 
    starttime.id as gapid, starttime.time as starttime, endtime.time as endtime, 
    /* Replace next line with your DB's way of calculating the gap */ 
    DATEDIFF(second, starttime.time, endtime.time) as gap 
from 
    test as starttime 
inner join test as endtime on 
    (starttime.userid = endtime.userid) 
    and (starttime.time < endtime.time) 
left join test as intermediatetime on 
    (starttime.userid = intermediatetime.userid) 
    and (starttime.time < intermediatetime.time) 
    and (intermediatetime.time < endtime.time) 
where 
    (intermediatetime.id is null) 

dicta la siguiente :

gapid starttime    endtime     gap 
1  2009-03-11 08:00:00.000 2009-03-11 18:00:00.000 36000 
2  2009-03-11 18:00:00.000 2009-03-13 19:00:00.000 176400 
3  2009-03-13 19:00:00.000 2009-03-14 18:00:00.000 82800 

Puede simplemente ORDEN POR la ​​expresión de brecha descendente, y elija el resultado superior.

Algunas explicaciones: al igual que la respuesta de richardtallent, se une a la tabla para encontrar un registro 'posterior', esto básicamente empareja todos los registros con CUALQUIERA de sus registros posteriores (por lo tanto, los pares 1 + 2, 1 + 3, 1+ 4, 2 + 3, 2 + 4, 3 + 4). Luego hay otra auto-unión, esta vez una combinación a la izquierda, para encontrar filas entre las dos previamente seleccionadas así (1 + 2 + nulo, 1 + 3 + 2, 1 + 4 + 2, 1 + 4 + 3, 2+ 3 + nulo, 2 + 4 + 3, 3 + 4 + nulo). La cláusula WHERE, sin embargo, los filtra (mantiene solo las filas sin una fila intermedia), por lo tanto, conserva solo 1 + 2 + nulo, 2 + 3 + nulo y 3 + 4 + nulo. Taa-daa!

Si pudiera, potencialmente, tener el mismo tiempo allí dos veces (un "espacio" de 0), entonces necesitará una forma de romper las ataduras, como señala Dems. Si puede usar la identificación como desempate, cambie, p. Ej.

and (starttime.time < intermediatetime.time) 

a

and ((starttime.time < intermediatetime.time) 
    or ((starttime.time = intermediatetime.time) and (starttime.id < intermediatetime.id))) 

suponiendo que 'id' es una forma válida para desempatar.

De hecho, si usted sabe ese ID será monótona creciente (Sé que dijo 'no secuencial' - no está claro si esto significa que no se incrementan con cada fila, o simplemente que los ID de la dos entradas relevantes pueden no ser secuenciales porque, por ejemplo, otro usuario tiene entradas intermedias), puede usar ID en lugar de tiempo en todas las comparaciones para hacerlo aún más simple.

+1

+1 para: usar alias de tablas significativas. (¡Gracias!) Y me gustó la combinación externa para encontrar los pares de fechas para solo aquellos que no tenían nada en el medio. Nunca he visto eso, pero tiene mucho sentido. Y señalando que datediff es SQL Server específico. Hubiera sido bueno ver esto tomado todo el camino hasta filtrar el resultado para simplemente mostrar la información para max (gap) –

+0

Nice. Voto ascendente, me gusta el uso de la combinación LEFT OUTER mejor que mi doble uso de una subconsulta correlacionada. – richardtallent

1

En primer lugar, se une a la mesa a sí mismo por lo que cada registro de un usuario determinado se empareja con cualquier registro para ese mismo usuario.

Luego, seleccione solo aquellos pares donde el primero es anterior al anterior, no hay registro antes del primero, y no hay registro después del último.

SELECT t1.id, t1.[user-id], t1.time, (t2.time - t1.time) AS GapTime 
FROM 
    t AS t1 
    INNER JOIN t AS t2 ON t1.[user-id] = t2.[user-id] 
WHERE 
    t1.time < t2.time 
    AND NOT EXISTS (SELECT NULL FROM t AS t3 WHERE t3.[user-id] = t1.[user-id] 
     AND t3.time > t2.time) 
    AND NOT EXISTS (SELECT NULL FROM t AS t4 WHERE t4.[user-id] = t1.[user-id] 
     AND t4.time < t1.time) 

Advertencias:

  1. no regresa a los usuarios que tienen 0 o 1 registros.
  2. No devuelve usuarios donde todos los registros tienen la misma fecha/hora.
  3. Devolverá múltiples registros para un usuario si el usuario tiene registros duplicados en el límite inicial o final de su espacio más grande.

Si lo desea, puede fijar # 2 anterior cambiando "t1.time < t2.time" a "t1.time < = t2.time", el cual le dará una brecha de 0 si sólo hay un registro para el usuario.

+0

Esta _is_ base de datos es independiente, por lo que +1 :) – MatBailie

+0

EXISTS (SELECT * FROM x) ha demostrado ser más rápido que SELECT NULL en SQL Server. Esencialmente SQL Server ha sido ajustado para ese propósito. – MatBailie

+0

-1, No está buscando espacios entre puntos secuenciales en el tiempo, pero obteniendo: _select "user-id", min (tiempo), max (tiempo), diff (..) del grupo t por "user-id_ Y el ID coincidente mínimo (tiempo) para esa ID de usuario. –

3

Únete Tiempo clasificado en el rango de una sola vez para obtener la brecha:

with cte_ranked as (
select *, row_number() over (partition by UserId order by Time) as rn 
from table) 
select l.*, datediff(minute, r.Time, l.Time) as gap_length 
from cte_ranked l join cte_ranked r on l.UserId = r.UserId and l.rn = r.rn-1 

continuación, puede utilizar muchos métodos para identificar el máximo de separación, cuando empezó etc.

actualización

Mi respuesta original fue escrita desde una base de datos Mac w/oa para probar. Tuve más tiempo para jugar con este problema y probar y medir cómo funciona en una tabla de registros de 1M. Mi tabla de prueba se define así:

create table access (id int identity(1,1) 
    , UserId int not null 
    , Time datetime not null); 
create clustered index cdx_access on access(UserID, Time); 
go 

Para seleccionar el registro para cualquier información, mi respuesta preferida hasta el momento es la siguiente:

with cte_gap as (
    select Id, UserId, a.Time, (a.Time - prev.Time) as gap 
    from access a 
    cross apply (
     select top(1) Time 
     from access b 
     where a.UserId = b.UserId 
      and a.Time > b.Time 
     order by Time desc) as prev) 
, cte_max_gap as (
    select UserId, max(gap) as max_gap 
    from cte_gap 
    group by UserId) 
select g.* 
    from cte_gap g 
    join cte_max_gap m on m.UserId = g.UserId and m.max_gap = g.gap 
where g.UserId = 42; 

De registro 1M, ~ 47k usuarios distintos, el resultado de esto se devuelve en 1 ms en mi instancia de prueba insignificante (memoria caché caliente), lecturas de 48 páginas.

Si se elimina el UserId = 42, la brecha máxima y el tiempo que pasó para cada usuario (con duplicados para múltiples espacios máximos) necesitan 6379139 lecturas, bastante pesadas y 14 en mi máquina de prueba.

El tiempo puede reducirse a la mitad si sólo el ID de usuario y brecha máximo es necesaria (sin información cuando se produjo la brecha max):

select UserId, max(a.Time-prev.Time) as gap 
    from access a 
    cross apply (
     select top(1) Time 
     from access b 
     where a.UserId = b.UserId 
      and a.Time > b.Time 
     order by Time desc 
    ) as prev 
group by UserId 

Esto sólo necesita 3.193.448 lee, sólo la mitad en comparación con el anterior y completado en 6 segundos en registros de 1M. La diferencia se debe a que la versión anterior necesitaba evaluar cada brecha una vez para encontrar la máxima, luego evaluarlas nuevamente para encontrar las que son iguales al máximo. Tenga en cuenta que, para estos resultados de rendimiento, la estructura de la tabla que propuse con un índice en (UserId, Time) es critical.

En cuanto al uso de CTE y 'particiones' (mejor conocidas como funciones de clasificación): todo esto es ANSI SQL-99 y es compatible con la mayoría de los proveedores. El único constructo específico de SQL Server fue el uso de la función datediff, que ahora se elimina. Tengo la sensación de que algunos lectores entienden 'agnóstico' como el 'mínimo común denominador SQL entendido también por mi proveedor favorito'. También tenga en cuenta que el uso de expresiones de tabla comunes y operador de aplicación cruzada se utilizan únicamente para mejorar la legibilidad de la consulta. Ambos pueden reemplazarse con una tabla derivada usando un reemplazo simple y mecánico. Aquí está la consulta muy igual donde los CTE se reemplazaron con tablas derivadas. Voy a dejar que juzgan en su legibilidad en comparación con el CTE basa uno:

select g.* 
    from ( 
     select Id, UserId, a.Time, (a.Time - (
      select top(1) Time 
      from access b 
      where a.UserId = b.UserId 
       and a.Time > b.Time 
      order by Time desc 
     )) as gap 
     from access a) as g 
    join (
     select UserId, max(gap) as max_gap 
      from (
       select Id, UserId, a.Time, (a.Time - (
        select top(1) Time 
        from access b 
        where a.UserId = b.UserId 
        and a.Time > b.Time 
        order by Time desc 
        )) as gap 
      from access a) as cte_gap 
     group by UserId) as m on m.UserId = g.UserId and m.max_gap = g.gap 
    where g.UserId = 42 

Maldición, era esperada terminará lol más enrevesado. Esto es bastante legible porque solo tenía dos CTE para empezar. Aún así, en consultas con 5-6 tablas derivadas, el formulario CTE es mucho más legible.

Para completar, aquí es la misma transformación aplicada a mi consulta simplificada (sólo lagunas max, sin hora de finalización y el acceso brecha id):

select UserId, max(gap) 
    from (
     select UserId, a.Time-(
      select top(1) Time 
      from access b 
      where a.UserId = b.UserId 
       and a.Time > b.Time 
      order by Time desc) as gap 
    from access a) as gaps 
group by UserId 
+0

Las expresiones comunes de tabla, particiones, etc. no son agnósticas de base de datos ... – MatBailie

+0

Pero si está implementando en SQL Server, CTE con función de ventana puede ser bastante más rápido. Es bueno dar respuestas agnósticas y específicas, creo, a veces, cuando ves la diferencia en el rendimiento, el deseo de ir con un enfoque agnóstico puede desaparecer. –

+0

Aunque esta no es una respuesta completa. Debería envolver la selección que genera gap_lengh en otro CTE nombrado, luego clasificarlo por usuario, y finalmente seleccionar donde rank = 1. –

1

muy similar a la respuesta de RichardTallent ...

SELECT 
    t1.id, 
    t1.[user-id], 
    t1.time, 
    DATEDIFF(s, t1.time, t2.time) AS GapTime 
FROM 
    t AS t1 
INNER JOIN 
    t AS t2 
     ON t2.[user-id] = t1.[user-id] 
     AND t2.time = (
     SELECT 
      MIN(time) 
     FROM 
      t 
     WHERE 
      [user-id] = t1.[user-id] 
      AND time > t1.time 
    ) 


AS sólo se está utilizando realmente el valor de tiempo de t2, en realidad se puede volver a organizar la siguiente manera para hacer frente a los usuarios con sólo en e entrada ...

SELECT 
    t1.id, 
    t1.[user-id], 
    t1.time, 
    DATEDIFF(
     s, 
     t1.time, 
     (
     SELECT 
      MIN(time) 
     FROM 
      t 
     WHERE 
      [user-id] = t1.[user-id] 
      AND time > t1.time 
    ) 
    ) AS GapTime 
FROM 
    t1 


Por último, existe la posibilidad de múltiples entradas con el mismo sello de tiempo. Cuando eso sucede, necesitamos información adicional para decidir el orden que nos permite determinar qué registro es 'siguiente'.

Donde hay varias entradas con el mismo sello de tiempo, todos excepto uno tendrá un GapTime de 0:
- '12: 00' (espacio de 1 hasta la próxima entrada)
- '12: 01' (Gap de 0 hasta la próxima entrada)
- '12: 01' (Gap de 0 hasta la próxima entrada)
- '12: 01' (Gap de 0 hasta la próxima entrada)
- '12: 01' (Gap de 1 hasta la próxima entrada)

- '12: 02 '(Gap de NULL hasta la próxima entrada)

Solo el que es 'último' tendrá una marca de tiempo distinta de cero. Aunque la pregunta establece que el "id" puede no estar en orden, es la única información que tenemos para determinar qué registro es "último" cuando las marcas de tiempo son las mismas.

SELECT 
    t1.id, 
    t1.[user-id], 
    t1.time, 
    DATEDIFF(
     s, 
     t1.time, 
     (
     SELECT 
      MIN(time) 
     FROM 
      t 
     WHERE 
      [user-id] = t1.[user-id] 
      AND 
      (
       (time > t1.time) 
       OR 
       (time = t1.time AND id > t1.id) 
      ) 
    ) 
    ) AS GapTime 
FROM 
    t1 
+0

reemplazar DATEDIFF con cualquier función que exista en su implementación de databsae, el resto debería ser bastante genérico – MatBailie

+0

No está mal ... Fui a la ruta de unión entre el registro de inicio y el de finalización en lugar de las subconsultas correlacionadas porque es más flexible si el OP desea seleccionar más adelante información adicional de cualquier lado. Ambos deben tener un rendimiento similar. – richardtallent

Cuestiones relacionadas