SQL, um minimale aufeinanderfolgende Zugriffstage zu bestimmen?

125

Die folgende Benutzerverlaufstabelle enthält einen Datensatz für jeden Tag, an dem ein bestimmter Benutzer auf eine Website zugegriffen hat (innerhalb von 24 Stunden UTC). Es hat viele tausend Datensätze, aber nur einen Datensatz pro Tag und Benutzer. Wenn der Benutzer an diesem Tag nicht auf die Website zugegriffen hat, wird kein Datensatz generiert.

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

Was ich suche, ist eine SQL-Abfrage in dieser Tabelle mit guter Leistung , die mir sagt, welche Benutzer-IDs für (n) aufeinanderfolgende Tage auf die Website zugegriffen haben, ohne einen Tag zu verpassen.

Mit anderen Worten, wie viele Benutzer haben (n) Datensätze in dieser Tabelle mit aufeinander folgenden (Tag vor oder Tag nach) Daten ? Wenn ein Tag in der Sequenz fehlt, ist die Sequenz unterbrochen und sollte bei 1 erneut gestartet werden. Wir suchen Benutzer, die hier eine kontinuierliche Anzahl von Tagen ohne Lücken erreicht haben.

Jede Ähnlichkeit zwischen dieser Abfrage und einem bestimmten Stapelüberlauf-Abzeichen ist natürlich rein zufällig. :)

Jeff Atwood
quelle
Ich habe das Enthusiastenabzeichen nach 28 (<30) Tagen Mitgliedschaft erhalten. Mystik.
Kirill V. Lyadvinsky
3
Werden Ihre Daten als UTC gespeichert? Wenn ja, was passiert, wenn ein CA-Bewohner die Site an einem Tag um 8 Uhr und am nächsten Tag um 20 Uhr besucht? Obwohl er / sie an aufeinanderfolgenden Tagen in der pazifischen Zeitzone besucht, wird dies nicht als solches in der Datenbank aufgezeichnet, da die Datenbank Zeiten als UTC speichert.
Guy
Jeff / Jarrod - können Sie bitte meta.stackexchange.com/questions/865/… überprüfen ?
Rob Farley

Antworten:

69

Die Antwort lautet offensichtlich:

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

BEARBEITEN:

Okay, hier ist meine ernsthafte Antwort:

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

BEARBEITEN:

[Jeff Atwood] Dies ist eine großartige schnelle Lösung und verdient es, akzeptiert zu werden, aber Rob Farleys Lösung ist auch ausgezeichnet und wahrscheinlich sogar noch schneller (!). Bitte probieren Sie es auch aus!

Spencer Ruport
quelle
@Artem: Das habe ich anfangs gedacht, aber als ich darüber nachdachte, werden die Datensätze nacheinander im Index angezeigt, wenn Sie einen Index für (UserId, CreationDate) haben, und er sollte eine gute Leistung erbringen.
Mehrdad Afshari
Upvote für dieses, ich erhalte Ergebnisse in ~ 15 Sekunden auf 500k Zeilen zurück.
Jim T
4
Schneiden Sie das Erstellungsdatum in all diesen Tests (nur auf der rechten Seite oder Sie töten SARG) mit DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) auf Tage ab. Dies subtrahiert das angegebene Datum von Null - was Microsoft SQL Server interpretiert als 1900-01-01 00:00:00 und gibt die Anzahl der Tage an. Dieser Wert wird dann zum Nulldatum erneut addiert, was dasselbe Datum mit der abgeschnittenen Zeit ergibt.
IDisposable
1
Ich kann Ihnen nur sagen, dass die Berechnung ohne die Änderung von IDisposable falsch ist . Ich habe die Daten persönlich validiert. Einige Benutzer mit 1 Tag Lücken WüRDEN das Abzeichen falsch erhalten.
Jeff Atwood
3
Diese Abfrage kann einen Besuch verpassen, der um 23: 59: 59.5 Uhr stattfindet. Wie wäre es, wenn Sie ihn in: ändern ON uh2.CreationDate >= uh1.CreationDate AND uh2.CreationDate < DATEADD(dd, DATEDIFF(dd, 0, uh1.CreationDate) + @days, 0), was "Noch nicht am 31. Tag später" bedeutet. Dies bedeutet auch, dass Sie die Berechnung von @seconds überspringen können.
Rob Farley
147

Wie wäre es (und stellen Sie bitte sicher, dass die vorherige Aussage mit einem Semikolon endet):

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  

Die Idee ist, dass, wenn wir eine Liste der Tage (als Zahl) und eine Zeilennummer haben, verpasste Tage den Versatz zwischen diesen beiden Listen etwas größer machen. Wir suchen also nach einem Bereich mit einem konsistenten Versatz.

