SQL per determinare i giorni sequenziali minimi di accesso?


125

La seguente tabella Cronologia utente contiene un record per ogni giorno in cui un determinato utente ha avuto accesso a un sito Web (in un periodo UTC di 24 ore). Ha molte migliaia di record, ma solo un record al giorno per utente. Se l'utente non ha effettuato l'accesso al sito Web per quel giorno, non verrà generato alcun record.

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

Quello che sto cercando è una query SQL su questa tabella con buone prestazioni , che mi dice quali userid hanno avuto accesso al sito Web per (n) giorni continui senza perdere un giorno.

In altre parole, quanti utenti hanno (n) record in questa tabella con date sequenziali (il giorno prima o il giorno dopo) ? Se un giorno manca dalla sequenza, la sequenza viene interrotta e dovrebbe ricominciare da 1; stiamo cercando utenti che hanno raggiunto un numero continuo di giorni qui senza lacune.

Qualsiasi somiglianza tra questa query e un particolare badge Stack Overflow è puramente casuale, ovviamente .. :)


Ho ottenuto il badge entusiasta dopo 28 (<30) giorni di abbonamento. Misticismo.
Kirill V. Lyadvinsky,

3
La tua data è memorizzata come UTC? In tal caso, cosa succede se un residente della CA visita il sito alle 8 del mattino un giorno e poi alle 20:00 del giorno seguente? Sebbene lui / lei visiti in giorni consecutivi nel fuso orario del Pacifico, non verrà registrato come tale nel DB perché il DB memorizza i tempi come UTC.
Guy

Jeff / Jarrod - puoi consultare meta.stackexchange.com/questions/865/… per favore?
Rob Farley,

Risposte:


69

La risposta è ovviamente:

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

MODIFICARE:

Ok, ecco la mia risposta 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

MODIFICARE:

[Jeff Atwood] Questa è un'ottima soluzione veloce e merita di essere accettata, ma la soluzione di Rob Farley è anche eccellente e probabilmente anche più veloce (!). Dai un'occhiata anche a te!


@Artem: Questo è quello che inizialmente pensavo, ma quando ci ho pensato, se hai un indice su (UserId, CreationDate), i record verranno visualizzati consecutivamente nell'indice e dovrebbe funzionare bene.
Mehrdad Afshari,

A questo proposito, sto ottenendo risultati in circa 15 secondi su 500.000 righe.
Jim T,

4
Troncare la Data di creazione fino a giorni in tutti questi test (solo a destra o si uccide SARG) utilizzando DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) Funziona sottraendo la data fornita da zero - che Microsoft SQL Server interpreta 1900-01-01 00:00:00 e indica il numero di giorni. Questo valore viene quindi aggiunto nuovamente alla data zero, restituendo la stessa data con l'ora troncata.
IDisposibile il

1
tutto quello che posso dirti è che, senza la modifica di IDisposable, il calcolo non è corretto . Personalmente ho convalidato i dati da solo. Alcuni utenti con intervalli di 1 giorno VERRANNO ottenere il badge in modo errato.
Jeff Atwood,

3
Questa query ha il potenziale di perdere una visita che si verifica in 23: 59: 59.5 - che ne dici di cambiarla ON uh2.CreationDate >= uh1.CreationDate AND uh2.CreationDate < DATEADD(dd, DATEDIFF(dd, 0, uh1.CreationDate) + @days, 0)in:, per dire "Non ancora il 31 ° giorno dopo". Inoltre, puoi saltare il calcolo @seconds.
Rob Farley,

147

Che ne dici (e assicurati che la frase precedente sia terminata con un punto e virgola):

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  

L'idea è che se abbiamo un elenco dei giorni (come un numero) e un numero di riga, i giorni mancati aumentano leggermente l'offset tra questi due elenchi. Quindi stiamo cercando una gamma che abbia un offset costante.

È possibile utilizzare "ORDER BY NumConsecutiveDays DESC" alla fine di questo, oppure pronunciare "HAVING count (*)> 14" per una soglia ...

Non l'ho ancora provato, ma lo sto scrivendo dalla cima della mia testa. Speriamo che funzioni in SQL2005 e successivi.

... e sarebbe molto aiutato da un indice su tablename (UserID, CreationDate)

Modificato: risulta che Offset è una parola riservata, quindi ho usato TheOffset.

A cura di: il suggerimento di utilizzare COUNT (*) è molto valido - in primo luogo avrei dovuto farlo ma non ci pensavo davvero. In precedenza utilizzava invece dateiff (giorno, min (CreationDate), max (CreationDate)).

rapinare


1
oh dovresti anche aggiungere; prima con ->; con
Mladen Prajdic il

2
Mladen - no, dovresti terminare la frase precedente con un punto e virgola. ;) Jeff - Ok, metti invece [Offset]. Immagino che Offset sia una parola riservata. Come ho detto, non l'avevo provato.
Rob Farley,

