SQL para determinar os dias sequenciais mínimos de acesso?

125

A tabela Histórico do usuário a seguir contém um registro para cada dia que um determinado usuário acessa um site (em um período UTC de 24 horas). Possui muitos milhares de registros, mas apenas um registro por dia por usuário. Se o usuário não tiver acessado o site naquele dia, nenhum registro será gerado.

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

O que estou procurando é uma consulta SQL nesta tabela com bom desempenho , que me diga quais IDs de usuário acessaram o site por (n) dias contínuos sem perder um dia.

Em outras palavras, quantos usuários possuem (n) registros nesta tabela com datas seqüenciais (dia antes ou depois) ? Se algum dia estiver faltando na sequência, a sequência será interrompida e deverá reiniciar novamente em 1; estamos procurando usuários que tenham alcançado um número contínuo de dias aqui sem lacunas.

Qualquer semelhança entre esta consulta e um distintivo Stack Overflow específico é pura coincidência, é claro .. :)

Jeff Atwood
fonte
Recebi o distintivo de entusiasta após 28 (<30) dias de associação. Misticismo.
224 Kirill V. Lyadvinsky
3
A sua data está armazenada como UTC? Em caso afirmativo, o que acontece se um residente da CA visitar o site às 8h em um dia e depois às 20h no dia seguinte? Embora ele visite em dias consecutivos no fuso horário do Pacífico, ele não seria registrado como tal no banco de dados porque o banco de dados está armazenando os horários como UTC.
266/09 Guy
Jeff / Jarrod - você pode conferir meta.stackexchange.com/questions/865/… por favor?
Rob Farley

Respostas:

69

A resposta é 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:

Ok, aqui está a minha resposta séria:

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 é uma ótima solução rápida e merece ser aceita, mas a solução de Rob Farley também é excelente e sem dúvida ainda mais rápida (!). Por favor, confira também!

Spencer Ruport
fonte
@ Artigo: Foi o que eu pensei inicialmente, mas quando eu pensei sobre isso, se você tiver um índice em (UserId, CreationDate), os registros aparecerão consecutivamente no índice e devem ter um bom desempenho.
Mehrdad Afshari
Upvote para este, estou obtendo resultados em ~ 15 segundos em 500k linhas.
Jim T
4
Trunque o CreateionDate até dias em todos esses testes (apenas no lado direito ou você mata o SARG) usando DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) Isso funciona subtraindo a data fornecida de zero - que é a Microsoft O SQL Server interpreta como 1900-01-01 00:00:00 e fornece o número de dias. Este valor é então adicionado novamente à data zero, produzindo a mesma data com o tempo truncado.
IDisposable
1
Tudo o que posso dizer é que, sem a alteração do IDisposable, o cálculo está incorreto . Eu pessoalmente validei os dados pessoalmente. Alguns usuários com 1 lacunas dia IRIA obter o emblema incorretamente.
911 Jeff Atwood
3
Esta consulta pode perder uma visita que ocorre às 23: 59: 59,5 - que tal alterá-la ON uh2.CreationDate >= uh1.CreationDate AND uh2.CreationDate < DATEADD(dd, DATEDIFF(dd, 0, uh1.CreationDate) + @days, 0)para:, para significar "Ainda não no 31º dia depois". Também significa que você pode pular o cálculo @seconds.
Rob Farley
147

Que tal (e verifique se a declaração anterior terminou com ponto e vírgula):

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  

A idéia é que, se tivermos uma lista dos dias (como um número) e um número de linha, os dias perdidos tornarão o deslocamento entre essas duas listas um pouco maior. Então, estamos procurando um intervalo que tenha um deslocamento consistente.

Você pode usar "ORDER BY NumConsecutiveDays DESC" no final deste tópico ou dizer "TENDO contagem (*)> 14" para um limite ...

Eu ainda não testei isso - apenas escrevi em cima da minha cabeça. Espero que funcione no SQL2005 e assim por diante.

... e seria muito ajudado por um índice no tablename (UserID, CreationDate)

Editado: Acontece que Offset é uma palavra reservada, então usei TheOffset.

Editado: a sugestão de usar COUNT (*) é muito válida - eu deveria ter feito isso em primeiro lugar, mas não estava pensando. Anteriormente, ele usava datediff (dia, min (CreationDate), max (CreationDate)).

Roubar

Rob Farley
fonte
1
oh você também deve adicionar; antes com ->; with
Mladen Prajdic
2
Mladen - não, você deve terminar a declaração anterior com ponto e vírgula. ;) Jeff - Ok, coloque [Offset]. Eu acho que Offset é uma palavra reservada. Como eu disse, não testei.
24411 Rob Farley
1
Apenas me repetindo, porque esse é um problema frequentemente visto. Trunque o CreateionDate até dias em todos esses testes (apenas no lado direito ou você mata o SARG) usando DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) Isso funciona subtraindo a data fornecida de zero - que é a Microsoft O SQL Server interpreta como 1900-01-01 00:00:00 e fornece o número de dias. Este valor é adicionado novamente à data zero, produzindo a mesma data com o tempo truncado.
IDisposable
1
IDisposable - sim, eu faço isso frequentemente. Só não me preocupei em fazer isso aqui. Não seria mais rápido do que convertê-lo para um int, mas tem a flexibilidade de contar horas, meses, o que for.
24411 Rob Farley
1
Acabei de escrever um post sobre como resolver isso com DENSE_RANK () também. tinyurl.com/denserank
Rob Farley
18

