Come trovare ricorsivamente spazi vuoti in cui sono trascorsi 90 giorni, tra le righe


17

Questo è un tipo di compito banale nel mio pianeta natale C #, ma non lo faccio ancora in SQL e preferirei risolverlo basato su set (senza cursori). Un gruppo di risultati dovrebbe provenire da una query come questa.

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

Come dovrebbe funzionare

Mando quei tre parametri in un UDF.
L'UDF usa internamente i parametri per recuperare le righe <= 90 giorni precedenti correlate, da una vista.
L'UDF attraversa 'MyDate' e restituisce 1 se deve essere incluso in un calcolo totale.
In caso contrario, restituisce 0. Nominato qui come "idoneo".

Cosa farà l'UDF

Elencare le righe in ordine di data. Calcola i giorni tra le righe. La prima riga del gruppo di risultati viene impostata per impostazione predefinita su Hit = 1. Se la differenza è fino a 90, - passa alla riga successiva fino a quando la somma degli spazi vuoti è 90 giorni (deve passare il 90 ° giorno) Al raggiungimento, imposta Hit su 1 e reimposta il gap su 0 Funzionerebbe anche invece di omettere la riga dal risultato.

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

Nella tabella sopra, la colonna MaxDiff è il gap dalla data nella riga precedente. Il problema con i miei tentativi finora è che non posso ignorare la seconda ultima riga nell'esempio sopra.

[EDIT]
Come da commento aggiungo un tag e incollo anche l'UDF che ho compilato proprio ora. Tuttavia, è solo un segnaposto e non darà risultati utili.

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

Ho un'altra query che definisco separatamente che è più vicina a ciò di cui ho bisogno, ma bloccata dal fatto che non posso calcolare su colonne con finestre. Ho anche provato un simile che dà più o meno lo stesso output solo con un LAG () su MyDate, circondato da 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;

I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Paul White Ripristina Monica

Risposte:


22

Mentre leggo la domanda, l'algoritmo ricorsivo di base richiesto è:

  1. Restituisce la riga con la prima data nell'insieme
  2. Imposta quella data come "corrente"
  3. Trova la riga con la prima data più di 90 giorni dopo la data corrente
  4. Ripetere dal passaggio 2 fino a quando non vengono più trovate righe

Questo è relativamente facile da implementare con un'espressione di tabella comune ricorsiva.

Ad esempio, utilizzando i seguenti dati di esempio (in base alla domanda):

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

Il codice ricorsivo è:

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

I risultati sono:

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

Con un indice che ha TheDatecome chiave principale, il piano di esecuzione è molto efficiente:

Progetto esecutivo

Potresti scegliere di racchiuderlo in una funzione ed eseguirlo direttamente contro la visione menzionata nella domanda, ma il mio istinto è contrario. Di solito, le prestazioni sono migliori quando si selezionano le righe da una vista in una tabella temporanea, si fornisce l'indice appropriato sulla tabella temporanea, quindi si applica la logica sopra. I dettagli dipendono dai dettagli della vista, ma questa è la mia esperienza generale.

Per completezza (e richiesto dalla risposta di ypercube), dovrei menzionare che la mia altra soluzione go-to per questo tipo di problema (fino a quando T-SQL non ottiene le funzioni di set ordinate appropriate) è un cursore SQLCLR ( vedere la mia risposta qui per un esempio della tecnica ). Funziona molto meglio di un cursore T-SQL ed è conveniente per chi ha competenze nei linguaggi .NET e la capacità di eseguire SQLCLR nel proprio ambiente di produzione. Potrebbe non offrire molto in questo scenario rispetto alla soluzione ricorsiva perché la maggior parte del costo è il tipo, ma vale la pena menzionarlo.


9

Poiché questa è una domanda di SQL Server 2014, potrei anche aggiungere una versione della procedura memorizzata compilata nativamente di un "cursore".

Tabella di origine con alcuni dati:

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 tipo di tabella che è il parametro della procedura memorizzata. Regola in modo bucket_countappropriato .

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

E una procedura memorizzata che scorre attraverso il parametro con valori di tabella e raccoglie le righe @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

