web-dev-qa-db-fra.com

Comment trouver récursivement des lacunes où 90 jours se sont écoulés, entre les lignes

C'est une sorte de tâche triviale dans mon monde natal C #, mais je ne le fais pas encore en SQL et préférerais le résoudre en fonction d'un ensemble (sans curseurs). Un jeu de résultats doit provenir d'une requête comme celle-ci.

SELECT SomeId, MyDate, 
    dbo.udfLastHitRecursive(param1, param2, MyDate) as 'Qualifying'
FROM T

Comment cela devrait-il fonctionner

J'envoie ces trois params dans un UDF.
L'UDF utilise en interne des paramètres pour récupérer les lignes associées <= 90 jours plus anciennes, à partir d'une vue.
L'UDF traverse 'MyDate' et renvoie 1 s'il doit être inclus dans un calcul total.
Si ce n'est pas le cas, il renvoie 0. Nommé ici comme "qualificatif".

Ce que fera le udf

Liste les lignes dans l'ordre des dates. Calculez les jours entre les rangées. La première ligne du jeu de résultats par défaut est Hit = 1. Si la différence est jusqu'à 90, - passez ensuite à la ligne suivante jusqu'à ce que la somme des intervalles soit de 90 jours (le 90e jour doit passer). Il serait également préférable d'omettre la ligne du résultat.

                                          |(column by udf, which not work yet)
Date              Calc_date     MaxDiff   | Qualifying
2014-01-01 11:00  2014-01-01    0         | 1
2014-01-03 10:00  2014-01-01    2         | 0
2014-01-04 09:30  2014-01-03    1         | 0
2014-04-01 10:00  2014-01-04    87        | 0
2014-05-01 11:00  2014-04-01    30        | 1

Dans le tableau ci-dessus, la colonne MaxDiff est l'écart par rapport à la date de la ligne précédente. Le problème avec mes tentatives jusqu'à présent est que je ne peux pas ignorer l'avant-dernière ligne de l'exemple ci-dessus.

[MODIFIER]
Selon le commentaire, j'ajoute une balise et colle également le udf que j'ai compilé tout à l'heure. Cependant, c'est juste un espace réservé et ne donnera pas de résultat utile.

;WITH cte (someid, otherkey, mydate, cost) AS
(
    SELECT someid, otherkey, mydate, cost
    FROM dbo.vGetVisits
    WHERE someid = @someid AND VisitCode = 3 AND otherkey = @otherkey 
    AND CONVERT(Date,mydate) = @VisitDate

    UNION ALL

    SELECT top 1 e.someid, e.otherkey, e.mydate, e.cost
    FROM dbo.vGetVisits AS E
    WHERE CONVERT(date, e.mydate) 
        BETWEEN DateAdd(dd,-90,CONVERT(Date,@VisitDate)) AND CONVERT(Date,@VisitDate)
        AND e.someid = @someid AND e.VisitCode = 3 AND e.otherkey = @otherkey 
        AND CONVERT(Date,e.mydate) = @VisitDate
        order by e.mydate
)

J'ai une autre requête que je définis séparément qui est plus proche de ce dont j'ai besoin, mais bloquée par le fait que je ne peux pas calculer sur des colonnes fenêtrées. J'ai également essayé un similaire qui donne plus ou moins la même sortie juste avec un LAG () sur MyDate, entouré d'un datiff.

SELECT
    t.Mydate, t.VisitCode, t.Cost, t.SomeId, t.otherkey, t.MaxDiff, t.DateDiff