Se você pode alterar o esquema da tabela, sugiro adicionar uma coluna LongestStreakà tabela que você definiria para o número de dias sequenciais que terminam em CreationDate. É fácil atualizar a tabela no momento do login (semelhante ao que você já está fazendo, se não houver linhas no dia atual, você verificará se existe alguma linha no dia anterior. Se for verdade, você aumentará o valor LongestStreakno nova linha, caso contrário, você a definirá como 1.)

A consulta será óbvia depois de adicionar esta coluna:

if exists(select * from table
          where LongestStreak >= 30 and UserId = @UserId)
   -- award the Woot badge.
Mehrdad Afshari
fonte
1
+1 eu estava tendo um pensamento semelhante, mas com um campo de bits (IsConsecutive) que seria 1 se houver um registro para o dia anterior, caso contrário 0.
Fredrik Mörk
7
nós não vamos mudar o esquema para este
Jeff Atwood
E o IsConsecutive pode ser uma coluna computada definida na tabela UserHistory. Você também pode transformá-la em uma coluna computada materializada (armazenada) criada quando a linha é inserida IFF (if e ONLY if). Você sempre insere as linhas em ordem cronológica.
IDisposable
(porque ninguém iria fazer um SELECT *, sabemos adicionar esta coluna computada não afetará os planos de consulta, a menos que a coluna é referenciado ... caras certos?!?)
IDisposable
3
é definitivamente uma solução válida, mas não é o que eu pedi. Então, eu dar-lhe um "thumbs Sideways" ..
Jeff Atwood
6

Alguns SQL bem expressivos ao longo das linhas de:

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

Assumindo que você tenha uma função agregada definida pelo usuário, algo como o seguinte (cuidado com isso é um buggy):

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);
        }
    }
}
Joshuamck
fonte
4

Parece que você poderia tirar proveito do fato de que para ser contínuo por n dias exigiria n linhas.

Então, algo como:

SELECT users.UserId, count(1) as cnt
FROM users
WHERE users.CreationDate > now() - INTERVAL 30 DAY
GROUP BY UserId
HAVING cnt = 30
Conta
fonte
sim, nós podemos porta -lo pelo número de registros, com certeza .. mas que só elimina algumas possibilidades, como poderíamos ter 120 dias de visitar em vários anos com muitas lacunas diárias
Jeff Atwood
1
Tudo bem, mas depois de acompanhar a concessão desta página, você só precisa executá-la uma vez por dia. Eu acho que, nesse caso, algo como o acima faria o truque. Para acompanhar, tudo o que você precisa fazer é transformar a cláusula WHERE em uma janela deslizante usando BETWEEN.
Bill
1
cada execução da tarefa é apátrida e independente; não tem conhecimento de execuções anteriores além da tabela da pergunta
Jeff Atwood
3

Fazer isso com uma única consulta SQL parece muito complicado para mim. Deixe-me dividir esta resposta em duas partes.

  1. O que você deveria ter feito até agora e deve começar a fazer agora:
    execute uma tarefa cron diária que verifique se todos os usuários estão logados hoje e, em seguida, incrementa um contador se ele tiver ou o define como 0 se não tiver.
  2. O que você deve fazer agora:
    - Exporte esta tabela para um servidor que não executa o site e não será necessário por um tempo. ;)
    - Classifique por usuário e data.
    - passe por isso sequencialmente, mantenha um contador ...
Kim Stebel
fonte
podemos escrever código para query-and-loop, isso é ... desconfio eu digo ... trivial. Estou curioso sobre o SQL único caminho no momento.
911 Jeff Atwood
2

Se isso é tão importante para você, crie esse evento e conduza uma tabela para fornecer essas informações. Não há necessidade de matar a máquina com todas essas perguntas malucas.


fonte
2

Você pode usar um 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
Pôneis OMG
fonte
2

Joe Celko tem um capítulo completo sobre isso no SQL for Smarties (chamando de Execuções e Sequências). Eu não tenho esse livro em casa, então, quando eu chegar ao trabalho ... na verdade eu vou responder isso. (supondo que a tabela de histórico seja chamada dbo.UserHistory e o número de dias seja @Days)

Outra vantagem é de blog SQL Team sobre execuções

A outra idéia que tive, mas não tenho um servidor SQL à mão para trabalhar aqui, é usar um CTE com um 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

