SQL pour déterminer les jours d'accès séquentiels minimum?

125

Le tableau suivant de l'historique des utilisateurs contient un enregistrement pour chaque jour où un utilisateur donné accède à un site Web (dans une période de 24 heures UTC). Il contient plusieurs milliers d'enregistrements, mais un seul enregistrement par jour et par utilisateur. Si l'utilisateur n'a pas accédé au site Web ce jour-là, aucun enregistrement ne sera généré.

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

Ce que je recherche, c'est une requête SQL sur cette table avec de bonnes performances , qui me dit quels userids ont accédé au site pendant (n) jours consécutifs sans manquer un jour.

En d'autres termes, combien d'utilisateurs ont (n) enregistrements dans cette table avec des dates séquentielles (jour avant ou après) ? Si un jour est absent de la séquence, la séquence est interrompue et doit recommencer à 1; nous recherchons des utilisateurs qui ont accompli un nombre continu de jours ici sans interruption.

Toute ressemblance entre cette requête et un badge Stack Overflow particulier est purement fortuite, bien sûr .. :)

Jeff Atwood
la source
J'ai obtenu le badge de passionné après 28 (<30) jours d'abonnement. Mysticisme.
Kirill V. Lyadvinsky le
3
Votre date est-elle enregistrée au format UTC? Si tel est le cas, que se passe-t-il si un résident de l'AC visite le site à 8 heures un jour, puis à 20 heures le lendemain? Bien qu'il / elle visite des jours consécutifs dans le fuseau horaire du Pacifique, cela ne sera pas enregistré comme tel dans la base de données car la base de données stocke les heures au format UTC.
Guy
Jeff / Jarrod - pouvez-vous consulter meta.stackexchange.com/questions/865/… s'il vous plaît?
Rob Farley

Réponses:

69

La réponse est évidemment:

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

ÉDITER:

Ok, voici ma réponse sérieuse:

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

ÉDITER:

[Jeff Atwood] C'est une excellente solution rapide et mérite d'être acceptée, mais la solution de Rob Farley est également excellente et sans doute encore plus rapide (!). S'il vous plaît vérifier aussi!

Spencer Ruport
la source
@Artem: C'est ce que je pensais au départ, mais quand j'y ai pensé, si vous avez un index sur (UserId, CreationDate), les enregistrements apparaîtront consécutivement dans l'index et cela devrait bien fonctionner.
Mehrdad Afshari
Vote positif pour celui-ci, j'obtiens des résultats dans ~ 15 secondes sur 500000 lignes.
Jim T
4
Tronquez le CreateionDate en jours dans tous ces tests (sur le côté droit uniquement ou vous tuez SARG) en utilisant DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) Cela fonctionne en soustrayant la date fournie de zéro - ce que Microsoft SQL Server interprète comme 1900-01-01 00:00:00 et donne le nombre de jours. Cette valeur est ensuite rajoutée à la date zéro, ce qui donne la même date avec l'heure tronquée.
IDisposable le
1
tout ce que je peux vous dire, c'est que sans le changement d'IDisposable, le calcul est incorrect . J'ai personnellement validé les données moi-même. Certains utilisateurs avec des intervalles de 1 jour recevraient le badge de manière incorrecte.
Jeff Atwood
3
Cette requête a le potentiel de manquer une visite qui se produit à 23: 59: 59.5 - que diriez-vous de la changer ON uh2.CreationDate >= uh1.CreationDate AND uh2.CreationDate < DATEADD(dd, DATEDIFF(dd, 0, uh1.CreationDate) + @days, 0)en:, pour signifier "Pas encore le 31ème jour plus tard". Cela signifie également que vous pouvez ignorer le calcul des @seconds.
Rob Farley
147

Que diriez-vous (et assurez-vous que la déclaration précédente se termine par un point-virgule):

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'idée étant que si nous avons la liste des jours (sous forme de nombre), et un row_number, alors les jours manqués augmentent légèrement le décalage entre ces deux listes. Nous recherchons donc une fourchette qui présente un décalage constant.

