Cómo encontrar huecos recursivamente donde pasaron 90 días, entre filas


17

Este es un tipo de tarea trivial en mi mundo natal de C #, pero todavía no lo hago en SQL y preferiría resolverlo basado en conjuntos (sin cursores). Un conjunto de resultados debe provenir de una consulta como esta.

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

¿Cómo debería funcionar?

Envío esos tres parámetros a un UDF.
La UDF utiliza internamente parámetros para obtener filas relacionadas <= 90 días más antiguas, desde una vista.
El UDF atraviesa 'MyDate' y devuelve 1 si debe incluirse en un cálculo total.
Si no debería, entonces devuelve 0. Nombrado aquí como "calificado".

Lo que hará la udf

Liste las filas en orden de fecha. Calcule los días entre filas. La primera fila en el conjunto de resultados por defecto es Hit = 1. Si la diferencia es de hasta 90, - pase a la siguiente fila hasta que la suma de las brechas sea de 90 días (el día 90 debe pasar) Cuando se alcanza, establezca Hit en 1 y restablezca la brecha a 0 También funcionaría en su lugar omitir la fila del 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

En la tabla anterior, la columna MaxDiff es el intervalo desde la fecha en la línea anterior. El problema con mis intentos hasta ahora es que no puedo ignorar la segunda última fila de la muestra anterior.

[EDITAR]
Según el comentario, agrego una etiqueta y también pego el udf que he compilado en este momento. Sin embargo, es solo un marcador de posición y no dará resultados útiles.

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

Tengo otra consulta que defino por separado que está más cerca de lo que necesito, pero bloqueada con el hecho de que no puedo calcular en columnas con ventanas. También probé uno similar que da más o menos la misma salida solo con un LAG () sobre MyDate, rodeado de un fechado.

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;

Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Paul White reinstala a Monica

Respuestas:


22

Mientras leo la pregunta, el algoritmo recursivo básico requerido es:

  1. Devuelve la fila con la fecha más temprana en el conjunto
  2. Establecer esa fecha como "actual"
  3. Encuentre la fila con la fecha más temprana más de 90 días después de la fecha actual
  4. Repita desde el paso 2 hasta que no se encuentren más filas

Esto es relativamente fácil de implementar con una expresión de tabla común recursiva.

Por ejemplo, utilizando los siguientes datos de muestra (basados ​​en la pregunta):

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

El código recursivo es:

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

Los resultados son:

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

Con un índice TheDatecomo clave principal, el plan de ejecución es muy eficiente:

Plan de ejecución

Puede elegir incluir esto en una función y ejecutarlo directamente contra la vista mencionada en la pregunta, pero mis instintos están en contra. Por lo general, el rendimiento es mejor cuando selecciona filas de una vista en una tabla temporal, proporciona el índice apropiado en la tabla temporal y luego aplica la lógica anterior. Los detalles dependen de los detalles de la vista, pero esta es mi experiencia general.

Para completar (e impulsado por la respuesta de ypercube), debo mencionar que mi otra solución para este tipo de problema (hasta que T-SQL obtenga las funciones de conjunto ordenadas adecuadas) es un cursor SQLCLR ( vea mi respuesta aquí para ver un ejemplo de la técnica ) Esto funciona mucho mejor que un cursor T-SQL, y es conveniente para aquellos con habilidades en lenguajes .NET y la capacidad de ejecutar SQLCLR en su entorno de producción. Puede que no ofrezca mucho en este escenario sobre la solución recursiva porque la mayoría del costo es del tipo, pero vale la pena mencionarlo.


9

Como se trata de una pregunta de SQL Server 2014, también podría agregar una versión de procedimiento almacenado compilada de forma nativa de un "cursor".

Tabla fuente con algunos datos:

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 de tabla que es el parámetro del procedimiento almacenado. Ajuste el bucket_countapropiado .

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