1
Mi sto solo ripetendo, perché questo è un problema spesso visto. Troncare la Data di creazione fino a giorni in tutti questi test (solo a destra o si uccide SARG) utilizzando DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) Funziona sottraendo la data fornita da zero - che Microsoft SQL Server interpreta 1900-01-01 00:00:00 e indica il numero di giorni. Questo valore viene quindi aggiunto nuovamente alla data zero, restituendo la stessa data con l'ora troncata.
IDisposibile il

1
IDisposable - sì, lo faccio spesso da solo. Non mi preoccupavo che lo facesse qui. Non sarebbe più veloce di lanciarlo su un int, ma ha la flessibilità di contare ore, mesi, qualunque cosa.
Rob Farley,

1
Ho appena scritto un post sul blog per risolvere questo problema anche con DENSE_RANK (). tinyurl.com/denserank
Rob Farley,

18

Se puoi modificare lo schema della tabella, ti suggerirei di aggiungere una colonna LongestStreakalla tabella che avresti impostato sul numero di giorni sequenziali che terminano con il CreationDate. È facile aggiornare la tabella al momento dell'accesso (simile a quello che stai già facendo, se non esistono righe del giorno corrente, controllerai se esiste una riga per il giorno precedente. Se vero, aumenterai ilLongestStreak nel nuova riga, altrimenti la imposterai su 1.)

La query sarà ovvia dopo aver aggiunto questa colonna:

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

1
+1 Stavo avendo un pensiero simile, ma con un po 'di campo (IsConsecutivo) che sarebbe 1 se ci fosse un record per il giorno precedente, altrimenti 0.
Fredrik Mörk,

7
non cambieremo lo schema per questo
Jeff Atwood,

E IsConsecutive può essere una colonna calcolata definita nella tabella UserHistory. Potresti anche renderlo una colonna calcolata materializzata (memorizzata) che viene creata quando si inserisce la riga IFF (se e SOLO se) si inseriscono sempre le righe in ordine cronologico.
IDisposibile il

(poiché NOBODY farebbe un SELECT *, sappiamo che l'aggiunta di questa colonna calcolata non influirà sui piani di query a meno che la colonna non sia referenziata ... giusto ragazzi?!?)
IDisposable

3
è sicuramente una soluzione valida ma non è quello che ho chiesto. Quindi gli do un "pollice di lato" ..
Jeff Atwood,

6

Alcuni SQL ben espressivi sulla falsariga di:

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

Supponendo che tu abbia una funzione aggregata definita dall'utente qualcosa sulla falsariga di ( fai attenzione, questo è un bug):

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

Sembra che tu possa trarre vantaggio dal fatto che essere continui per n giorni richiederebbe che ci siano n file.

Quindi qualcosa come:

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

sì, possiamo sicuramente superare il numero di record, ma questo elimina solo alcune possibilità, dato che potremmo avere 120 giorni di visita in diversi anni con un sacco di lacune giornaliere
Jeff Atwood,

1
Va bene, ma una volta che sei stato colto dall'assegnazione di questa pagina, devi solo eseguirlo una volta al giorno. Penso che per quel caso, qualcosa come sopra avrebbe funzionato. Per recuperare, tutto ciò che devi fare è trasformare la clausola WHERE in una finestra scorrevole usando BETWEEN.
Bill,

1
ogni esecuzione dell'attività è stateless e standalone; non è a conoscenza di precedenti esecuzioni diverse dalla tabella nella domanda
Jeff Atwood,

3

Farlo con una singola query SQL mi sembra eccessivamente complicato. Vorrei suddividere questa risposta in due parti.

  1. Cosa avresti dovuto fare fino ad ora e iniziare a farlo ora:
    esegui un cron job giornaliero che controlla ogni utente che ha effettuato l'accesso oggi e quindi incrementa un contatore se lo ha o lo imposta su 0 se non lo ha fatto.
  2. Cosa dovresti fare ora:
    - Esporta questa tabella su un server che non esegue il tuo sito Web e non sarà necessario per un po '. ;)
    - Ordinalo per utente, quindi data.
    - attraversalo in sequenza, tieni un contatore ...

possiamo scrivere codice su query-and-loop, questo è ... dico, dico ... banale. Sono curioso di conoscere l'unico modo SQL al momento.
Jeff Atwood,

2

Se questo è così importante per te, procurati questo evento e guida una tabella per darti queste informazioni. Non c'è bisogno di uccidere la macchina con tutte quelle domande folli.


2

È possibile utilizzare un CTE ricorsivo (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 ha un capitolo completo su questo in SQL for Smarties (chiamandolo Runs and Sequences). Non ho quel libro a casa, quindi quando arrivo al lavoro ... risponderò davvero a questo. (supponendo che la tabella della cronologia si chiami dbo.UserHistory e il numero di giorni sia @Days)

Un altro vantaggio è dal blog del team SQL sulle corse

L'altra idea che ho avuto, ma non ho un server SQL a portata di mano su cui lavorare qui è usare un CTE con un ROW_NUMBER partizionato come questo:

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

Quanto sopra è probabilmente MODO PIÙ DURO di quanto deve essere, ma lasciato come un solletico cerebrale per quando hai qualche altra definizione di "una corsa" che solo date.


2

Un paio di opzioni di SQL Server 2012 (presupponendo N = 100 di seguito).

;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 

Sebbene con i miei dati di esempio, il seguente sia risultato più efficiente

;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

Entrambi si basano sul vincolo indicato nella domanda che esiste al massimo un record al giorno per utente.


1

Qualcosa come questo?

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

Ho usato una semplice proprietà matematica per identificare chi accedeva consecutivamente al sito. Questa proprietà è che si dovrebbe avere la differenza del giorno tra il primo accesso e l'ultima volta uguale al numero di record nel registro della tabella di accesso.

Ecco lo script SQL che ho testato in Oracle DB (dovrebbe funzionare anche in altri 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 di preparazione della tabella:

-- 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 dichiarazione cast(convert(char(11), @startdate, 113) as datetime)rimuove la parte di tempo della data in modo da iniziare a mezzanotte.

Suppongo anche che le colonne creationdatee useridsiano indicizzate.

Mi sono appena reso conto che questo non ti dirà tutti gli utenti e i loro giorni consecutivi totali. Ma ti dirà quali utenti visiteranno un determinato numero di giorni da una data di tua scelta.

Soluzione rivista:

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

Ho controllato questo e verrà interrogato per tutti gli utenti e tutte le date. Si basa sulla prima soluzione (scherzo?) Di Spencer , ma la mia funziona.

Aggiornamento: migliorata la gestione della data nella seconda soluzione.


vicino, ma abbiamo bisogno di qualcosa che funzioni per qualsiasi (n) giorno, non a una data di inizio fissa
Jeff Atwood

0

Questo dovrebbe fare quello che vuoi ma non ho abbastanza dati per testare l'efficienza. L'elemento convoluto CONVERT / FLOOR è di eliminare la porzione di tempo dal campo datetime. Se si utilizza SQL Server 2008, è possibile utilizzare CAST (x.CreationDate AS DATE).

DECLARE @Range as INT
SET @Range = 10

SELEZIONA ID utente DISTINCT, CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate)))
  DA tblUserLogin a
