SQL para determinar los días secuenciales mínimos de acceso?


125

La siguiente tabla de Historial de usuarios contiene un registro por cada día que un usuario determinado ha accedido a un sitio web (en un período UTC de 24 horas). Tiene muchos miles de registros, pero solo un registro por día por usuario. Si el usuario no ha accedido al sitio web para ese día, no se generará ningún registro.

Id UserId CreationDate
------ ------ ------------
750997 12 2009-07-07 18: 42: 20.723
750998 15 2009-07-07 18: 42: 20.927
751000 19 2009-07-07 18: 42: 22.283

Lo que estoy buscando es una consulta SQL en esta tabla con buen rendimiento , que me diga qué ID de usuario ha accedido al sitio web durante (n) días continuos sin perder un día.

En otras palabras, ¿cuántos usuarios tienen (n) registros en esta tabla con fechas secuenciales (día anterior o posterior) ? Si falta algún día de la secuencia, la secuencia se interrumpe y debería reiniciarse nuevamente en 1; Estamos buscando usuarios que hayan logrado un número continuo de días aquí sin brechas.

Cualquier parecido entre esta consulta y una insignia de Stack Overflow es pura coincidencia, por supuesto ... :)


Obtuve la insignia de entusiasta después de 28 (<30) días de membresía. Misticismo.
Kirill V. Lyadvinsky

3
¿Su fecha está almacenada como UTC? Si es así, ¿qué sucede si un residente de CA visita el sitio a las 8 a.m. un día y luego a las 8 p.m. del día siguiente? Aunque él / ella visita en días consecutivos en la Zona Horaria del Pacífico, no se registrará como tal en el DB porque el DB está almacenando los tiempos como UTC.
Guy

Jeff / Jarrod: ¿puedes consultar meta.stackexchange.com/questions/865/… por favor?
Rob Farley

Respuestas:


69

La respuesta es obviamente:

SELECT DISTINCT UserId
FROM UserHistory uh1
WHERE (
       SELECT COUNT(*) 
       FROM UserHistory uh2 
       WHERE uh2.CreationDate 
       BETWEEN uh1.CreationDate AND DATEADD(d, @days, uh1.CreationDate)
      ) = @days OR UserId = 52551

EDITAR:

Bien, aquí está mi respuesta seria:

DECLARE @days int
DECLARE @seconds bigint
SET @days = 30
SET @seconds = (@days * 24 * 60 * 60) - 1
SELECT DISTINCT UserId
FROM (
    SELECT uh1.UserId, Count(uh1.Id) as Conseq
    FROM UserHistory uh1
    INNER JOIN UserHistory uh2 ON uh2.CreationDate 
        BETWEEN uh1.CreationDate AND 
            DATEADD(s, @seconds, DATEADD(dd, DATEDIFF(dd, 0, uh1.CreationDate), 0))
        AND uh1.UserId = uh2.UserId
    GROUP BY uh1.Id, uh1.UserId
    ) as Tbl
WHERE Conseq >= @days

EDITAR:

[Jeff Atwood] Esta es una gran solución rápida y merece ser aceptada, pero la solución de Rob Farley también es excelente y posiblemente incluso más rápida (!). ¡Por favor échale un vistazo también!


@Artem: Eso fue lo que pensé inicialmente, pero cuando lo pensé, si tiene un índice en (UserId, CreationDate), los registros se mostrarán consecutivamente en el índice y debería funcionar bien.
Mehrdad Afshari

Voto a favor de este, estoy obteniendo resultados en ~ 15 segundos en 500k filas.
Jim T

44
Trunca el CreateionDate a días en todas estas pruebas (solo en el lado derecho o matas a SARG) usando DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) Esto funciona restando la fecha suministrada de cero, que Microsoft SQL Server interpreta como 1900-01-01 00:00:00 y da el número de días. Este valor se vuelve a agregar a la fecha cero, produciendo la misma fecha con el tiempo truncado.
IDisposable