FROM 
(
    SELECT *,
        MaxDiff = LAST_VALUE(Diff.Diff)  OVER (
            ORDER BY Diff.Mydate ASC
                ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
    FROM 
    (
        SELECT *,
            Diff =  ISNULL(DATEDIFF(DAY, LAST_VALUE(r.Mydate) OVER (
                        ORDER BY r.Mydate ASC
                            ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING), 
                                r.Mydate),0),
            DateDiff =  ISNULL(LAST_VALUE(r.Mydate) OVER (
                        ORDER BY r.Mydate ASC
                            ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING), 
                                r.Mydate)
        FROM dbo.vGetVisits AS r
        WHERE r.VisitCode = 3 AND r.SomeId = @SomeID AND r.otherkey = @otherkey
    ) AS Diff
) AS t
WHERE t.VisitCode = 3 AND t.SomeId = @SomeId AND t.otherkey = @otherkey
    AND t.Diff <= 90
ORDER BY
    t.Mydate ASC;
18
Independent

En lisant la question, l'algorithme récursif de base requis est:

  1. Renvoie la ligne avec la date la plus ancienne de l'ensemble
  2. Définissez cette date comme "actuelle"
  3. Rechercher la ligne avec la date la plus ancienne plus de 90 jours après la date actuelle
  4. Répétez à partir de l'étape 2 jusqu'à ce que plus aucune ligne ne soit trouvée

Ceci est relativement facile à implémenter avec une expression de table commune récursive.

Par exemple, en utilisant les exemples de données suivants (basés sur la question):

DECLARE @T AS table (TheDate datetime PRIMARY KEY);

INSERT @T (TheDate)
VALUES
    ('2014-01-01 11:00'),
    ('2014-01-03 10:00'),
    ('2014-01-04 09:30'),
    ('2014-04-01 10:00'),
    ('2014-05-01 11:00'),
    ('2014-07-01 09:00'),
    ('2014-07-31 08:00');

Le code récursif est:

WITH CTE AS
(
    -- Anchor:
    -- Start with the earliest date in the table
    SELECT TOP (1)
        T.TheDate
    FROM @T AS T
    ORDER BY
        T.TheDate

    UNION ALL

    -- Recursive part   
    SELECT
        SQ1.TheDate
    FROM 
    (
        -- Recursively find the earliest date that is 
        -- more than 90 days after the "current" date
        -- and set the new date as "current".
        -- ROW_NUMBER + rn = 1 is a trick to get
        -- TOP in the recursive part of the CTE
        SELECT
            T.TheDate,
            rn = ROW_NUMBER() OVER (
                ORDER BY T.TheDate)
        FROM CTE
        JOIN @T AS T
            ON T.TheDate > DATEADD(DAY, 90, CTE.TheDate)
    ) AS SQ1
    WHERE
        SQ1.rn = 1
)
SELECT 
    CTE.TheDate 
FROM CTE
OPTION (MAXRECURSION 0);

Les résultats sont:

╔═════════════════════════╗
║         TheDate         ║
╠═════════════════════════╣
║ 2014-01-01 11:00:00.000 ║
║ 2014-05-01 11:00:00.000 ║
║ 2014-07-31 08:00:00.000 ║
╚═════════════════════════╝

Avec un index ayant TheDate comme clé principale, le plan d'exécution est très efficace:

Execution plan

Vous pouvez choisir de l'envelopper dans une fonction et de l'exécuter directement contre la vue mentionnée dans la question, mais mes instincts sont contre. Habituellement, les performances sont meilleures lorsque vous sélectionnez des lignes d'une vue dans une table temporaire, fournissez l'index approprié sur la table temporaire, puis appliquez la logique ci-dessus. Les détails dépendent des détails de la vue, mais c'est mon expérience générale.

Pour être complet (et invité par la réponse de ypercube), je dois mentionner que mon autre solution de référence pour ce type de problème (jusqu'à ce que T-SQL obtienne les fonctions d'ensemble ordonnées appropriées) est un curseur SQLCLR ( voir ma réponse ici = pour un exemple de la technique). Cela fonctionne beaucoup mieux qu'un curseur T-SQL, et est pratique pour ceux qui ont des compétences dans les langages .NET et la capacité d'exécuter SQLCLR dans leur environnement de production . Il ne peut pas offrir beaucoup dans ce scénario sur la solution récursive parce que la majorité du coût est le genre, mais il convient de le mentionner.

23
Paul White 9

Étant donné que est une question SQL Server 2014, je pourrais aussi bien ajouter une version de procédure stockée compilée en mode natif d'un "curseur".

Tableau source avec quelques données:

create table T 
(
  TheDate datetime primary key
);

go

insert into T(TheDate) values
('2014-01-01 11:00'),
('2014-01-03 10:00'),
('2014-01-04 09:30'),
('2014-04-01 10:00'),
('2014-05-01 11:00'),
('2014-07-01 09:00'),
('2014-07-31 08:00');

Un type de table qui est le paramètre de la procédure stockée. Ajustez le bucket_count De manière appropriée .

create type TType as table
(
  ID int not null primary key nonclustered hash with (bucket_count = 16),
  TheDate datetime not null
) with (memory_optimized = on);

Et une procédure stockée qui parcourt le paramètre de valeur de table et collecte les lignes dans @R.

create procedure dbo.GetDates
  @T dbo.TType readonly
with native_compilation, schemabinding, execute as owner 
as
begin atomic with (transaction isolation level = snapshot, language = N'us_english', delayed_durability = on)

  declare @R dbo.TType;
  declare @ID int = 0;
  declare @RowsLeft bit = 1;  
  declare @CurDate datetime = '1901-01-01';
  declare @LastDate datetime = '1901-01-01';

  while @RowsLeft = 1
  begin
    set @ID += 1;

    select @CurDate = T.TheDate
    from @T as T
    where T.ID = @ID

    if @@rowcount = 1
    begin
      if datediff(day, @LastDate, @CurDate) > 90
      begin
        insert into @R(ID, TheDate) values(@ID, @CurDate);
        set @LastDate = @CurDate;
      end;
    end
    else
    begin
      set @RowsLeft = 0;
    end

  end;

  select R.TheDate
  from @R as R;
end

Code pour remplir une variable de table optimisée en mémoire qui est utilisée comme paramètre de la procédure stockée compilée en mode natif et appeler la procédure.

declare @T dbo.TType;

insert into @T(ID, TheDate)
select row_number() over(order by T.TheDate),
       T.TheDate
from T;

exec dbo.GetDates @T;

Résultat:

TheDate
-----------------------
2014-07-31 08:00:00.000
2014-01-01 11:00:00.000
2014-05-01 11:00:00.000

Mise à jour:

Si pour une raison quelconque, vous n'avez pas besoin de visiter chaque ligne du tableau, vous pouvez faire l'équivalent de la version "passer à la date suivante" qui est implémentée dans le CTE récursif par Paul White.

Le type de données n'a pas besoin de la colonne ID et vous ne devez pas utiliser d'index de hachage.

create type TType as table
(
  TheDate datetime not null primary key nonclustered
) with (memory_optimized = on);

Et la procédure stockée utilise une select top(1) .. pour trouver la valeur suivante.

create procedure dbo.GetDates
  @T dbo.TType readonly
with native_compilation, schemabinding, execute as owner 
as
begin atomic with (transaction isolation level = snapshot, language = N'us_english', delayed_durability = on)

  declare @R dbo.TType;
  declare @RowsLeft bit = 1;  
  declare @CurDate datetime = '1901-01-01';

  while @RowsLeft = 1
  begin

    select top(1) @CurDate = T.TheDate
    from @T as T
    where T.TheDate > dateadd(day, 90, @CurDate)
    order by T.TheDate;

    if @@rowcount = 1
    begin
      insert into @R(TheDate) values(@CurDate);
    end
    else
    begin
      set @RowsLeft = 0;
    end

  end;

  select R.TheDate
  from @R as R;
end
9
Mikael Eriksson

Une solution qui utilise un curseur.
(d'abord, quelques tableaux et variables nécessaires):

-- a table to hold the results
DECLARE @cd TABLE
(   TheDate datetime PRIMARY KEY,
    Qualify INT NOT NULL
);

-- some variables
DECLARE
    @TheDate DATETIME,
    @diff INT,
    @Qualify     INT = 0,
    @PreviousCheckDate DATETIME = '1900-01-01 00:00:00' ;

Le curseur réel:

-- declare the cursor
DECLARE c CURSOR
    LOCAL STATIC FORWARD_ONLY READ_ONLY
    FOR
    SELECT TheDate
      FROM T
      ORDER BY TheDate ;

-- using the cursor to fill the @cd table
OPEN c ;

FETCH NEXT FROM c INTO @TheDate ;

WHILE @@FETCH_STATUS = 0
BEGIN
    SET @diff = DATEDIFF(day, @PreviousCheckDate, @Thedate) ;
    SET @Qualify = CASE WHEN @diff > 90 THEN 1 ELSE 0 END ;

    INSERT @cd (TheDate, Qualify)
        SELECT @TheDate, @Qualify ;

    SET @PreviousCheckDate = 
            CASE WHEN @diff > 90 
                THEN @TheDate 
                ELSE @PreviousCheckDate END ;

    FETCH NEXT FROM c INTO @TheDate ;
END

CLOSE c;
DEALLOCATE c;

Et obtenir les résultats:

-- get the results
SELECT TheDate, Qualify
    FROM @cd
    -- WHERE Qualify = 1        -- optional, to see only the qualifying rows
    ORDER BY TheDate ;

Testé à SQLFiddle

5
ypercubeᵀᴹ
IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[vGetVisits]') AND type in (N'U'))
DROP TABLE [dbo].[vGetVisits]
GO

CREATE TABLE [dbo].[vGetVisits](
    [id] [int] NOT NULL,
    [mydate] [datetime] NOT NULL,
 CONSTRAINT [PK_vGetVisits] PRIMARY KEY CLUSTERED 
(
    [id] ASC
)
)

GO

INSERT INTO [dbo].[vGetVisits]([id], [mydate])
VALUES
    (1, '2014-01-01 11:00'),
    (2, '2014-01-03 10:00'),
    (3, '2014-01-04 09:30'),
    (4, '2014-04-01 10:00'),
    (5, '2014-05-01 11:00'),
    (6, '2014-07-01 09:00'),
    (7, '2014-07-31 08:00');
GO


-- Clean up 
IF OBJECT_ID (N'dbo.udfLastHitRecursive', N'FN') IS NOT NULL
DROP FUNCTION udfLastHitRecursive;
GO

-- Actual Function  
CREATE FUNCTION dbo.udfLastHitRecursive
( @MyDate datetime)

RETURNS TINYINT

AS
    BEGIN 
        -- Your returned value 1 or 0
        DECLARE @Returned_Value TINYINT;
        SET @Returned_Value=0;
    -- Prepare gaps table to be used.
    WITH gaps AS
    (
                        -- Select Date and MaxDiff from the original table
                        SELECT 
                        CONVERT(Date,mydate) AS [date]
                        , DATEDIFF(day,ISNULL(LAG(mydate, 1) OVER (ORDER BY mydate), mydate) , mydate) AS [MaxDiff]
                        FROM dbo.vGetVisits
    )

        SELECT @Returned_Value=
            (SELECT DISTINCT -- DISTINCT in case we have same date but different time
                    CASE WHEN
                     (
                    -- It is a first entry
                    [date]=(SELECT MIN(CONVERT(Date,mydate)) FROM dbo.vGetVisits))
                    OR 
                    /* 
                    --Gap between last qualifying date and entered is greater than 90 
                        Calculate Running sum upto and including required date 
                        and find a remainder of division by 91. 
                    */
                     ((SELECT SUM(t1.MaxDiff)  
                    FROM (SELECT [MaxDiff] FROM gaps WHERE [date]<=t2.[date] 
                    ) t1 
                    )%91 - 
                    /* 
                        ISNULL added to include first value that always returns NULL 
                        Calculate Running sum upto and NOT including required date 
                        and find a remainder of division by 91 
                    */
                    ISNULL((SELECT SUM(t1.MaxDiff)  
                    FROM (SELECT [MaxDiff] FROM gaps WHERE [date]<t2.[date] 
                    ) t1 
                    )%91, 0) -- End ISNULL
                     <0 )
                    /* End Running sum upto and including required date */
                    OR
                    -- Gap between two nearest dates is greater than 90 
                    ((SELECT SUM(t1.MaxDiff)  
                    FROM (SELECT [MaxDiff] FROM gaps WHERE [date]<=t2.[date] 
                    ) t1 
                    ) - ISNULL((SELECT SUM(t1.MaxDiff)  
                    FROM (SELECT [MaxDiff] FROM gaps WHERE [date]<t2.[date] 
                    ) t1 
                    ), 0) > 90) 
                    THEN 1
                    ELSE 0
                    END 
                    AS [Qualifying]
                    FROM gaps t2
                    WHERE [date]=CONVERT(Date,@MyDate))
        -- What is neccesary to return when entered date is not in dbo.vGetVisits?
        RETURN @Returned_Value
    END
GO

SELECT 
dbo.udfLastHitRecursive(mydate) AS [Qualifying]
, [id]
, mydate 
FROM dbo.vGetVisits
ORDER BY mydate 

Résultat

enter image description here

Jetez également un œil à Comment calculer le total cumulé dans SQL Server

mise à jour: veuillez voir ci-dessous les résultats des tests de performances.

En raison de la logique différente utilisée pour trouver un "écart de 90 jours", les ypercubes et mes solutions s'ils sont laissés intacts peuvent renvoyer des résultats différents à la solution de Paul White. Cela est dû à l'utilisation des fonctions DATEIFF et DATEADD respectivement.

Par exemple:

SELECT DATEADD(DAY, 90, '2014-01-01 00:00:00.000')

renvoie "2014-04-01 00: 00: 00.000", ce qui signifie que "2014-04-01 01: 00: 00.000" est au-delà d'un intervalle de 90 jours

mais

SELECT DATEDIFF(DAY, '2014-01-01 00:00:00.000', '2014-04-01 01:00:00.000')

Renvoie "90", ce qui signifie qu'il est toujours dans l'écart.

Prenons un exemple de détaillant. Dans ce cas, la vente d'un produit périssable qui s'est vendu à la date "2014-01-01" à "2014-01-01 23: 59: 59: 999" est acceptable. La valeur DATEDIFF (DAY, ...) dans ce cas est donc OK.

Un autre exemple est un patient qui attend d'être vu. Pour quelqu'un qui vient à '2014-01-01 00: 00: 00: 000' et part à '2014-01-01 23: 59: 59: 999' c'est 0(zero) = jours si DATEDIFF est utilisé même si l'attente réelle était de près de 24 heures. Encore une fois, le patient qui arrive le '2014-01-01 23:59:59' et repart le '2014-01-02 00:00:01' a attendu pendant un jour si DATEDIFF est utilisé.

Mais je m'égare.

J'ai laissé les solutions DATEDIFF et même les performances testées, mais elles devraient vraiment être dans leur propre ligue.

Il a également été noté que pour les grands ensembles de données, il est impossible d'éviter les valeurs du même jour. Donc, si nous disons 13 millions d'enregistrements couvrant 2 ans de données, nous finirons par avoir plus d'un enregistrement pendant quelques jours. Ces enregistrements sont filtrés dès que possible dans mes solutions DATEDIFF et ypercube. J'espère que ypercube ne s'en soucie pas.

Les solutions ont été testées sur le tableau suivant

CREATE TABLE [dbo].[vGetVisits](
    [id] [int] NOT NULL,
    [mydate] [datetime] NOT NULL,
) 

avec deux index clusterisés différents (mydate dans ce cas):

CREATE CLUSTERED INDEX CI_mydate on vGetVisits(mydate) 
GO

Le tableau a été rempli de la manière suivante

SET NOCOUNT ON
GO

INSERT INTO dbo.vGetVisits(id, mydate)
VALUES (1, '01/01/1800')
GO

DECLARE @i bigint
SET @i=2

DECLARE @MaxRows bigint
SET @MaxRows=13001

WHILE @i<@MaxRows 
BEGIN
INSERT INTO dbo.vGetVisits(id, mydate)
VALUES (@i, DATEADD(day,FLOOR(Rand()*(3)),(SELECT MAX(mydate) FROM dbo.vGetVisits)))
SET @i=@i+1
END

Pour un cas de plusieurs millions de lignes, INSERT a été modifié de telle manière que des entrées de 0 à 20 minutes ont été ajoutées au hasard.

Toutes les solutions ont été soigneusement emballées dans le code suivant

SET NOCOUNT ON
GO

DECLARE @StartDate DATETIME

SET @StartDate = GETDATE()

--- Code goes here

PRINT 'Total milliseconds: ' + CONVERT(varchar, DATEDIFF(ms, @StartDate, GETDATE()))

Codes réels testés (sans ordre particulier):

La solution DATEDIFF d'Ypercube ( YPC, DATEDIFF )

DECLARE @cd TABLE
(   TheDate datetime PRIMARY KEY,
    Qualify INT NOT NULL
);

DECLARE
    @TheDate DATETIME,
    @Qualify     INT = 0,
    @PreviousCheckDate DATETIME = '1799-01-01 00:00:00' 


DECLARE c CURSOR
    LOCAL STATIC FORWARD_ONLY READ_ONLY
    FOR
SELECT 
   mydate
FROM 
 (SELECT
       RowNum = ROW_NUMBER() OVER(PARTITION BY cast(mydate as date) ORDER BY mydate)
       , mydate
   FROM 
       dbo.vGetVisits) Actions
WHERE
   RowNum = 1
ORDER BY 
  mydate;

OPEN c ;

FETCH NEXT FROM c INTO @TheDate ;

WHILE @@FETCH_STATUS = 0
BEGIN

    SET @Qualify = CASE WHEN DATEDIFF(day, @PreviousCheckDate, @Thedate) > 90 THEN 1 ELSE 0 END ;
    IF  @Qualify=1
    BEGIN
        INSERT @cd (TheDate, Qualify)
        SELECT @TheDate, @Qualify ;
        SET @PreviousCheckDate=@TheDate 
    END
    FETCH NEXT FROM c INTO @TheDate ;
END

CLOSE c;
DEALLOCATE c;


SELECT TheDate
    FROM @cd
    ORDER BY TheDate ;

La solution DATEADD d'Ypercube ( YPC, DATEADD )

DECLARE @cd TABLE
(   TheDate datetime PRIMARY KEY,
    Qualify INT NOT NULL
);

DECLARE
    @TheDate DATETIME,
    @Next_Date DATETIME,
    @Interesting_Date DATETIME,
    @Qualify     INT = 0

DECLARE c CURSOR
    LOCAL STATIC FORWARD_ONLY READ_ONLY
    FOR
  SELECT 
  [mydate]
  FROM [test].[dbo].[vGetVisits]
  ORDER BY mydate
  ;

OPEN c ;

FETCH NEXT FROM c INTO @TheDate ;

SET @Interesting_Date=@TheDate

INSERT @cd (TheDate, Qualify)
SELECT @TheDate, @Qualify ;

WHILE @@FETCH_STATUS = 0
BEGIN

    IF @TheDate>DATEADD(DAY, 90, @Interesting_Date)
    BEGIN
        INSERT @cd (TheDate, Qualify)
        SELECT @TheDate, @Qualify ;
        SET @Interesting_Date=@TheDate;
    END

    FETCH NEXT FROM c INTO @TheDate;
END

CLOSE c;
DEALLOCATE c;


SELECT TheDate
    FROM @cd
    ORDER BY TheDate ;

La solution de Paul White ( [~ # ~] pw [~ # ~] )

;WITH CTE AS
(
    SELECT TOP (1)
        T.[mydate]
    FROM dbo.vGetVisits AS T
    ORDER BY
        T.[mydate]

    UNION ALL

    SELECT
        SQ1.[mydate]
    FROM 
    (
        SELECT
            T.[mydate],
            rn = ROW_NUMBER() OVER (
                ORDER BY T.[mydate])
        FROM CTE
        JOIN dbo.vGetVisits AS T
            ON T.[mydate] > DATEADD(DAY, 90, CTE.[mydate])
    ) AS SQ1
    WHERE
        SQ1.rn = 1
)

SELECT 
    CTE.[mydate]
FROM CTE
OPTION (MAXRECURSION 0);

Ma solution DATEADD ( PN, DATEADD )

DECLARE @cd TABLE
(   TheDate datetime PRIMARY KEY
);

DECLARE @TheDate DATETIME

SET @TheDate=(SELECT MIN(mydate) as mydate FROM [dbo].[vGetVisits])

WHILE (@TheDate IS NOT NULL)
    BEGIN

        INSERT @cd (TheDate) SELECT @TheDate;

        SET @TheDate=(  
            SELECT MIN(mydate) as mydate 
            FROM [dbo].[vGetVisits]
            WHERE mydate>DATEADD(DAY, 90, @TheDate)
                    )
    END

SELECT TheDate
    FROM @cd
    ORDER BY TheDate ;

Ma solution DATEDIFF ( PN, DATEDIFF )

DECLARE @MinDate DATETIME;
SET @MinDate=(SELECT MIN(mydate) FROM dbo.vGetVisits);
    ;WITH gaps AS
    (
       SELECT 
       t1.[date]
       , t1.[MaxDiff]
       , SUM(t1.[MaxDiff]) OVER (ORDER BY t1.[date]) AS [Running Total]
            FROM
            (
                SELECT 
                mydate AS [date]
                , DATEDIFF(day,LAG(mydate, 1, mydate) OVER (ORDER BY mydate) , mydate) AS [MaxDiff] 
                FROM 
                    (SELECT
                    RowNum = ROW_NUMBER() OVER(PARTITION BY cast(mydate as date) ORDER BY mydate)
                    , mydate
                    FROM dbo.vGetVisits
                    ) Actions
                WHERE RowNum = 1
            ) t1
    )

    SELECT [date]
    FROM gaps t2
    WHERE                         
         ( ([Running Total])%91 - ([Running Total]- [MaxDiff])%91 <0 )      
         OR
         ( [MaxDiff] > 90) 
         OR
         ([date]=@MinDate)    
    ORDER BY [date]

J'utilise SQL Server 2012, donc je m'excuse auprès de Mikael Eriksson, mais son code ne sera pas testé ici. Je m'attendrais toujours à ce que ses solutions avec DATADIFF et DATEADD retournent des valeurs différentes sur certains ensembles de données.

Et les résultats réels sont: enter image description here

2
Pavel Nefyodov

Ok, est-ce que j'ai raté quelque chose ou pourquoi ne sauteriez-vous pas simplement la récursivité et rejoignez-vous? Si la date est la clé primaire, elle doit être unique et dans l'ordre chronologique si vous prévoyez de calculer le décalage à la ligne suivante

    DECLARE @T AS TABLE
  (
     TheDate DATETIME PRIMARY KEY
  );

INSERT @T
       (TheDate)
VALUES ('2014-01-01 11:00'),
       ('2014-01-03 10:00'),
       ('2014-01-04 09:30'),
       ('2014-04-01 10:00'),
       ('2014-05-01 11:00'),
       ('2014-07-01 09:00'),
       ('2014-07-31 08:00');

SELECT [T1].[TheDate]                               [first],
       [T2].[TheDate]                               [next],
       Datediff(day, [T1].[TheDate], [T2].[TheDate])[offset],
       ( CASE
           WHEN Datediff(day, [T1].[TheDate], [T2].[TheDate]) >= 30 THEN 1
           ELSE 0
         END )                                      [qualify]
FROM   @T[T1]
       LEFT JOIN @T[T2]
              ON [T2].[TheDate] = (SELECT Min([TheDate])
                                   FROM   @T
                                   WHERE  [TheDate] > [T1].[TheDate]) 

Rendements

enter image description here

Sauf si j'ai raté quelque chose d'important ...

0
Sabre