DOVE ESISTE
   (SELEZIONA 1 
      DA tblUserLogin b 
     DOVE a.userId = b.userId 
       E (SELEZIONA CONTEGGIO (DISTINCT (CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, CreationDate))))) 
              DA tblUserLogin c 
             DOVE c.userid = b.userid 
               E CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, c.CreationDate))) TRA CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate))) e CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate)) ) + @ Range-1) = @Range)

Script di creazione

CREATE TABLE [dbo]. [TblUserLogin] (
    [Id] [int] IDENTITY (1,1) NOT NULL,
    [UserId] [int] NULL,
    [CreationDate] [datetime] NULL
) SU [PRIMARIO]

piuttosto brutale. 26 secondi su 406.624 file.
Jeff Atwood,

Quanto spesso controlli per assegnare il badge? Se è solo una volta al giorno, un colpo di 26 secondi in un periodo lento non sembra poi così male. Anche se, le prestazioni rallenteranno man mano che la tabella cresce. Dopo aver riletto la domanda, il tempo potrebbe non essere rilevante in quanto esiste un solo record al giorno.
Dave Barker,

0

Spencer ci è quasi riuscito, ma questo dovrebbe essere il codice funzionante:

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

In cima alla mia testa, 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

Non testato, e quasi sicuramente necessita di qualche conversione per MSSQL, ma penso che dia qualche idea.


0

Che ne dici di usare una tabella Tally? Segue un approccio più algoritmico e il piano di esecuzione è un gioco da ragazzi. Compilare la tabella dei riscontri con i numeri da 1 a "MaxDaysBehind" che si desidera scansionare la tabella (ad es. 90 cercherà 3 mesi indietro, ecc.).

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

Ottimizzare un po 'la domanda di Bill. Potrebbe essere necessario troncare la data prima del raggruppamento per contare solo un accesso al giorno ...

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

EDITED per usare DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) invece di convert (char (10), CreationDate, 101).

@IDisposable Stavo cercando di usare datepart prima, ma ero troppo pigro per cercare la sintassi, quindi ho pensato di usare invece convert. Non so che abbia avuto un impatto significativo Grazie! adesso lo so.


Il troncamento di un DATETIME SQL solo a data viene eseguito meglio con DATEADD (dd, DATEDIFF (dd, 0, UH.CreationDate), 0)
IDisposable

(quanto sopra funziona prendendo la differenza in giorni interi tra 0 (ad esempio 1900-01-01 00: 00: 00.000) e quindi aggiungendo quella differenza in giorni interi a 0 (ad esempio 1900-01-01 00:00:00) Ciò comporta l'eliminazione della parte temporale del DATETIME)
IDisposibile

0

assumendo uno schema che assomigli a:

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

questo estrarrà intervalli contigui da una sequenza di date con spazi vuoti.

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.