1
Todo lo que puedo decir es que, sin el cambio de IDisposable, el cálculo es incorrecto . Yo personalmente validé los datos yo mismo. Algunos usuarios con deficiencias 1 día SERÍAN conseguir la etiqueta incorrectamente.
Jeff Atwood

3
Esta consulta tiene el potencial de perder una visita que ocurre a las 23: 59: 59.5. ¿Qué tal si la cambiamos a: ON uh2.CreationDate >= uh1.CreationDate AND uh2.CreationDate < DATEADD(dd, DATEDIFF(dd, 0, uh1.CreationDate) + @days, 0)que significa "Todavía no el día 31 después?". También significa que puede omitir el cálculo de @ segundos.
Rob Farley

147

¿Qué tal (y asegúrese de que la declaración anterior terminara con un punto y coma):

WITH numberedrows
     AS (SELECT ROW_NUMBER() OVER (PARTITION BY UserID 
                                       ORDER BY CreationDate)
                - DATEDIFF(day,'19000101',CreationDate) AS TheOffset,
                CreationDate,
                UserID
         FROM   tablename)
SELECT MIN(CreationDate),
       MAX(CreationDate),
       COUNT(*) AS NumConsecutiveDays,
       UserID
FROM   numberedrows
GROUP  BY UserID,
          TheOffset  

La idea es que si tenemos una lista de los días (como un número) y un número de fila, los días perdidos hacen que el desplazamiento entre estas dos listas sea un poco más grande. Por lo tanto, estamos buscando un rango que tenga un desplazamiento constante.

Podrías usar "ORDER BY NumConsecutivoDays DESC" al final de esto, o decir "HAVING count (*)> 14" para un umbral ...

Sin embargo, no he probado esto, solo escribiéndolo en la parte superior de mi cabeza. Esperemos que funcione en SQL2005 y en adelante.

... y sería muy útil con un índice en tablename (UserID, CreationDate)

Editado: resulta que Offset es una palabra reservada, así que usé TheOffset en su lugar.

Editado: la sugerencia de usar COUNT (*) es muy válida; debería haberlo hecho en primer lugar, pero no estaba pensando realmente. Anteriormente usaba dateiff (day, min (CreationDate), max (CreationDate)) en su lugar.

Robar


1
oh, también deberías agregar; antes con ->; con
Mladen Prajdic

2
Mladen: no, debe finalizar la declaración anterior con un punto y coma. ;) Jeff - Ok, pon [Offset] en su lugar. Supongo que Offset es una palabra reservada. Como dije, no lo había probado.
Rob Farley

1
Solo repitiéndome, porque este es un problema que se ve con frecuencia. Trunca el CreateionDate a días en todas estas pruebas (solo en el lado derecho o matas a SARG) usando DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) Esto funciona restando la fecha suministrada de cero, que Microsoft SQL Server interpreta como 1900-01-01 00:00:00 y da el número de días. Este valor se vuelve a agregar a la fecha cero, produciendo la misma fecha con el tiempo truncado.
IDisposable

1
IDisposable - sí, lo hago a menudo yo mismo. Simplemente no me preocupaba que lo hiciera aquí. No sería más rápido que enviarlo a un int, pero tiene la flexibilidad de contar horas, meses, lo que sea.
Rob Farley

1
Acabo de escribir una publicación de blog sobre cómo resolver esto con DENSE_RANK () también. tinyurl.com/denserank
Rob Farley

18

Si puede cambiar el esquema de la tabla, sugeriría agregar una columna LongestStreaka la tabla que establecería en la cantidad de días secuenciales que terminan en CreationDate. Es fácil actualizar la tabla en el momento de inicio de sesión (similar a lo que está haciendo ya, si no existen filas del día actual, comprobará si existe alguna fila para el día anterior. Si es cierto, aumentará LongestStreakel nueva fila, de lo contrario, lo establecerá en 1.)