Vous pouvez utiliser "ORDER BY NumConsecutiveDays DESC" à la fin de ceci, ou dire "HAVING count (*)> 14" pour un seuil ...

Je n'ai pas testé cela cependant - je l'ai simplement écrit par le haut de ma tête. Espérons que cela fonctionne dans SQL2005 et autres.

... et serait très aidé par un index sur le nom de la table (UserID, CreationDate)

Modifié: Il s'avère que Offset est un mot réservé, j'ai donc utilisé TheOffset à la place.

Modifié: La suggestion d'utiliser COUNT (*) est très valable - j'aurais dû le faire en premier lieu mais je n'y pensais pas vraiment. Auparavant, il utilisait à la place datéiff (jour, min (CreationDate), max (CreationDate)).

Rob

Rob Farley
la source
1
oh vous devriez aussi ajouter; avant avec ->; avec
Mladen Prajdic
2
Mladen - non, vous devriez terminer la déclaration précédente par un point-virgule. ;) Jeff - Ok, mettez [Offset] à la place. Je suppose que Offset est un mot réservé. Comme je l'ai dit, je ne l'avais pas testé.
Rob Farley
1
Je me répète simplement, car c'est un problème souvent vu. Tronquez le CreateionDate en jours dans tous ces tests (sur le côté droit uniquement ou vous tuez SARG) en utilisant DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) Cela fonctionne en soustrayant la date fournie de zéro - ce que Microsoft SQL Server interprète comme 1900-01-01 00:00:00 et donne le nombre de jours. Cette valeur est ensuite rajoutée à la date zéro, ce qui donne la même date avec l'heure tronquée.
IDisposable le
1
IDisposable - oui, je le fais souvent moi-même. Je ne me souciais simplement pas de le faire ici. Ce ne serait pas plus rapide que de le convertir en un int, mais il a la flexibilité de compter des heures, des mois, peu importe.
Rob Farley
1
Je viens d'écrire un article de blog sur la résolution de cela avec DENSE_RANK () aussi. tinyurl.com/denserank
Rob Farley
18

Si vous pouvez modifier le schéma de la table, je vous suggère d'ajouter une colonne LongestStreakà la table que vous définiriez sur le nombre de jours séquentiels se terminant par CreationDate. Il est facile de mettre à jour le tableau au moment de la connexion (comme vous le faites déjà, si aucune ligne n'existe pour le jour en cours, vous vérifierez si une ligne existe pour le jour précédent. Si la valeur est true, vous incrémenterez le LongestStreakdans le nouvelle ligne, sinon, vous la définissez sur 1.)

La requête sera évidente après l'ajout de cette colonne:

if exists(select * from table
          where LongestStreak >= 30 and UserId = @UserId)
   -- award the Woot badge.
Mehrdad Afshari
la source
1
+1 J'avais une pensée similaire, mais avec un petit champ (IsConsecutive) qui serait 1 s'il y a un record pour la veille, sinon 0.
Fredrik Mörk
7
nous n'allons pas changer le schéma pour cela
Jeff Atwood
Et le IsConsecutive peut être une colonne calculée définie dans la table UserHistory. Vous pouvez également en faire une colonne calculée matérialisée (stockée) qui est créée lorsque la ligne est insérée IFF (si et UNIQUEMENT si) vous insérez toujours les lignes dans l'ordre chronologique.
IDisposable le
(parce que NOBODY ferait un SELECT *, nous savons que l'ajout de cette colonne calculée n'affectera pas les plans de requête à moins que la colonne ne soit référencée ... pas vrai?!?)
IDisposable
3
c'est certainement une solution valable mais ce n'est pas ce que j'ai demandé. Alors je lui donne un "pouce sur le côté" ..
Jeff Atwood
6

Un SQL joliment expressif comme:

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