Y un procedimiento almacenado que recorre el parámetro con valores de tabla y recoge las filas @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 llenar una variable de tabla de memoria optimizada que se utiliza como parámetro para el procedimiento almacenado compilado de forma nativa y llamar al procedimiento.

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

Actualizar:

Si por alguna razón no necesita visitar todas las filas de la tabla, puede hacer el equivalente de la versión "saltar a la próxima fecha" que Paul White implementa en el CTE recursivo.

El tipo de datos no necesita la columna ID y no debe usar un índice hash.

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

Y el procedimiento almacenado utiliza a select top(1) ..para encontrar el siguiente 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

Sus soluciones que usan DATEADD y DATEDIFF pueden devolver resultados diferentes dependiendo del conjunto de datos inicial.
Pavel Nefyodov

@PavelNefyodov No veo eso. ¿Puedes explicar o dar un ejemplo?
Mikael Eriksson

¿Podría verificarlo en fechas como esta ('2014-01-01 00: 00: 00.000'), ('2014-04-01 01: 00: 00.000'), por favor? Más información se puede encontrar en mi respuesta.
Pavel Nefyodov

@PavelNefyodov Ah, ya veo. Entonces, si cambio el segundo a T.TheDate >= dateadd(day, 91, @CurDate)todo, estaría bien, ¿verdad?
Mikael Eriksson

O, si corresponde a OP, cambie el tipo de datos de TheDatein TTypea Date.
Mikael Eriksson

5

Una solución que usa un cursor.
(primero, algunas tablas y variables necesarias) :

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

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

Y obteniendo los resultados:

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

Probado en SQLFiddle


+1 a esta solución pero no porque sea la forma más eficiente de hacer las cosas.
Pavel Nefyodov

@PavelNefyodov, ¡entonces deberíamos probar el rendimiento!
ypercubeᵀᴹ

Confío en Paul White en eso. Mi experiencia con las pruebas de rendimiento no es tan impresionante. Nuevamente, esto no me impide votar tu respuesta.
Pavel Nefyodov

Gracias ypercube. Como se esperaba rápido en una cantidad limitada de filas. En 13000 filas, The CTE y esto se desempeñaron más o menos igual. En 130,000 filas hubo una diferencia del 600%. En 13m, pasan 15 minutos en mi equipo de prueba. También tuve que eliminar la clave primaria, lo que puede afectar un poco el rendimiento.
Independiente

Thnx para pruebas. También puede probar modificando para hacer INSERT @cdsolo cuando @Qualify=1(y así no insertar 13M filas si no las necesita todas en la salida). Y la solución depende de encontrar un índice en TheDate. Si no hay uno, no será eficiente.
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 

Resultado

ingrese la descripción de la imagen aquí

También eche un vistazo a Cómo calcular el total acumulado en SQL Server

actualización: vea a continuación los resultados de las pruebas de rendimiento.

Debido a la lógica diferente utilizada para encontrar el "intervalo de 90 días", los ypercube y mis soluciones si se dejan intactos pueden devolver resultados diferentes a la solución de Paul White. Esto se debe al uso de las funciones DATEDIFF y DATEADD respectivamente.

Por ejemplo:

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

devuelve '2014-04-01 00: 00: 00.000', lo que significa que '2014-04-01 01: 00: 00.000' tiene más de 90 días de diferencia

pero

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

Devuelve '90', lo que significa que todavía está dentro del espacio.

Considere un ejemplo de un minorista. En este caso, está bien vender un producto perecedero que tenga fecha de vencimiento '2014-01-01' en '2014-01-01 23: 59: 59: 999'. Entonces el valor DATEDIFF (DÍA, ...) en este caso está bien.

Otro ejemplo es un paciente que espera ser visto. Para alguien que viene en '2014-01-01 00: 00: 00: 000' y se va en '2014-01-01 23: 59: 59: 999' son 0 (cero) días si se usa DATEDIFF aunque La espera real fue de casi 24 horas. Una vez más, el paciente que llega a '2014-01-01 23:59:59' y se retira a '2014-01-02 00:00:01' esperó un día si se usa DATEDIFF.

