Como encontrar recursivamente lacunas onde passaram 90 dias, entre linhas

17

Esse é um tipo de tarefa trivial no meu mundo natal em C #, mas ainda não o faço em SQL e prefere resolvê-lo com base em conjunto (sem cursores). Um conjunto de resultados deve vir de uma consulta como esta.

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

Como deve funcionar

Envio esses três parâmetros para uma UDF.
O UDF usa internamente parâmetros para buscar linhas relacionadas <= 90 dias mais antigas, de uma exibição.
O UDF percorre 'MyDate' e retorna 1 se for incluído em um cálculo total.
Caso contrário, retornará 0. Nomeado aqui como "qualificado".

O que o udf fará

Listar as linhas em ordem de data. Calcular os dias entre as linhas. A primeira linha do conjunto de resultados é padronizada como Hit = 1. Se a diferença for de até 90, - passe para a próxima linha até que a soma das lacunas seja de 90 dias (o 90º dia deve passar) Quando atingido, defina Hit como 1 e redefina o intervalo como 0 Também funcionaria para omitir a linha do resultado.

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

Na tabela acima, a coluna MaxDiff é o intervalo da data na linha anterior. O problema com minhas tentativas até agora é que não posso ignorar a segunda última linha da amostra acima.

[EDIT]
Conforme comentário, adiciono uma tag e colo o udf que compilei agora. Porém, é apenas um espaço reservado e não dará resultados úteis.

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

Eu tenho outra consulta que eu defino separadamente, que é mais próxima do que eu preciso, mas bloqueada pelo fato de não poder calcular em colunas com janelas. Eu também tentei um similar que fornece mais ou menos a mesma saída apenas com um LAG () sobre MyDate, cercado por um datado.

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;
Independente
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Paul White Reinstate Monica

Respostas:

22

Enquanto leio a pergunta, o algoritmo recursivo básico necessário é:

  1. Retornar a linha com a data mais antiga do conjunto
  2. Defina essa data como "atual"
  3. Encontre a linha com a data mais antiga mais de 90 dias após a data atual
  4. Repita da etapa 2 até que não sejam encontradas mais linhas

Isso é relativamente fácil de implementar com uma expressão de tabela comum recursiva.

Por exemplo, usando os seguintes dados de amostra (com base na pergunta):

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

O código recursivo é:

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

Os resultados são:

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

Com um índice tendo TheDatecomo chave principal, o plano de execução é muito eficiente:

Plano de execução

Você pode optar por agrupar isso em uma função e executá-lo diretamente contra a visão mencionada na pergunta, mas meus instintos são contra. Geralmente, o desempenho é melhor quando você seleciona linhas de uma visualização em uma tabela temporária, fornece o índice apropriado na tabela temporária e aplica a lógica acima. Os detalhes dependem dos detalhes da exibição, mas esta é minha experiência geral.

Para completar (e solicitado pela resposta do ypercube), devo mencionar que minha outra solução para esse tipo de problema (até o T-SQL obter as funções de conjunto ordenadas apropriadas) é um cursor SQLCLR ( consulte minha resposta aqui para obter um exemplo da técnica ) Isso tem um desempenho muito melhor do que um cursor T-SQL e é conveniente para aqueles com habilidades em linguagens .NET e a capacidade de executar o SQLCLR em seu ambiente de produção. Pode não oferecer muito nesse cenário em relação à solução recursiva, porque a maior parte do custo é do tipo, mas vale a pena mencionar.

Paul White restabelece Monica
fonte
9

Uma vez que este é uma pergunta do SQL Server 2014, também posso adicionar uma versão de procedimento armazenado compilada nativamente de um "cursor".

Tabela de origem com alguns dados:

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

Um tipo de tabela que é o parâmetro para o procedimento armazenado. Ajuste bucket_countadequadamente .

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 um procedimento armazenado que percorre o parâmetro com valor de tabela e coleta as linhas em @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