Sie können am Ende "ORDER BY NumConsecutiveDays DESC" verwenden oder "HAVING count (*)> 14" für einen Schwellenwert sagen ...

Ich habe das allerdings nicht getestet - schreibe es einfach von oben auf meinen Kopf. Funktioniert hoffentlich in SQL2005 und weiter.

... und würde sehr durch einen Index für Tabellennamen (UserID, CreationDate) unterstützt werden

Bearbeitet: Es stellt sich heraus, dass Offset ein reserviertes Wort ist, daher habe ich stattdessen TheOffset verwendet.

Bearbeitet: Der Vorschlag, COUNT (*) zu verwenden, ist sehr gültig - ich hätte das zuerst tun sollen, aber nicht wirklich nachgedacht. Zuvor wurde stattdessen dateiff (Tag, min (CreationDate), max (CreationDate)) verwendet.

rauben

Rob Farley
quelle
1
oh du solltest auch hinzufügen; vorher mit ->; mit
Mladen Prajdic
2
Mladen - nein, Sie sollten die vorherige Aussage mit einem Semikolon beenden. ;) Jeff - Ok, setze stattdessen [Offset]. Ich denke, Offset ist ein reserviertes Wort. Wie gesagt, ich hatte es nicht getestet.
Rob Farley
1
Ich wiederhole mich nur, weil dies ein oft gesehenes Problem ist. Schneiden Sie das Erstellungsdatum in all diesen Tests (nur auf der rechten Seite oder Sie töten SARG) mit DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) auf Tage ab. Dies subtrahiert das angegebene Datum von Null - was Microsoft SQL Server interpretiert als 1900-01-01 00:00:00 und gibt die Anzahl der Tage an. Dieser Wert wird dann zum Nulldatum erneut addiert, was dasselbe Datum mit der abgeschnittenen Zeit ergibt.
IDisposable
1
IDisposable - ja, das mache ich oft selbst. Ich habe mir hier einfach keine Sorgen gemacht. Es wäre nicht schneller als das Umwandeln in ein Int, hat aber die Flexibilität, Stunden, Monate oder was auch immer zu zählen.
Rob Farley
1
Ich habe gerade einen Blog-Beitrag über das Lösen dieses Problems mit DENSE_RANK () geschrieben. tinyurl.com/denserank
Rob Farley
18

Wenn Sie das Tabellenschema ändern können, würde ich vorschlagen LongestStreak, der Tabelle eine Spalte hinzuzufügen , die Sie auf die Anzahl der aufeinander folgenden Tage festlegen, die bis zum enden CreationDate. Es ist einfach, die Tabelle zum Zeitpunkt der Anmeldung zu aktualisieren (ähnlich wie Sie es bereits tun: Wenn am aktuellen Tag keine Zeilen vorhanden sind, überprüfen Sie, ob für den vorherigen Tag eine Zeile vorhanden ist. Wenn true, erhöhen Sie die LongestStreakin neue Zeile, sonst setzen Sie es auf 1.)

Die Abfrage wird nach dem Hinzufügen dieser Spalte offensichtlich:

if exists(select * from table
          where LongestStreak >= 30 and UserId = @UserId)
   -- award the Woot badge.
Mehrdad Afshari
quelle
1
+1 Ich hatte einen ähnlichen Gedanken, aber mit einem Bitfeld (IsConsecutive) wäre das 1, wenn es einen Rekord für den Vortag gibt, sonst 0.
Fredrik Mörk
7
Wir werden das Schema dafür nicht ändern
Jeff Atwood
Und IsConsecutive kann eine berechnete Spalte sein, die in der UserHistory-Tabelle definiert ist. Sie können es auch zu einer materialisierten (gespeicherten) berechneten Spalte machen, die beim Einfügen der Zeile erstellt wird. IFF (wenn und NUR wenn) Sie fügen die Zeilen immer in chronologischer Reihenfolge ein.
IDisposable
(Da NIEMAND ein SELECT * ausführen würde, wissen wir, dass das Hinzufügen dieser berechneten Spalte die Abfragepläne nicht beeinflusst, es sei denn, auf die Spalte wird verwiesen ... oder?!?)
IDisposable
3
Es ist definitiv eine gültige Lösung, aber es ist nicht das, wonach ich gefragt habe. Also gebe ich ihm einen "Daumen seitwärts".
Jeff Atwood
6

Einige gut ausdrucksstarke SQL in Anlehnung an:

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

Angenommen, Sie haben eine benutzerdefinierte Aggregatfunktion in der Art von (Vorsicht, dies ist fehlerhaft):

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
quelle
4

Scheint, als könnten Sie die Tatsache ausnutzen, dass für eine Kontinuität über n Tage n Zeilen erforderlich sind.