Pero yo divago.

Dejé las soluciones DATEDIFF e incluso el rendimiento las probó, pero realmente deberían estar en su propia liga.

También se observó que para los grandes conjuntos de datos es imposible evitar los valores del mismo día. Entonces, si tenemos 13 millones de registros que abarcan 2 años de datos, terminaremos teniendo más de un registro durante algunos días. Esos registros se filtran lo antes posible en las soluciones DATEDIFF de mi y ypercube. Espero que a Ypercube no le importe esto.

Las soluciones se probaron en la siguiente tabla

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

con dos índices agrupados diferentes (mydate en este caso):

CREATE CLUSTERED INDEX CI_mydate on vGetVisits(mydate) 
GO

La tabla se rellenó de la siguiente manera

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 un caso de filas multimillonarias, INSERT se cambió de tal manera que se agregaron aleatoriamente entradas de 0-20 minutos.

Todas las soluciones fueron cuidadosamente envueltas en el siguiente código

SET NOCOUNT ON
GO

DECLARE @StartDate DATETIME

SET @StartDate = GETDATE()

--- Code goes here

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

Códigos reales probados (sin ningún orden en particular):

Solución DATEDIFF de 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 ;

Solución DATEADD de 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 solución 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);

Mi solución 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 ;

Mi solución 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]

Estoy usando SQL Server 2012, así que pido disculpas a Mikael Eriksson, pero su código no se probará aquí. Todavía esperaría que sus soluciones con DATADIFF y DATEADD devuelvan valores diferentes en algunos conjuntos de datos.

Y los resultados reales son: ingrese la descripción de la imagen aquí


Gracias Pavel. Realmente no obtuve el resultado de su solución a tiempo. Reduje mis datos de prueba a 1000 filas hasta que obtuve un tiempo de ejecución de 25 segundos. Cuando agregué un grupo por fecha y lo convertí a fechas en la selección, ¡obtuve el resultado correcto! Solo por el bien, dejé que la consulta continuara con mi pequeña tabla de datos de prueba (13k filas) y obtuve más de 12 minutos, lo que significa un rendimiento de más de o (nx). Por lo tanto, parece útil para conjuntos que seguramente serán pequeños.
Independiente

¿Cuál fue la tabla que usaste en las pruebas? Cuantas filas Sin embargo, no estoy seguro de por qué tuvo que agregar el grupo por fecha para obtener la salida correcta. No dude en publicar sus fondos como parte de su pregunta (actualizada).
Pavel Nefyodov

¡Hola! Agregaré eso mañana. El grupo por debía combinar fechas duplicadas. Pero tenía prisa (tarde en la noche) y tal vez ya lo había hecho agregando convertir (fecha, z). La cantidad de filas está en mi comentario. Intenté 1000 filas con tu solución. También probé 13,000 filas con 12 minutos de ejecución. Pauls y Ypercubes también fueron tentados a la mesa de 130,000 y 13 millones. La tabla era una tabla simple con fechas aleatorias creadas desde ayer y hace -2 años. Índice agrupado en el campo de fecha.
Independiente

0

Ok, ¿me perdí algo o por qué no te saltearías la recursión y te unirías a ti mismo? Si la fecha es la clave principal, debe ser única y en orden cronológico si planea calcular el desplazamiento a la siguiente fila

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

Rendimientos

ingrese la descripción de la imagen aquí

A menos que me haya perdido por completo algo importante ...


2
Probablemente desee cambiar eso WHERE [TheDate] > [T1].[TheDate]para tener en cuenta el umbral de diferencia de 90 días. Pero aún así, su salida no es la deseada.
ypercubeᵀᴹ

Importante: su código debe tener "90" en alguna parte.
Pavel Nefyodov
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.