Codice per riempire una variabile di tabella ottimizzata per la memoria che viene utilizzata come parametro per la procedura memorizzata compilata in modo nativo e chiamare la procedura.

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;

Risultato:

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

Aggiornare:

Se per qualche motivo non hai bisogno di visitare ogni riga della tabella puoi fare l'equivalente della versione "vai alla prossima data" implementata nel CTE ricorsivo di Paul White.

Il tipo di dati non richiede la colonna ID e non è necessario utilizzare un indice hash.

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

E la procedura memorizzata utilizza a select top(1) ..per trovare il valore successivo.

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

Le soluzioni che utilizzano DATEADD e DATEDIFF possono restituire risultati diversi a seconda del set di dati iniziale.
Pavel Nefyodov,

@PavelNefyodov Non lo vedo. Puoi spiegare o fare un esempio?
Mikael Eriksson,

Potresti verificarlo in date come questa ('2014-01-01 00: 00: 00.000'), ('2014-04-01 01: 00: 00.000'), per favore? Maggiori informazioni possono essere trovate nella mia risposta.
Pavel Nefyodov,

@PavelNefyodov Ah, capisco. Quindi se cambio il secondo a T.TheDate >= dateadd(day, 91, @CurDate)tutto andrebbe bene vero?
Mikael Eriksson,

Oppure, se appropriato per OP, modificare il tipo di dati TheDatein TTypein Date.
Mikael Eriksson,

5

Una soluzione che utilizza un cursore.
(in primo luogo, alcune tabelle e variabili necessarie) :

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

Il cursore attuale:

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

E ottenere i risultati:

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

Testato su SQLFiddle


+1 a questa soluzione, ma non perché è il modo più efficiente di fare le cose.
Pavel Nefyodov,

@PavelNefyodov allora dovremmo testare le prestazioni!
ypercubeᵀᴹ

Mi fido di Paul White su questo. La mia esperienza con i test delle prestazioni non è così impressionante. Ancora una volta, questo non mi impedisce di votare la tua risposta.
Pavel Nefyodov,

Grazie a ypercube. Come previsto veloce su un numero limitato di righe. Su 13000 righe, The CTE e questo hanno funzionato più o meno allo stesso modo. Su 130.000 file c'era una differenza del 600%. Su 13m passano 15 minuti sulla mia attrezzatura di prova. Inoltre, ho dovuto rimuovere la chiave primaria, che potrebbe influire leggermente sulle prestazioni.
Indipendente dal

Grazie per il test. Puoi anche testare modificando per fare INSERT @cdsolo quando @Qualify=1(e quindi non inserire 13M righe se non hai bisogno di tutte nell'output). E la soluzione dipende dalla ricerca di un indice TheDate. Se non ce n'è uno, non sarà efficiente.
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 

Risultato

inserisci qui la descrizione dell'immagine

Dai anche un'occhiata a Come calcolare il totale parziale in SQL Server

aggiornamento: vedere di seguito i risultati dei test delle prestazioni.

A causa della diversa logica utilizzata nel trovare "90 giorni gap" di ypercube e le mie soluzioni se lasciate intatte possono restituire risultati diversi alla soluzione di Paul White. Ciò è dovuto all'uso delle funzioni DATEDIFF e DATEADD rispettivamente.

Per esempio:

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

restituisce "01-04-2014 00: 00: 00.000", il che significa che "01-04-2014 01: 00: 00.000" supera l'intervallo di 90 giorni

ma

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

Restituisce '90', il che significa che è ancora all'interno del gap.

Prendi in considerazione un esempio di rivenditore. In questo caso la vendita di un prodotto deperibile che ha venduto per data '2014-01-01' a '2014-01-01 23: 59: 59: 999' va bene. Quindi il valore DATEDIFF (DAY, ...) in questo caso è OK.

Un altro esempio è un paziente in attesa di essere visto. Per qualcuno che arriva al '01-01-2010 00: 00: 00: 000' e parte al '01-01-2014 23: 59: 59: 999' sono 0 (zero) giorni se DATEDIFF viene utilizzato anche se il l'attesa effettiva è stata di quasi 24 ore. Ancora una volta il paziente che arriva a '2014-01-01 23:59:59' e si allontana a '2014-01-02 00:00:01' ha aspettato un giorno se si utilizza DATEDIFF.

Ma sto divagando.

Ho lasciato le soluzioni DATEDIFF e persino le prestazioni le hanno testate, ma dovrebbero davvero essere nella loro stessa lega.

Inoltre è stato notato che per i grandi set di dati è impossibile evitare i valori dello stesso giorno. Quindi se diciamo 13 milioni di record che coprono 2 anni di dati, finiremo per avere più di un record per alcuni giorni. Tali registrazioni vengono filtrate alla prima opportunità nelle soluzioni DATEDIFF di my e ypercube. Spero che ypercube non si preoccupi di questo.

Le soluzioni sono state testate nella tabella seguente

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

con due diversi indici cluster (mydate in questo caso):

CREATE CLUSTERED INDEX CI_mydate on vGetVisits(mydate) 
GO

La tabella è stata popolata nel modo seguente

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

Per un caso di più di un milione di righe INSERT è stato modificato in modo da aggiungere casualmente 0-20 minuti.

Tutte le soluzioni sono state accuratamente racchiuse nel seguente codice

SET NOCOUNT ON
GO

DECLARE @StartDate DATETIME

SET @StartDate = GETDATE()

--- Code goes here

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

Codici effettivi testati (in nessun ordine particolare):

La soluzione DATEDIFF di 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 soluzione DATEADD di 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 soluzione di 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);