En supposant que vous ayez une fonction d'agrégation définie par l'utilisateur, quelque chose du genre (attention, c'est bogué):

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
la source
4

On dirait que vous pourriez tirer parti du fait que pour être continu sur n jours, il faudrait qu'il y ait n lignes.

Donc quelque chose comme:

SELECT users.UserId, count(1) as cnt
FROM users
WHERE users.CreationDate > now() - INTERVAL 30 DAY
GROUP BY UserId
HAVING cnt = 30
Facture
la source
oui, nous pouvons le bloquer par le nombre de disques, bien sûr .. mais cela n'élimine que certaines possibilités, car nous pourrions avoir 120 jours de visite sur plusieurs années avec beaucoup de lacunes quotidiennes
Jeff Atwood
1
D'accord, mais une fois que vous êtes rattrapé par l'attribution de cette page, vous ne devez l'exécuter qu'une fois par jour. Je pense que pour ce cas, quelque chose comme ci-dessus ferait l'affaire. Pour rattraper votre retard, tout ce que vous avez à faire est de transformer la clause WHERE en une fenêtre glissante à l'aide de BETWEEN.
Bill
1
chaque exécution de la tâche est sans état et autonome; il n'a aucune connaissance des exécutions précédentes autres que le tableau dans la question
Jeff Atwood
3

Faire cela avec une seule requête SQL me semble trop compliqué. Permettez-moi de diviser cette réponse en deux parties.

  1. Ce que vous auriez dû faire jusqu'à présent et que vous devriez commencer à faire maintenant:
    Exécutez une tâche cron quotidienne qui vérifie pour chaque utilisateur s'il s'est connecté aujourd'hui, puis incrémente un compteur s'il l'a ou le met à 0 s'il ne l'a pas fait.
  2. Ce que vous devez faire maintenant:
    - Exportez cette table vers un serveur qui n'exécute pas votre site Web et ne sera pas nécessaire pendant un certain temps. ;)
    - Triez-le par utilisateur, puis par date.
    - parcourez-le séquentiellement, gardez un compteur ...
Kim Stebel
la source
nous pouvons écrire du code en requête et en boucle, c'est .. dary je dis .. trivial. Je suis curieux de connaître le seul moyen SQL pour le moment.
Jeff Atwood
2

Si cela est si important pour vous, recherchez cet événement et gérez une table pour vous donner cette information. Pas besoin de tuer la machine avec toutes ces requêtes folles.


la source
2

Vous pouvez utiliser un CTE récursif (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
Poneys OMG
la source
2

Joe Celko a un chapitre complet à ce sujet dans SQL for Smarties (en l'appelant Runs and Sequences). Je n'ai pas ce livre à la maison, alors quand j'arriverai au travail ... je répondrai à ça. (en supposant que la table d'historique s'appelle dbo.UserHistory et que le nombre de jours est @Days)

Une autre piste provient du blog de SQL Team sur les exécutions

L'autre idée que j'ai eue, mais je n'ai pas de serveur SQL sur lequel travailler ici, est d'utiliser un CTE avec un ROW_NUMBER partitionné comme ceci:

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

Ce qui précède est probablement BEAUCOUP PLUS DIFFICILE qu'il ne doit l'être, mais laissé comme un chatouillement cérébral lorsque vous avez une autre définition de "une course" que de simples dates.

IDisposable
la source
2

Quelques options SQL Server 2012 (en supposant N = 100 ci-dessous).

;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 

Bien qu'avec mes exemples de données, les éléments suivants ont été plus efficaces

;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

Tous deux reposent sur la contrainte énoncée dans la question selon laquelle il y a au plus un enregistrement par jour et par utilisateur.

Martin Smith
la source
1

Quelque chose comme ça?

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
la source
1

J'ai utilisé une propriété mathématique simple pour identifier qui a accédé consécutivement au site. Cette propriété est que vous devez avoir la différence de jour entre le premier accès et la dernière fois égale au nombre d'enregistrements dans votre journal de table d'accès.

Voici le script SQL que j'ai testé dans Oracle DB (il devrait également fonctionner dans d'autres 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 préparation de table:

-- 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
la source
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

L'instruction cast(convert(char(11), @startdate, 113) as datetime)supprime la partie horaire de la date, nous commençons donc à minuit.

Je suppose également que les colonnes creationdateet useridsont indexées.

Je viens de réaliser que cela ne vous dira pas tous les utilisateurs et leur nombre total de jours consécutifs. Mais vous dira quels utilisateurs auront visité un certain nombre de jours à partir d'une date de votre choix.

Solution révisée:

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

J'ai vérifié cela et il interrogera tous les utilisateurs et toutes les dates. Il est basé sur la première solution (blague?) De Spencer , mais la mienne fonctionne.

Mise à jour: amélioration de la gestion des dates dans la deuxième solution.

Stephen Perelson
la source
fermer, mais nous avons besoin de quelque chose qui fonctionne pour n'importe quelle période de (n) jours, pas à une date de début fixe
Jeff Atwood
0

Cela devrait faire ce que vous voulez, mais je n'ai pas assez de données pour tester l'efficacité. Le truc convoluté CONVERT / FLOOR consiste à supprimer la partie heure du champ datetime. Si vous utilisez SQL Server 2008, vous pouvez utiliser CAST (x.CreationDate AS DATE).

DÉCLARER @Range comme INT
SET @Range = 10

SELECT DISTINCT UserId, CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate)))
  DE tblUserLogin a