Also so etwas wie:

SELECT users.UserId, count(1) as cnt
FROM users
WHERE users.CreationDate > now() - INTERVAL 30 DAY
GROUP BY UserId
HAVING cnt = 30
Rechnung
quelle
ja, wir können Gatter es durch die Anzahl der Datensätze, sicher .. aber das nur beseitigt einige Möglichkeiten, wie wir 120 Tage Besuch über mehrere Jahre mit vielen täglichen Lücken haben könnten
Jeff Atwood
1
Okay, aber sobald Sie mit der Vergabe dieser Seite vertraut sind, müssen Sie sie nur noch einmal pro Tag ausführen. Ich denke für diesen Fall würde so etwas wie oben den Trick machen. Um aufzuholen, müssen Sie lediglich die WHERE-Klausel mit BETWEEN in ein Schiebefenster verwandeln.
Bill
1
Jeder Lauf der Aufgabe ist zustandslos und eigenständig. Es hat keine Kenntnis von früheren Läufen außer der Tabelle in der Frage
Jeff Atwood
3

Dies mit einer einzigen SQL-Abfrage zu tun, scheint mir zu kompliziert. Lassen Sie mich diese Antwort in zwei Teile aufteilen.

  1. Was Sie bis jetzt hätten tun sollen und jetzt anfangen sollten:
    Führen Sie einen täglichen Cron-Job aus, der für jeden Benutzer prüft, ob er sich heute angemeldet hat, und dann einen Zähler erhöht, wenn er ihn hat, oder ihn auf 0 setzt, wenn er dies nicht hat.
  2. Was Sie jetzt tun sollten:
    - Exportieren Sie diese Tabelle auf einen Server, auf dem Ihre Website nicht ausgeführt wird und der für eine Weile nicht benötigt wird. ;)
    - Sortiere es nach Benutzer, dann Datum.
    - nacheinander durchgehen, einen Zähler behalten ...
Kim Stebel
quelle
Wir können Code in Query-and-Loop schreiben, das ist ... dary ich sage ... trivial. Ich bin im Moment nur so neugierig auf SQL.
Jeff Atwood
2

Wenn dies für Sie so wichtig ist, geben Sie dieses Ereignis an und erstellen Sie eine Tabelle, um diese Informationen zu erhalten. Keine Notwendigkeit, die Maschine mit all diesen verrückten Fragen zu töten.


quelle
2

Sie können einen rekursiven CTE (SQL Server 2005+) verwenden:

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
OMG Ponys
quelle
2

Joe Celko hat ein vollständiges Kapitel dazu in SQL für Smarties (Runs and Sequences genannt). Ich habe das Buch nicht zu Hause, also wenn ich zur Arbeit komme ... werde ich das tatsächlich beantworten. (Angenommen, die Verlaufstabelle heißt dbo.UserHistory und die Anzahl der Tage ist @Days.)

Ein weiterer Hinweis stammt aus dem Blog von SQL Team über Läufe

Die andere Idee, die ich hatte, aber keinen SQL-Server zur Hand habe, ist die Verwendung eines CTE mit einer partitionierten ROW_NUMBER wie folgt:

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

Das Obige ist wahrscheinlich viel härter als es sein muss, aber es bleibt als Gehirnkitzel übrig, wenn Sie eine andere Definition von "einem Lauf" haben als nur Daten.

IDisposable
quelle
2

Einige SQL Server 2012-Optionen (unter der Annahme von N = 100 unten).

;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 

Mit meinen Beispieldaten hat sich Folgendes jedoch als effizienter erwiesen

;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

Beide stützen sich auf die in der Frage angegebene Einschränkung, dass es höchstens einen Datensatz pro Tag und Benutzer gibt.

Martin Smith
quelle
1

Etwas wie das?

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
quelle
1

Ich habe eine einfache mathematische Eigenschaft verwendet, um zu identifizieren, wer nacheinander auf die Site zugegriffen hat. Diese Eigenschaft besagt, dass die Tagesdifferenz zwischen dem ersten und dem letzten Zugriff gleich der Anzahl der Datensätze in Ihrem Zugriffstabellenprotokoll sein sollte.

Hier sind SQL-Skripte, die ich in Oracle DB getestet habe (es sollte auch in anderen DBs funktionieren):

-- 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;

Tabellenvorbereitungsskript:

-- 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
quelle
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

Die Aussage cast(convert(char(11), @startdate, 113) as datetime) entfernt den Zeitteil des Datums, sodass wir um Mitternacht beginnen.

Ich würde auch davon ausgehen, dass die creationdateunduserid Spalten indiziert sind.