Código para preencher uma variável de tabela otimizada para memória usada como parâmetro para o procedimento armazenado compilado nativamente e chame o procedimento

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;

Resultado:

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

Atualizar:

Se, por algum motivo, você não precisar visitar todas as linhas da tabela, poderá fazer o equivalente à versão "pular para a próxima data" implementada no CTE recursivo de Paul White.

O tipo de dados não precisa da coluna ID e você não deve usar um índice de hash.

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

E o procedimento armazenado usa a select top(1) ..para encontrar o próximo valor.

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
fonte
Suas soluções usando DATEADD e DATEDIFF podem retornar resultados diferentes, dependendo do conjunto de dados inicial.
Pavel Nefyodov 12/12/14
@PavelNefyodov Não vejo isso. Você pode explicar ou dar um exemplo?
Mikael Eriksson
Você poderia verificá-lo em datas como esta ('2014-01-01 00: 00: 00.000'), ('2014-04-01 01: 00: 00.000'), por favor? Mais informações podem ser encontradas na minha resposta.
Pavel Nefyodov
@PavelNefyodov Ah, entendo. Então, se eu mudar o segundo para T.TheDate >= dateadd(day, 91, @CurDate)tudo ficaria bem certo?
Mikael Eriksson
Ou, se apropriado, para OP, altere o tipo de dados de TheDatepara TTypepara Date.
Mikael Eriksson
5

Uma solução que usa um cursor.
(primeiro, algumas tabelas e variáveis ​​necessárias) :

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

O cursor real:

-- 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 obtendo os resultados:

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

Testado no SQLFiddle

ypercubeᵀᴹ
fonte
Marque com +1 essa solução, mas não porque é a maneira mais eficiente de fazer as coisas.
Pavel Nefyodov
@PavelNefyodov, devemos testar o desempenho!
precisa saber é o seguinte
Eu confio em Paul White nisso. Minha experiência com testes de desempenho não é tão impressionante. Novamente, isso não me impede de votar sua resposta.
Pavel Nefyodov 01/12/2014
Obrigado ypercube. Como esperado rapidamente em quantidade limitada de linhas. Em 13000 linhas, o CTE e este executaram mais ou menos o mesmo. Em 130.000 linhas, houve uma diferença de 600%. Em 13m, passa 15 minutos no meu equipamento de teste. Também tive que remover a chave primária, o que pode afetar um pouco o desempenho.
Independente
Thnx para teste. Você também pode testar modificando para fazer INSERT @cdapenas quando @Qualify=1(e, portanto, não inserindo 13 milhões de linhas se não precisar de todas elas na saída). E a solução depende de encontrar um índice TheDate. Se não houver, não será eficiente.
precisa saber é o seguinte
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 

Resultado

insira a descrição da imagem aqui

Veja também Como calcular o total em execução no SQL Server

atualização: veja abaixo os resultados dos testes de desempenho.

Devido à lógica diferente usada para encontrar "intervalo de 90 dias", a ypercube e minhas soluções, se deixadas intactas, podem retornar resultados diferentes à solução de Paul White. Isso se deve ao uso de DATEDIFF e DATEADD funções respectivamente.

Por exemplo:

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

retorna '2014-04-01 00: 00: 00.000', o que significa que '2014-04-01 01: 00: 00.000' está além do intervalo de 90 dias

mas

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

Retorna '90', o que significa que ainda está dentro da lacuna.

Considere um exemplo de varejista. Nesse caso, vender um produto perecível que tenha sido vendido por data '01-01-2014' em '01-01-2014 23: 59: 59: 999' está correto. Portanto, o valor DATEDIFF (DIA, ...) neste caso é OK.

Outro exemplo é um paciente esperando para ser visto. Para alguém que chega em '2014-01-01 00: 00: 00: 000' e sai em '2014-01-01 23: 59: 59: 999', são 0 (zero) dias se DATEDIFF for usado, mesmo que o a espera real foi de quase 24 horas. Novamente, o paciente que chega em '2014-01-01 23:59:59' e sai em '2014-01-02 00:00:01' esperou um dia se DATEDIFF for usado.

