Как рекурсивно найти промежутки, где прошло 90 дней, между рядами


17

Это своего рода тривиальная задача в моем домашнем мире C #, но я еще не сделал это в SQL и предпочел бы решать ее на основе множеств (без курсоров). Набор результатов должен исходить из запроса, подобного этому.

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

Как это должно работать

Я отправляю эти три параметра в UDF.
UDF внутренне использует параметры для извлечения связанных строк <= 90 дней из представления.
UDF пересекает «MyDate» и возвращает 1, если оно должно быть включено в общий расчет.
Если это не так, то возвращается 0. Именуется здесь как «квалификация».

Что сделает udf

Перечислите строки в порядке дат. Подсчитайте дни между строками. Первая строка в наборе результатов по умолчанию имеет значение Hit = 1. Если разница составляет до 90, - затем переходите к следующей строке, пока сумма разрывов не станет равной 90 дням (90-й день должен пройти). По достижении установите Hit на 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 - это разрыв с датой в предыдущей строке. Проблема с моими попытками до сих пор состоит в том, что я не могу игнорировать вторую последнюю строку в приведенном выше примере.

[РЕДАКТИРОВАТЬ]
В соответствии с комментарием я добавляю тег, а также вставляю только что скомпилированный 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);

И хранимая процедура использует, 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)все, то все будет хорошо, верно?
Микаэль Эрикссон

Или, если это необходимо для OP, измените тип данных 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ᵀᴹ

Я доверяю Полу Уайту в этом. Мой опыт тестирования производительности не так впечатляет. Опять же, это не мешает мне проголосовать за ваш ответ.
Павел Нефёдов

Спасибо, ypercube. Как и ожидалось, быстро на ограниченном количестве строк. На 13000 строк CTE и это выполнялось более или менее одинаково. На 130 000 строк разница составляла 600%. На 13м это проходит 15 минут на моем испытательном оборудовании. Также мне пришлось удалить первичный ключ, что может немного повлиять на производительность.
Независимо

Спасибо за тестирование. Вы также можете выполнить тестирование, изменив на делать INSERT @cdтолько когда @Qualify=1(и, следовательно, не вставляя 13M строк, если вам не нужны все из них в выводе). И решение зависит от поиска индекса 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 от my и ypercube. Надеюсь, что ypercube не возражает против этого.

Решения были проверены на следующей таблице

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

с двумя разными кластерными индексами (в данном случае mydate):

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

Фактические проверенные коды (в произвольном порядке):

Решение Ypercube DATEDIFF ( 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 ;

Решение Ypercube для DATEADD ( 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 секунд. Когда я добавил группу по дате и преобразовал ее в выбранные даты, я получил правильный вывод! Ради этого я позволил продолжить выполнение запроса с моей таблицей small-testdata-таблицей (13 тыс. Строк) и получил более 12 минут, что означает производительность более чем o (nx)! Так что это выглядит полезным для наборов, которые наверняка будут маленькими.
Независимо

Какую таблицу вы использовали в тестах? Сколько строк? Не уверен, почему вам пришлось добавить группу по дате, чтобы получить правильный вывод, хотя. Пожалуйста, не стесняйтесь публиковать ваши средства как часть вашего вопроса (обновлено).
Павел Нефёдов

Здравствуй! Я добавлю это завтра. Группа должна была объединить повторяющиеся даты. Но я спешил (поздно ночью) и, возможно, это было уже сделано путем добавления convert (date, 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.