Ich habe gerade festgestellt, dass dies nicht alle Benutzer und ihre gesamten aufeinander folgenden Tage anzeigt. Sie erfahren jedoch, welche Benutzer ab einem Datum Ihrer Wahl eine bestimmte Anzahl von Tagen besucht haben.

Überarbeitete Lösung:

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

Ich habe dies überprüft und es wird nach allen Benutzern und allen Daten abgefragt. Es basiert auf Spencers 1. (Scherz?) Lösung , aber meine funktioniert.

Update: Die Datumsverarbeitung in der zweiten Lösung wurde verbessert.

Stephen Perelson
quelle
schließen, aber wir brauchen etwas, das für jeden (n) Tag funktioniert, nicht an einem festen Starttermin
Jeff Atwood
0

Dies sollte tun, was Sie wollen, aber ich habe nicht genügend Daten, um die Effizienz zu testen. Das verschlungene CONVERT / FLOOR-Zeug besteht darin, den Zeitanteil aus dem Datum / Uhrzeit-Feld zu entfernen. Wenn Sie SQL Server 2008 verwenden, können Sie CAST (x.CreationDate AS DATE) verwenden.

DECLARE @Range als INT
SET @Range = 10

SELECT DISTINCT UserId, CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate)))
  FROM tblUserLogin a
WO EXISTIERT
   (AUSWÄHLEN 1 
      FROM tblUserLogin b 
     WO a.userId = b.userId 
       AND (SELECT COUNT (DISTINCT (CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, CreationDate)))) 
              FROM tblUserLogin c 
             WO c.userid = b.userid 
               UND KONVERTIEREN (DATETIME, FLOOR (CONVERT (FLOAT, c.CreationDate))) ZWISCHEN CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate))) und CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate)) ) + @ Range-1) = @Range)

Erstellungsskript

CREATE TABLE [dbo]. [TblUserLogin] (
    [Id] [int] IDENTITÄT (1,1) NICHT NULL,
    [UserId] [int] NULL,
    [CreationDate] [datetime] NULL
) ON [PRIMARY]
Dave Barker
quelle
ziemlich brutal. 26 Sekunden in 406.624 Zeilen.
Jeff Atwood
Wie oft überprüfen Sie, um das Abzeichen zu vergeben? Wenn es nur einmal am Tag ist, dann scheint ein 26-Sekunden-Treffer in einer langsamen Periode nicht so schlimm zu sein. Die Leistung wird sich jedoch verlangsamen, wenn der Tisch wächst. Nach dem erneuten Lesen der Frage ist das Entfernen der Frage möglicherweise nicht relevant, da nur ein Datensatz pro Tag vorhanden ist.
Dave Barker
0

Spencer hätte es fast geschafft, aber dies sollte der Arbeitscode sein:

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
quelle
0

Aus dem Kopf, 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

Ungetestet und braucht mit ziemlicher Sicherheit eine Konvertierung für MSSQL, aber ich denke, das gibt einige Ideen.

Cebjyre
quelle
0

Wie wäre es mit einer Tally-Tabelle? Es folgt einem algorithmischeren Ansatz, und der Ausführungsplan ist ein Kinderspiel. Füllen Sie die TallyTable mit Zahlen von 1 bis 'MaxDaysBehind', mit denen Sie die Tabelle scannen möchten (dh 90 wird 3 Monate zurückbleiben usw.).

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
quelle
0

Bills Abfrage ein wenig optimieren. Möglicherweise müssen Sie das Datum vor der Gruppierung abschneiden, um nur eine Anmeldung pro Tag zu zählen ...

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

BEARBEITET, um DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) anstelle von convert (char (10), CreationDate, 101) zu verwenden.

@IDisposable Ich wollte Datepart früher verwenden, war aber zu faul, um die Syntax nachzuschlagen, also dachte ich, ich würde stattdessen convert verwenden. Ich weiß nicht, dass es einen signifikanten Einfluss hatte. Danke! jetzt weiß ich.

Jaskirat
quelle
Das Abschneiden einer SQL-DATETIME auf ein Datum erfolgt am besten mit DATEADD (dd, DATEDIFF (dd, 0, UH.CreationDate), 0)
IDisposable
(Das Obige funktioniert, indem die Differenz in ganzen Tagen zwischen 0 (z. B. 1900-01-01 00: 00: 00.000) genommen und diese Differenz in ganzen Tagen wieder auf 0 addiert wird (z. B. 1900-01-01 00:00:00). Dies führt dazu, dass der Zeitteil der DATETIME verworfen wird.)
IDisposable
0

Angenommen, ein Schema lautet wie folgt:

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

Dadurch werden zusammenhängende Bereiche aus einer Datumssequenz mit Lücken extrahiert.

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
quelle