La consulta será obvia después de agregar esta columna:

if exists(select * from table
          where LongestStreak >= 30 and UserId = @UserId)
   -- award the Woot badge.

1
+1 Estaba teniendo un pensamiento similar, pero con un campo de bits (IsConsecutivo) que sería 1 si hay un registro para el día anterior, de lo contrario 0.
Fredrik Mörk el

77
no vamos a cambiar el esquema para esto
Jeff Atwood

Y el IsConsecutivo puede ser una columna calculada definida en la tabla UserHistory. También puede convertirlo en una columna calculada materializada (almacenada) que se crea cuando se inserta la fila IFF (si y SOLO si) siempre inserta las filas en orden cronológico.
IDisposable

(porque nadie haría un SELECT *, sabemos que la adición de esta columna calculada no afectará a los planes de consulta a menos que se hace referencia a la columna de la derecha ... chicos?!?)
IDisposable

3
Definitivamente es una solución válida, pero no es lo que pedí. Así que le doy un "pulgar hacia los lados" ..
Jeff Atwood

6

Algunos SQL muy bien expresivos en la línea de:

select
        userId,
    dbo.MaxConsecutiveDates(CreationDate) as blah
from
    dbo.Logins
group by
    userId

Suponiendo que tiene una función agregada definida por el usuario, algo similar a (tenga cuidado con esto es defectuoso):

using System;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Runtime.InteropServices;

namespace SqlServerProject1
{
    [StructLayout(LayoutKind.Sequential)]
    [Serializable]
    internal struct MaxConsecutiveState
    {
        public int CurrentSequentialDays;
        public int MaxSequentialDays;
        public SqlDateTime LastDate;
    }

    [Serializable]
    [SqlUserDefinedAggregate(
        Format.Native,
        IsInvariantToNulls = true, //optimizer property
        IsInvariantToDuplicates = false, //optimizer property
        IsInvariantToOrder = false) //optimizer property
    ]
    [StructLayout(LayoutKind.Sequential)]
    public class MaxConsecutiveDates
    {
        /// <summary>
        /// The variable that holds the intermediate result of the concatenation
        /// </summary>
        private MaxConsecutiveState _intermediateResult;

        /// <summary>
        /// Initialize the internal data structures
        /// </summary>
        public void Init()
        {
            _intermediateResult = new MaxConsecutiveState { LastDate = SqlDateTime.MinValue, CurrentSequentialDays = 0, MaxSequentialDays = 0 };
        }

        /// <summary>
        /// Accumulate the next value, not if the value is null
        /// </summary>
        /// <param name="value"></param>
        public void Accumulate(SqlDateTime value)
        {
            if (value.IsNull)
            {
                return;
            }
            int sequentialDays = _intermediateResult.CurrentSequentialDays;
            int maxSequentialDays = _intermediateResult.MaxSequentialDays;
            DateTime currentDate = value.Value.Date;
            if (currentDate.AddDays(-1).Equals(new DateTime(_intermediateResult.LastDate.TimeTicks)))
                sequentialDays++;
            else
            {
                maxSequentialDays = Math.Max(sequentialDays, maxSequentialDays);
                sequentialDays = 1;
            }
            _intermediateResult = new MaxConsecutiveState
                                      {
                                          CurrentSequentialDays = sequentialDays,
                                          LastDate = currentDate,
                                          MaxSequentialDays = maxSequentialDays
                                      };
        }

        /// <summary>
        /// Merge the partially computed aggregate with this aggregate.
        /// </summary>
        /// <param name="other"></param>
        public void Merge(MaxConsecutiveDates other)
        {
            // add stuff for two separate calculations
        }

        /// <summary>
        /// Called at the end of aggregation, to return the results of the aggregation.
        /// </summary>
        /// <returns></returns>
        public SqlInt32 Terminate()
        {
            int max = Math.Max((int) ((sbyte) _intermediateResult.CurrentSequentialDays), (sbyte) _intermediateResult.MaxSequentialDays);
            return new SqlInt32(max);
        }
    }
}