O exposto acima provavelmente é MUITO MAIS DURO do que deveria ser, mas deixou como um cócegas no cérebro quando você tem alguma outra definição de "uma corrida" além de datas.

IDisposable
fonte
2

Algumas opções do SQL Server 2012 (assumindo N = 100 abaixo).

;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 

Embora com meus dados de amostra o seguinte tenha funcionado de maneira mais 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 contam com a restrição declarada na pergunta de que há no máximo um registro por dia por usuário.

Martin Smith
fonte
1

Algo assim?

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
John Nilsson
fonte
1

Usei uma propriedade matemática simples para identificar quem acessou o site consecutivamente. Essa propriedade é que você deve ter a diferença do dia entre o primeiro acesso e a última vez igual ao número de registros no log da tabela de acesso.

Aqui estão os scripts SQL que eu testei no Oracle DB (ele também deve funcionar em outros DBs):

-- 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 preparação da tabela:

-- 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);
Dilshod Tadjibaev
fonte
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

A declaração cast(convert(char(11), @startdate, 113) as datetime) remove a parte da hora da data e, portanto, começamos à meia-noite.

Eu assumiria também que o creationdateeuserid colunas são indexadas.

Acabei de perceber que isso não informa todos os usuários e o total de dias consecutivos. Mas informará quais usuários visitarão um número definido de dias a partir da data de sua escolha.

Solução 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

Eu verifiquei isso e ele consultará todos os usuários e todas as datas. É baseado na 1ª solução de Spencer (piada?) , Mas a minha funciona.

Atualização: aprimorou a manipulação de datas na segunda solução.

Stephen Perelson
fonte
perto, mas precisamos de algo que funciona para qualquer (n) dias, e não em uma data de início fixa
Jeff Atwood
0

Isso deve fazer o que você deseja, mas não tenho dados suficientes para testar a eficiência. O material complicado CONVERT / FLOOR é retirar a parte do tempo do campo de data e hora. Se você estiver usando o SQL Server 2008, poderá usar o CAST (x.CreationDate AS DATE).

DECLARAR @Range como INT
SET @Range = 10

SELECT DISTINCT UserId, CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate))))
  De tblUserLogin a
ONDE EXISTE
   (SELECIONE 1 
      FROM tblUserLogin b 
     WHERE a.userId = b.userId 
       AND (SELECT COUNT (DISTINCT (CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, CreationDate))))) 
              FROM tblUserLogin c 
             WHERE c.userid = b.userid 
               AND CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, c.CreationDate))) ENTRE CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate))) e CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate)) ) + @ Intervalo-1) = @Range)

Script de criação

CREATE TABLE [dbo]. [TblUserLogin] (
    [Id] [int] IDENTIDADE (1,1) NÃO NULL,
    [UserId] [int] NULL,
    [CreationDate] [datetime] NULL
) ON [PRIMÁRIO]
Dave Barker
fonte
bastante brutal. 26 segundos em 406.624 linhas.
911 Jeff Atwood
Com que frequência você está checando para atribuir o distintivo? Se é apenas uma vez por dia, um golpe de 26 segundos em um período lento não parece tão ruim. Embora o desempenho diminua à medida que a tabela cresce. Após reler a pergunta, o tempo pode não ser relevante, pois há apenas um registro por dia.
24430 Dave Barker
0

Spencer quase fez isso, mas este deveria ser o código de trabalho:

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
Recep
fonte
0

Em cima da minha cabeça, 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

Não testado e quase certamente precisa de alguma conversão para o MSSQL, mas acho que isso dá algumas idéias.

Cebjyre
fonte
0

Que tal alguém usando tabelas Tally? Ele segue uma abordagem mais algorítmica e o plano de execução é fácil. Preencha o tallyTable com números de 1 a 'MaxDaysBehind' que você deseja escanear na tabela (ou seja, 90 ficará com 3 meses de atraso, 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
Radu094
fonte
0

Ajustando um pouco a consulta de Bill. Pode ser necessário truncar a data antes do agrupamento para contar apenas um login por dia ...

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) em vez de converter (char (10), CreationDate, 101).

@IDisposable Eu estava procurando usar o datepart anteriormente, mas estava com preguiça de procurar a sintaxe, então imaginei usar o id convert. Eu sei que teve um impacto significativo Obrigado! agora eu sei.

Jaskirat
fonte
Truncar um DATETIME SQL até o momento somente é o melhor feito com DATEADD (dd, DATEDIFF (dd, 0, UH.CreationDate), 0)
IDisposable
(o procedimento acima funciona tomando a diferença em dias inteiros entre 0 (por exemplo, 1900-01-01 00: 00: 00.000) e adicionando essa diferença em dias inteiros de volta a 0 (por exemplo, 1900-01-01 00:00:00) Isso resulta no descarte da parte do tempo de DATETIME) #
IDisposable
0

assumindo um esquema que seja como:

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

isso extrairá intervalos contíguos de uma sequência de datas com intervalos.

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
Vincent Buck
fonte