Як рекурсивно знаходити проміжки, де пройшло 90 днів, між рядками


17

Це своєрідне тривіальне завдання в моєму домашньому світі C #, але я ще не роблю його в SQL і вважаю за краще вирішити його на основі набору (без курсорів). Набір результатів має виходити з такого запиту.

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

Як це має працювати

Я надсилаю ці три парами в АДС.
UDF внутрішньо використовує параметри для отримання зв'язаних <= 90 днів старих рядків.
Перехід UDF "MyDate" та повернення 1, якщо він повинен бути включений до загального розрахунку.
Якщо цього не слід, то він повертається 0. Названий тут як "кваліфікований".

Що буде робити УДФ

Перерахуйте рядки за порядком дати. Обчисліть дні між рядками. Перший рядок у наборі результатів за замовчуванням до Хіт = 1. Якщо різниця дорівнює 90, - тоді переходите до наступного рядка, доки сума пропусків не складе 90 днів (90-й день повинен пройти). Коли досягнуто, встановіть Хіт на 1 та скиньте пробіл у 0 . Також було б працювати, щоб замість цього опустити рядок з результату.

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

У таблиці вище, стовпчик MaxDiff - це розрив від дати у попередньому рядку. Проблема моїх спроб досі полягає в тому, що я не можу ігнорувати другий останній рядок у зразку вище.

[EDIT]
Відповідно до коментаря я додаю тег і також вставляю udf, який я склав саме зараз. Хоча це просто заповнювач і не дасть корисного результату.

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

У мене є ще один запит, який я визначаю окремо, який ближче до того, що мені потрібно, але заблокований тим, що я не можу обчислити на вікнах стовпців. Я також спробував один аналогічний, який дає більш-менш однаковий вихід лише за допомогою LAG () над MyDate, оточений датованим.

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;

Коментарі не для розширеного обговорення; ця розмова перенесена в чат .
Пол Білий Відновити Моніку

Відповіді:


22

Коли я читаю питання, основний рекурсивний алгоритм необхідний:

  1. Поверніть рядок із найбільш ранньою датою в наборі
  2. Встановіть дату як "поточну"
  3. Знайдіть рядок із найбільш ранньою датою через 90 днів після поточної дати
  4. Повторіть дії з кроку 2, поки не знайдеться більше рядків

Це відносно легко реалізувати за допомогою рекурсивного загального вираження таблиці.

Наприклад, використовуючи такі вибіркові дані (на основі запитання):

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

Рекурсивний код:

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

Результати:

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

З індексом, що TheDateє головним ключем, план виконання дуже ефективний:

План виконання

Ви можете обрати цю функцію у функцію та виконати її безпосередньо проти думки, згаданої у питанні, але мої інстинкти проти. Зазвичай ефективність краща, коли ви вибираєте рядки з подання у тимчасову таблицю, надаєте відповідний індекс у тимчасовій таблиці, а потім застосуйте логіку вище. Деталі залежать від деталей погляду, але це мій загальний досвід.

Для повноти (і на запит відповіді ypercube) слід зазначити, що іншим моїм рішенням для такого типу проблеми (поки T-SQL не отримає належним чином впорядковані задані функції) є курсором SQLCLR ( див. Мою відповідь тут для прикладу методики ). Це набагато краще, ніж курсор T-SQL, і зручно для тих, хто володіє знаннями мов .NET і здатністю запускати SQLCLR у виробничому середовищі. Це може не запропонувати багато в цьому сценарії через рекурсивне рішення, оскільки більша частина вартості є такою, але варто згадати.


9

Оскільки це є 2014 питання SQL Server я міг би також додати спочатку скомпільовані версії збереженої процедури у вигляді «движка».

Таблиця джерела з деякими даними:

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

Тип таблиці, який є параметром для збереженої процедури. Налаштуйте bucket_countвідповідним чином .

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

І збережена процедура, яка проходить через параметр, що оцінюється в таблиці, і збирає рядки в @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

