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

17

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 ça devrait 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".

Que fera l'UDF

Répertoriez 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.

[EDIT]
Selon le commentaire, j'ajoute une balise et je 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;
Indépendant
la source
Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
Paul White réintègre Monica

Réponses:

22

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 TheDatecomme clé principale, le plan d'exécution est très efficace:

Plan d'exécution

Vous pouvez choisir d'envelopper cela 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 en 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.

Paul White réintègre Monica
la source
9

Comme il s'agit d' une question sur 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. Réglez le de bucket_countmaniè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 @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 un select top(1) ..pour rechercher 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
Mikael Eriksson
la source
Vos solutions utilisant DATEADD et DATEDIFF peuvent renvoyer des résultats différents selon le jeu de données initial.
Pavel Nefyodov
@ PavelNefyodov Je ne vois pas ça. Pouvez-vous expliquer ou donner un exemple?
Mikael Eriksson
Pourriez-vous le vérifier à des dates comme celle-ci ('2014-01-01 00: 00: 00.000'), ('2014-04-01 01: 00: 00.000'), s'il vous plaît? Plus d'informations peuvent être trouvées dans ma réponse.
Pavel Nefyodov,
@ PavelNefyodov Ah, je vois. Donc, si je change le deuxième en T.TheDate >= dateadd(day, 91, @CurDate)tout, ça irait, non?
Mikael Eriksson
Ou le cas échéant à l' OP, changer le type de données TheDatedans TTypela Date.
Mikael Eriksson
5

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é chez SQLFiddle

ypercubeᵀᴹ
la source
+1 à cette solution, mais pas parce que c'est la façon la plus efficace de faire les choses.
Pavel Nefyodov, du
@PavelNefyodov alors nous devrions tester les performances!
ypercubeᵀᴹ
Je fais confiance à Paul White là-dessus. Mon expérience avec les tests de performances n'est pas si impressionnante. Encore une fois, cela ne m'empêche pas de voter pour votre réponse.
Pavel Nefyodov
Merci ypercube. Comme prévu, rapidement sur un nombre limité de lignes. Sur 13000 lignes, le CTE et celui-ci ont réalisé sensiblement la même chose. Sur 130 000 lignes, il y avait une différence de 600%. Sur 13m ça passe 15 minutes sur mon matériel de test. J'ai également dû supprimer la clé primaire, ce qui peut affecter un peu les performances.
Indépendant
Thnx pour les tests. Vous pouvez également tester en modifiant pour ne faire INSERT @cdque quand @Qualify=1(et donc en n'insérant pas 13 millions de lignes si vous n'en avez pas besoin dans la sortie). Et la solution dépend de la recherche d'un indice TheDate. S'il n'y en a pas, ce ne sera pas efficace.
ypercubeᵀᴹ
2
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

entrez la description de l'image ici

Consultez également 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 DATEDIFF 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 le '2014-01-01 00: 00: 00: 000' et qui part le '2014-01-01 23: 59: 59: 999', il est 0 (zéro) jour si DATEDIFF est utilisé même si le l'attente réelle était de près de 24 heures. Encore une fois, le patient qui vient au '2014-01-01 23:59:59' et repart au '2014-01-02 00:00:01' a attendu 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: entrez la description de l'image ici

Pavel Nefyodov
la source
Merci Pavel. Je n'ai pas vraiment obtenu le résultat de votre solution dans le temps. J'ai réduit mes données de test à 1 000 lignes jusqu'à ce que j'obtienne un temps d'exécution en 25 secondes. Lorsque j'ai ajouté un groupe par date et converti en dates dans la sélection, j'ai obtenu la sortie correcte! Juste pour le plaisir, j'ai laissé la requête se poursuivre avec ma petite table de données de test (13k lignes) et j'ai obtenu plus de 12 minutes, ce qui signifie une performance supérieure à o (nx)! Il semble donc utile pour les ensembles qui seront certainement petits.
Indépendant
Quelle table avez-vous utilisée lors des tests? Combien de lignes? Je ne sais pas pourquoi vous avez dû ajouter un groupe par date pour obtenir une sortie correcte. N'hésitez pas à publier vos financements dans le cadre de votre question (mise à jour).
Pavel Nefyodov
Salut! J'ajouterai cela demain. Le regroupement devait combiner des dates en double. Mais j'étais pressé (tard le soir) et c'était peut-être déjà fait en ajoutant convert (date, z). La quantité de lignes est dans mon commentaire. J'ai essayé 1000 lignes avec votre solution. A également essayé 13 000 lignes avec une exécution de 12 minutes. Pauls et Ypercubes ont également été tentés à la table des 130 000 et 13 millions. Le tableau était un tableau simple avec des dates aléatoires créées hier et -2 ans en arrière. Index clustured sur le champ de date.
Indépendant
0

Ok, ai-je raté quelque chose ou pourquoi ne sauteriez-vous pas simplement la récursivité et vous rejoindriez-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

entrez la description de l'image ici

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

Sabre
la source
2
Vous voudrez probablement changer cela WHERE [TheDate] > [T1].[TheDate]pour tenir compte du seuil de différence de 90 jours. Mais encore, votre sortie n'est pas celle souhaitée.
ypercubeᵀᴹ
Important: Votre code doit avoir "90" quelque part.
Pavel Nefyodov