4

Parece que podría aprovechar el hecho de que ser continuo durante n días requeriría que haya n filas.

Entonces algo como:

SELECT users.UserId, count(1) as cnt
FROM users
WHERE users.CreationDate > now() - INTERVAL 30 DAY
GROUP BY UserId
HAVING cnt = 30

sí, podemos puerta que por el número de registros, seguro .. pero que sólo elimina algunas posibilidades, como podríamos tener 120 días de visita a través de varios años con una gran cantidad de lagunas diarias
Jeff Atwood

1
Está bien, pero una vez que esté atrapado en la adjudicación de esta página, solo necesita ejecutarla una vez al día. Creo que para ese caso, algo como lo anterior haría el truco. Para ponerse al día, todo lo que necesita hacer es convertir la cláusula WHERE en una ventana deslizante usando ENTRE.
Bill

1
cada ejecución de la tarea es apátrida e independiente; no tiene conocimiento de ejecuciones anteriores distintas de la tabla en la pregunta
Jeff Atwood

3

Hacer esto con una sola consulta SQL me parece demasiado complicado. Permítanme dividir esta respuesta en dos partes.

  1. Lo que debería haber hecho hasta ahora y debería comenzar a hacer ahora:
    ejecute un trabajo cron diario que verifique a cada usuario si ha iniciado sesión hoy y luego incrementa un contador si lo tiene o lo establece en 0 si no lo ha hecho.
  2. Lo que debe hacer ahora:
    - Exporte esta tabla a un servidor que no ejecute su sitio web y no sea necesario por un tiempo. ;)
    - Ordénelo por usuario, luego fecha.
    - Revíselo secuencialmente, mantenga un contador ...

podemos escribir código en query-and-loop, eso es ... dary, digo ... trivial. Tengo curiosidad por la única forma de SQL en este momento.
Jeff Atwood

2

Si esto es tan importante para usted, obtenga este evento y maneje una tabla para darle esta información. No es necesario matar la máquina con todas esas consultas locas.


2

Puede usar un CTE recursivo (SQL Server 2005+):

WITH recur_date AS (
        SELECT t.userid,
               t.creationDate,
               DATEADD(day, 1, t.created) 'nextDay',
               1 'level' 
          FROM TABLE t
         UNION ALL
        SELECT t.userid,
               t.creationDate,
               DATEADD(day, 1, t.created) 'nextDay',
               rd.level + 1 'level'
          FROM TABLE t
          JOIN recur_date rd on t.creationDate = rd.nextDay AND t.userid = rd.userid)
   SELECT t.*
    FROM recur_date t
   WHERE t.level = @numDays
ORDER BY t.userid

2

Joe Celko tiene un capítulo completo sobre esto en SQL para Smarties (llamándolo Runs and Sequences). No tengo ese libro en casa, así que cuando llegue al trabajo ... en realidad responderé esto. (suponiendo que la tabla de historial se llama dbo.UserHistory y el número de días es @Days)

Otra pista es del blog de SQL Team sobre carreras

La otra idea que he tenido, pero no tengo un servidor SQL a mano para trabajar aquí, es usar un CTE con un ROW_NUMBER particionado como este:

WITH Runs
AS
  (SELECT UserID
         , CreationDate
         , ROW_NUMBER() OVER(PARTITION BY UserId
                             ORDER BY CreationDate)
           - ROW_NUMBER() OVER(PARTITION BY UserId, NoBreak
                               ORDER BY CreationDate) AS RunNumber
  FROM
     (SELECT UH.UserID
           , UH.CreationDate
           , ISNULL((SELECT TOP 1 1 
              FROM dbo.UserHistory AS Prior 
              WHERE Prior.UserId = UH.UserId 
              AND Prior.CreationDate
                  BETWEEN DATEADD(dd, DATEDIFF(dd, 0, UH.CreationDate), -1)
                  AND DATEADD(dd, DATEDIFF(dd, 0, UH.CreationDate), 0)), 0) AS NoBreak
      FROM dbo.UserHistory AS UH) AS Consecutive
)
SELECT UserID, MIN(CreationDate) AS RunStart, MAX(CreationDate) AS RunEnd
FROM Runs
GROUP BY UserID, RunNumber
HAVING DATEDIFF(dd, MIN(CreationDate), MAX(CreationDate)) >= @Days