Код для заповнення змінної таблиці, оптимізованої для пам'яті, яка використовується як параметр для первісно складеної збереженої процедури та викликає процедуру.

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;

Результат:

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

Оновлення:

Якщо вам з якихось причин не потрібно відвідувати кожен рядок таблиці, ви можете виконати еквівалент версії "перейти до наступної дати", реалізованої в рекурсивному CTE Полом Вайт.

Тип даних не потребує стовпця ідентифікатора, і ви не повинні використовувати хеш-індекс.

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

І збережена процедура використовує a, select top(1) ..щоб знайти наступне значення.

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

Ваші рішення, що використовують DATEADD та DATEDIFF, можуть повертати різні результати залежно від початкового набору даних.
Павло Нефьодов

@PavelNefyodov Я цього не бачу. Чи можете ви пояснити чи навести приклад?
Мікаел Ерікссон

Чи можете ви перевірити це у такі дати ('2014-01-01 00: 00: 00.000'), ('2014-04-01 01: 00: 00.000'), будь ласка? Більше інформації можна знайти у моїй відповіді.
Павло Нефьодов

@PavelNefyodov Ага, бачу. Тож якщо я поміняю другий, T.TheDate >= dateadd(day, 91, @CurDate)все було б добре, правда?
Мікаель Ерікссон

Або, якщо доречно ОП, змінити тип даних TheDateв TTypeна Date.
Мікаель Ерікссон

5

Рішення, яке використовує курсор.
(по-перше, деякі необхідні таблиці та змінні) :

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

Фактичний курсор:

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

І отримання результатів:

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

Тестовано на SQLFiddle


+1 до цього рішення, але не тому, що це найефективніший спосіб робити справи.
Павло Нефьодов

@PavelNefyodov, тоді ми повинні перевірити продуктивність!
ypercubeᵀᴹ

Я довіряю цьому Полю Уайту. Мій досвід тестування продуктивності не такий вражаючий. Знову це не заважає мені голосувати за вашу відповідь.
Павло Нефьодов

Дякую, іперкуба. Як очікується швидкий на обмеженій кількості рядків. На 13000 рядків CTE і це виконували більш-менш те саме. На 130.000 рядків була різниця на 600%. На 13м проходить 15 хвилин на моєму тестовому обладнанні. Також мені довелося видалити первинний ключ, який може трохи вплинути на продуктивність.
Незалежне

Thnx для тестування. Ви також можете перевірити, змінивши параметр "робити" INSERT @cdлише тоді, коли @Qualify=1(і, таким чином, не вставляти 13М рядків, якщо вони вам не потрібні всі у висновку). І рішення залежить від знаходження індексу на TheDate. Якщо його немає, це не буде ефективно.
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 

Результат

введіть тут опис зображення

Подивіться також, як обчислити загальний обсяг роботи на SQL Server

оновлення: див. нижче результати тестування продуктивності.

Через різну логіку, яка використовується для пошуку «90-денного розриву» ypercube, і мої рішення, якщо залишити неушкодженим, можуть повернути різні результати до рішення Пола Вайта. Це пов’язано із застосуванням DATEDIFF і DATEADD функцій відповідно.

Наприклад:

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

повертає "2014-04-01 00: 00: 00.000", тобто "2014-04-01 01: 00: 00.000" перевищує 90 днів

але

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

Повертає "90", тобто все ще знаходиться в проміжку.

Розглянемо приклад роздрібної торгівлі. У цьому випадку продаж швидкопсувного продукту, який продається за датою "2014-01-01" на "2014-01-01 23: 59: 59: 999", прекрасно. Тому значення DATEDIFF (DAY, ...) у цьому випадку нормально.

Інший приклад - пацієнт, який чекає, щоб його побачили. Для тих, хто приходить у "2014-01-01 00: 00: 00: 000" і виходить у "2014-01-01 23: 59: 59: 999", це 0 (нульові) дні, якщо DATEDIFF використовується, хоча фактичне очікування було майже 24 години. Знову пацієнт, який прийшов у "2014-01-01 23:59:59" і пішов у "2014-01-02 00:00:01", чекав день, якщо використовується DATEDIFF.