Mas eu discordo.

Deixei as soluções DATEDIFF e até o desempenho as testou, mas elas realmente deveriam estar em sua própria liga.

Também foi observado que, para os grandes conjuntos de dados, é impossível evitar valores no mesmo dia. Portanto, se dissermos 13 milhões de registros com 2 anos de dados, teremos mais de um registro por alguns dias. Esses registros estão sendo filtrados na primeira oportunidade nas soluções DATEDIFF da minha e da ypercube. Espero que ypercube não se importe com isso.

As soluções foram testadas na tabela a seguir

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

com dois índices agrupados diferentes (mydate neste caso):

CREATE CLUSTERED INDEX CI_mydate on vGetVisits(mydate) 
GO

A tabela foi preenchida da seguinte maneira

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

Para um caso de milhões de linhas, o INSERT foi alterado de tal maneira que entradas de 0 a 20 minutos foram adicionadas aleatoriamente.

Todas as soluções foram cuidadosamente agrupadas no código a seguir

SET NOCOUNT ON
GO

DECLARE @StartDate DATETIME

SET @StartDate = GETDATE()

--- Code goes here

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

Códigos reais testados (em nenhuma ordem específica):

Solução DATEDIFF da 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 ;

Solução DATEADD da 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 ;

A solução 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);

Minha solução 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 ;

Minha solução 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]

Estou usando o SQL Server 2012, então peço desculpas a Mikael Eriksson, mas seu código não será testado aqui. Eu ainda esperaria que suas soluções com DATADIFF e DATEADD retornassem valores diferentes em alguns conjuntos de dados.

E os resultados reais são: insira a descrição da imagem aqui

Pavel Nefyodov
fonte
Pavel, obrigado. Eu realmente não obtive um resultado da sua solução dentro do prazo. Reduzi meus dados de teste para 1000 linhas até obter um tempo de execução em 25 segundos. Quando adicionei um grupo por data e converti em datas no select, obtive a saída correta! Apenas por uma questão, deixei a consulta continuar com minha pequena tabela de dados de teste (13k linhas) e obtive mais de 12 minutos, o que significa um desempenho maior que o (nx)! Portanto, parece útil para conjuntos que com certeza serão pequenos.
Independente
Qual foi a tabela que você usou nos testes? Quantas linhas? No entanto, não sei por que você precisou adicionar o grupo por data para obter a saída correta. Sinta-se à vontade para publicar seus recursos como parte de sua pergunta (atualizado).
Pavel Nefyodov 02/12/2014
Oi! Vou acrescentar isso amanhã. O grupo deveria combinar datas duplicadas. Mas eu estava com pressa (tarde da noite) e talvez isso já tenha sido feito adicionando convert (date, z). A quantidade de linhas está no meu comentário. Eu tentei 1000 linhas com sua solução. Também tentei 13.000 linhas com 12 minutos de execução. Pauls e Ypercubes também foram tentados à mesa de 130.000 e 13 milhões. A tabela era uma tabela simples com datas aleatórias criadas a partir de ontem e -2 anos atrás. Índice seguro no campo de data.
Independent
0

Ok, eu perdi alguma coisa ou por que você não pulava a recursão e voltava para si mesmo? Se a data for a chave primária, ela deverá ser única e, em ordem cronológica, se você planeja calcular o deslocamento para a próxima linha

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

Rendimentos

insira a descrição da imagem aqui

A menos que eu tenha perdido totalmente algo importante ...

Sabre
fonte
2
Você provavelmente deseja alterar isso WHERE [TheDate] > [T1].[TheDate]para levar em conta o limite de diferença de 90 dias. Mas ainda assim, sua saída não é a desejada.
precisa saber é o seguinte
Importante: seu código deve ter "90" em algum lugar.
Pavel Nefyodov 02/12/2014