Es probable que lo anterior sea MUCHO MÁS DIFÍCIL de lo que debe ser, pero se deja como un cosquilleo cerebral para cuando tienes alguna otra definición de "una carrera" que solo las fechas.


2

Un par de opciones de SQL Server 2012 (suponiendo N = 100 a continuación).

;WITH T(UserID, NRowsPrevious)
     AS (SELECT UserID,
                DATEDIFF(DAY, 
                        LAG(CreationDate, 100) 
                            OVER 
                                (PARTITION BY UserID 
                                     ORDER BY CreationDate), 
                         CreationDate)
         FROM   UserHistory)
SELECT DISTINCT UserID
FROM   T
WHERE  NRowsPrevious = 100 

Aunque con mis datos de muestra, lo siguiente resultó más eficiente

;WITH U
         AS (SELECT DISTINCT UserId
             FROM   UserHistory) /*Ideally replace with Users table*/
    SELECT UserId
    FROM   U
           CROSS APPLY (SELECT TOP 1 *
                        FROM   (SELECT 
                                       DATEDIFF(DAY, 
                                                LAG(CreationDate, 100) 
                                                  OVER 
                                                   (ORDER BY CreationDate), 
                                                 CreationDate)
                                FROM   UserHistory UH
                                WHERE  U.UserId = UH.UserID) T(NRowsPrevious)
                        WHERE  NRowsPrevious = 100) O

Ambos se basan en la restricción establecida en la pregunta de que hay como máximo un registro por día por usuario.


1

¿Algo como esto?

select distinct userid
from table t1, table t2
where t1.UserId = t2.UserId 
  AND trunc(t1.CreationDate) = trunc(t2.CreationDate) + n
  AND (
    select count(*)
    from table t3
    where t1.UserId  = t3.UserId
      and CreationDate between trunc(t1.CreationDate) and trunc(t1.CreationDate)+n
   ) = n

1

Utilicé una propiedad matemática simple para identificar quién accedió consecutivamente al sitio. Esta propiedad es que debe tener la diferencia de días entre el primer acceso y la última vez igual al número de registros en el registro de la tabla de acceso.

Aquí hay un script SQL que probé en Oracle DB (también debería funcionar en otros DB):

-- show basic understand of the math properties 
  select    ceil(max (creation_date) - min (creation_date))
              max_min_days_diff,
           count ( * ) real_day_count
    from   user_access_log
group by   user_id;


-- select all users that have consecutively accessed the site 
  select   user_id
    from   user_access_log
group by   user_id
  having       ceil(max (creation_date) - min (creation_date))
           / count ( * ) = 1;



-- get the count of all users that have consecutively accessed the site 
  select   count(user_id) user_count
    from   user_access_log
group by   user_id
  having   ceil(max (creation_date) - min (creation_date))
           / count ( * ) = 1;

Script de preparación de tabla:

-- create table 
create table user_access_log (id           number, user_id      number, creation_date date);


-- insert seed data 
insert into user_access_log (id, user_id, creation_date)
  values   (1, 12, sysdate);

insert into user_access_log (id, user_id, creation_date)
  values   (2, 12, sysdate + 1);

insert into user_access_log (id, user_id, creation_date)
  values   (3, 12, sysdate + 2);

insert into user_access_log (id, user_id, creation_date)
  values   (4, 16, sysdate);

insert into user_access_log (id, user_id, creation_date)
  values   (5, 16, sysdate + 1);