O EXISTE
   (SÉLECTIONNER 1 
      DE tblUserLogin b 
     O a.userId = b.userId 
       AND (SELECT COUNT (DISTINCT (CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, CreationDate))))) 
              DE tblUserLogin c 
             O c.userid = b.userid 
               ET CONVERTIR (DATETIME, FLOOR (CONVERT (FLOAT, c.CreationDate))) ENTRE CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate))) et CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a. ) + @ Plage-1) = @ Plage)

Script de création

CREATE TABLE [dbo]. [TblUserLogin] (
    [Id] [int] IDENTITY (1,1) NOT NULL,
    [UserId] [int] NULL,
    [CreationDate] [datetime] NULL
) ON [PRIMAIRE]
Dave Barker
la source
assez brutal. 26 secondes sur 406 624 lignes.
Jeff Atwood
À quelle fréquence vérifiez-vous l'attribution du badge? Si ce n'est qu'une fois par jour, un coup de 26 secondes dans une période lente ne semble pas si grave. Cependant, les performances ralentiront à mesure que la table grandira. Après avoir relu la question, le temps peut ne pas être pertinent car il n'y a qu'un seul enregistrement par jour.
Dave Barker
0

Spencer l'a presque fait, mais cela devrait être le code de travail:

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
la source
0

Du haut de ma tête, 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 testé, et nécessite presque certainement une conversion pour MSSQL, mais je pense que cela donne des idées.

Cebjyre
la source
0

Que diriez-vous d'utiliser des tableaux de pointage? Il suit une approche plus algorithmique et le plan d'exécution est un jeu d'enfant. Remplissez le tallyTable avec les nombres de 1 à «MaxDaysBehind» que vous voulez analyser le tableau (c'est-à-dire que 90 recherchera 3 mois de retard, 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
la source
0

Ajuster un peu la requête de Bill. Vous devrez peut-être tronquer la date avant le regroupement pour ne compter qu'une seule connexion par jour ...

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 pour utiliser DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) au lieu de convert (char (10), CreationDate, 101).

@IDisposable Je cherchais à utiliser datepart plus tôt, mais j'étais trop paresseux pour rechercher la syntaxe, alors j'ai pensé que j'utiliserais plutôt convert. Je ne sais pas que cela a eu un impact significatif Merci! maintenant je sais.

Jaskirat
la source
Il est préférable de tronquer un SQL DATETIME en date uniquement avec DATEADD (dd, DATEDIFF (dd, 0, UH.CreationDate), 0)
IDisposable
(ce qui précède fonctionne en prenant la différence en jours entiers entre 0 (par exemple 1900-01-01 00: 00: 00.000), puis en ajoutant cette différence en jours entiers à 0 (par exemple 1900-01-01 00:00:00) . Cela entraîne la suppression de la partie horaire de DATETIME)
IDisposable
0

en supposant un schéma qui va comme:

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

cela extraira des plages contiguës d'une séquence de dates avec des intervalles.

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
la source