Але я відволікаюсь.

Я залишив рішення DATEDIFF і навіть перевірив продуктивність, але вони справді повинні бути у власній лізі.

Також було відмічено, що для великих наборів даних неможливо уникнути значень одного дня. Отже, якщо ми скажемо, що 13 мільйонів записів, що охоплюють дані за 2 роки, то ми будемо мати кілька записів протягом декількох днів. Ці записи фільтруються при першій же можливості в рішеннях DATEDIFF мого та ypercube. Сподіваюся, що Іперкуб це не проти.

Розчини тестували на наступній таблиці

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

з двома різними кластерними індексами (в цьому випадку мій датчик):

CREATE CLUSTERED INDEX CI_mydate on vGetVisits(mydate) 
GO

Таблиця була заповнена таким чином

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

Для багатомільйонного випадку рядка INSERT було змінено таким чином, що 0-20 хвилин запису були випадковим чином додані.

Всі розчини ретельно були загорнуті в наступний код

SET NOCOUNT ON
GO

DECLARE @StartDate DATETIME

SET @StartDate = GETDATE()

--- Code goes here

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

Фактичні випробувані коди (не в конкретному порядку):

Рішення DATEDIFF від 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 ;

Рішення DATEADD 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 ;

Рішення Пола Уайта ( 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);

Моє рішення 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 ;

Моє рішення 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]

Я використовую SQL Server 2012, тому вибачаюся перед Мікаелем Ерікссоном, але його код тут не перевірятиметься. Я все ще очікую, що його рішення з DATADIFF та DATEADD повернуть різні значення для деяких наборів даних.

А фактичні результати: введіть тут опис зображення


Спасибі Павло. Я не отримав результату вашого рішення вчасно. Я зменшував свої тестові дані на 1000 рядків, поки не отримав час виконання на 25 сек. Коли я додав групу за датою та перетворив її на дати у вибраному, я отримав правильний вихід! Просто заради того, я дозволив запиту продовжувати свою маленьку таблицю-тестданих (13k рядків) і отримав більше 12 хвилин, що означає ефективність більше ніж o (nx)! Тож це виглядає корисним для наборів, яких точно буде мало.
Незалежне

Яку таблицю ви використовували при тестах? Скільки рядків? Не впевнений, чому вам довелося додати групу за датою, щоб отримати правильний результат. Будь ласка, не соромтеся публікувати свої кошти як частину вашого питання (оновлено).
Павло Нефьодов

Привіт! Я додам це завтра. Група by повинна була поєднати повторювані дати. Але я поспішав (пізньої ночі) і, можливо, це вже було зроблено, додавши Converter (дата, z). Кількість рядків у моєму коментарі. Я спробував 1000 рядків з вашим розчином. Також спробували 13.000 рядків з 12-хвилинним виконанням. Паульс і Іперкуб також спокушалися столом 130 000 і 13 мільйонів. Стіл являв собою звичайну таблицю з випадковими датами, створеними з вчорашнього дня і -2 років тому. Показ індексу в полі дати.
Незалежне

0

Гаразд, я щось пропустив або чому б ти не просто пропустив рекурсію і приєднався до себе? Якщо дата є первинним ключем, вона повинна бути унікальною, і в хронологічному порядку, якщо ви плануєте підрахувати зміщення до наступного рядка

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

Врожайність

введіть тут опис зображення

Якщо я зовсім не пропустив щось важливе…


2
Напевно, ви хочете змінити це, WHERE [TheDate] > [T1].[TheDate]щоб врахувати поріг різниці за 90 днів. Але все-таки ваш вихід не є шуканим.
ypercubeᵀᴹ

Важливо: у коді десь має бути "90".
Павло Нефьодов
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.