insert into user_access_log (id, user_id, creation_date)
  values   (6, 16, sysdate + 5);

1
declare @startdate as datetime, @days as int
set @startdate = cast('11 Jan 2009' as datetime) -- The startdate
set @days = 5 -- The number of consecutive days

SELECT userid
      ,count(1) as [Number of Consecutive Days]
FROM UserHistory
WHERE creationdate >= @startdate
AND creationdate < dateadd(dd, @days, cast(convert(char(11), @startdate, 113)  as datetime))
GROUP BY userid
HAVING count(1) >= @days

La declaración cast(convert(char(11), @startdate, 113) as datetime)elimina la parte de hora de la fecha, por lo que comenzamos a medianoche.

Supongo también que las columnas creationdatey useridestán indexadas.

Me acabo de dar cuenta de que esto no le dirá a todos los usuarios y sus días consecutivos totales. Pero le diremos qué usuarios habrán estado visitando un número determinado de días desde la fecha de su elección.

Solución revisada:

declare @days as int
set @days = 30
select t1.userid
from UserHistory t1
where (select count(1) 
       from UserHistory t3 
       where t3.userid = t1.userid
       and t3.creationdate >= DATEADD(dd, DATEDIFF(dd, 0, t1.creationdate), 0) 
       and t3.creationdate < DATEADD(dd, DATEDIFF(dd, 0, t1.creationdate) + @days, 0) 
       group by t3.userid
) >= @days
group by t1.userid

He comprobado esto y consultará a todos los usuarios y todas las fechas. Se basa en la primera solución de Spencer (¿broma?) , Pero la mía funciona.

Actualización: mejoró el manejo de la fecha en la segunda solución.


cerca, pero necesitamos algo que funcione para cualquier período de (n) días, no en una fecha de inicio fija
Jeff Atwood

0

Esto debería hacer lo que quiere, pero no tengo suficientes datos para probar la eficiencia. Lo complicado de CONVERTIR / PISO es quitar la porción de tiempo del campo de fecha y hora. Si está utilizando SQL Server 2008, podría usar CAST (x.CreationDate AS DATE).

DECLARE @Range como INT
SET @Range = 10

SELECCIONAR ID DE USUARIO DISTINTO, CONVERTIR (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate)))
  DESDE tblUserInicie sesión a
DONDE EXISTE
   (SELECCIONE 1 
      DESDE tblUserLogin b 
     DONDE a.userId = b.userId 
       Y (SELECCIONAR CONTEO (DISTINCT (CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, CreationDate)))))) 
              DESDE tblUserLogin c 
             DONDE c.userid = b.userid 
               Y CONVERTIR (DATETIME, FLOOR (CONVERT (FLOAT, c.CreationDate))) ENTRE CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate))) y CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a )CreationD) ) + @ Range-1) = @Range)

Guion de creacion

CREATE TABLE [dbo]. [TblUserLogin] (
    [Id] [int] IDENTIDAD (1,1) NO NULL,
    [UserId] [int] NULL,
    [Fecha de creación] [fecha y hora] NULL
) EN [PRIMARIO]

bastante brutal 26 segundos en 406,624 filas.
Jeff Atwood

¿Con qué frecuencia está revisando para otorgar la insignia? Si es solo una vez al día, un golpe de 26 segundos en un período lento no parece tan malo. Aunque, el rendimiento se ralentizará a medida que la tabla crezca. Después de releer la pregunta, el tiempo puede no ser relevante ya que solo hay un registro por día.
Dave Barker

0

Spencer casi lo hizo, pero este debería ser el código de trabajo:

SELECT DISTINCT UserId
FROM History h1
WHERE (
    SELECT COUNT(*) 
    FROM History
    WHERE UserId = h1.UserId AND CreationDate BETWEEN h1.CreationDate AND DATEADD(d, @n-1, h1.CreationDate)
) >= @n