La mia soluzione 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 ;

La mia soluzione 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]

Sto usando SQL Server 2012, quindi mi scuso con Mikael Eriksson, ma il suo codice non verrà testato qui. Mi aspetterei comunque che le sue soluzioni con DATADIFF e DATEADD restituiscano valori diversi su alcuni set di dati.

E i risultati effettivi sono: inserisci qui la descrizione dell'immagine


Grazie Pavel. Non ho ottenuto il risultato della tua soluzione in tempo. Ho ridotto i miei dati di test a 1000 righe fino a quando ho ottenuto un tempo di esecuzione in 25 secondi. Quando ho aggiunto un gruppo per data e convertito in date nella selezione, ho ottenuto l'output corretto! Solo per amor, ho lasciato andare avanti la query con la mia piccola tabella testdata (13k righe) e ho ottenuto più di 12 minuti, il che significa una performance di più di o (nx)! Quindi sembra utile per i set che sicuramente saranno piccoli.
Indipendente

Qual è stata la tabella che hai usato nei test? Quante file? Non sono sicuro del motivo per cui è stato necessario aggiungere il gruppo per data per ottenere un output corretto. Sentiti libero di pubblicare i tuoi finanziamenti come parte della tua domanda (aggiornato).
Pavel Nefyodov,

Ciao! Lo aggiungerò domani. Il gruppo doveva combinare date duplicate. Ma ero di fretta (a tarda notte) e forse era già stato fatto aggiungendo convert (date, z). La quantità di righe è nel mio commento. Ho provato 1000 file con la tua soluzione. Ho anche provato 13.000 righe con un'esecuzione di 12 minuti. Pauls e Ypercubes furono anche tentati dal tavolo da 130.000 e 13 milioni. Il tavolo era un tavolo semplice con date casuali create da ieri e -2 anni fa. Indice cluster nel campo data.
Indipendente

0

Ok, mi sono perso qualcosa o perché non dovresti semplicemente saltare la ricorsione e tornare a te stesso? Se la data è la chiave primaria, deve essere unica e in ordine cronologico se si prevede di calcolare l'offset alla riga successiva

    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]) 

I rendimenti

inserisci qui la descrizione dell'immagine

A meno che non mi sia perso completamente qualcosa di importante ...


2
Probabilmente vuoi cambiarlo WHERE [TheDate] > [T1].[TheDate]per tener conto della soglia di differenza di 90 giorni. Tuttavia, il tuo output non è quello desiderato.
ypercubeᵀᴹ

Importante: il tuo codice dovrebbe avere "90" da qualche parte.
Pavel Nefyodov,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.