0

Fuera de mi cabeza, MySQLish:

SELECT start.UserId
FROM UserHistory AS start
  LEFT OUTER JOIN UserHistory AS pre_start ON pre_start.UserId=start.UserId
    AND DATE(pre_start.CreationDate)=DATE_SUB(DATE(start.CreationDate), INTERVAL 1 DAY)
  LEFT OUTER JOIN UserHistory AS subsequent ON subsequent.UserId=start.UserId
    AND DATE(subsequent.CreationDate)<=DATE_ADD(DATE(start.CreationDate), INTERVAL 30 DAY)
WHERE pre_start.Id IS NULL
GROUP BY start.Id
HAVING COUNT(subsequent.Id)=30

No probado, y casi seguramente necesita alguna conversión para MSSQL, pero creo que eso da algunas ideas.


0

¿Qué tal uno usando las tablas de Tally? Sigue un enfoque más algorítmico, y el plan de ejecución es muy sencillo. Rellene la tabla de conteo con números del 1 al 'MaxDaysBehind' que desea escanear la tabla (es decir, 90 buscará 3 meses de retraso, etc.).

declare @ContinousDays int
set @ContinousDays = 30  -- select those that have 30 consecutive days

create table #tallyTable (Tally int)
insert into #tallyTable values (1)
...
insert into #tallyTable values (90) -- insert numbers for as many days behind as you want to scan

select [UserId],count(*),t.Tally from HistoryTable 
join #tallyTable as t on t.Tally>0
where [CreationDate]> getdate()-@ContinousDays-t.Tally and 
      [CreationDate]<getdate()-t.Tally 
group by [UserId],t.Tally 
having count(*)>=@ContinousDays

delete #tallyTable

0

Ajustando un poco la consulta de Bill. Es posible que deba truncar la fecha antes de agrupar para contar solo un inicio de sesión por día ...

SELECT UserId from History 
WHERE CreationDate > ( now() - n )
GROUP BY UserId, 
DATEADD(dd, DATEDIFF(dd, 0, CreationDate), 0) AS TruncatedCreationDate  
HAVING COUNT(TruncatedCreationDate) >= n

EDITADO para usar DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) en lugar de convertir (char (10), CreationDate, 101).

@IDisposable Estaba buscando usar datepart antes, pero era demasiado vago para buscar la sintaxis, así que pensé que id usar convert en su lugar. No sé que tuvo un impacto significativo ¡Gracias! ahora sé.


Truncar un DATETIME de SQL a solo fecha se hace mejor con DATEADD (dd, DATEDIFF (dd, 0, UH.CreationDate), 0)
ID no disponible el

(lo anterior funciona tomando la diferencia en días enteros entre 0 (por ejemplo, 1900-01-01 00: 00: 00.000) y luego agregando esa diferencia en días enteros de nuevo a 0 (por ejemplo, 1900-01-01 00:00:00) Esto da como resultado que se descarte la porción de tiempo de DATETIME)
disponible el

0

asumiendo un esquema que dice así:

create table dba.visits
(
    id  integer not null,
    user_id integer not null,
    creation_date date not null
);

esto extraerá rangos contiguos de una secuencia de fechas con espacios en blanco.

select l.creation_date  as start_d, -- Get first date in contiguous range
    (
        select min(a.creation_date ) as creation_date 
        from "DBA"."visits" a 
            left outer join "DBA"."visits" b on 
                   a.creation_date = dateadd(day, -1, b.creation_date ) and 
                   a.user_id  = b.user_id 
            where b.creation_date  is null and
                  a.creation_date  >= l.creation_date  and
                  a.user_id  = l.user_id 
    ) as end_d -- Get last date in contiguous range
from  "DBA"."visits" l
    left outer join "DBA"."visits" r on 
        r.creation_date  = dateadd(day, -1, l.creation_date ) and 
        r.user_id  = l.user_id 
    where r.creation_date  is null
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.