Jak rekurencyjnie znajdować luki, w których minęło 90 dni, między wierszami

17

Jest to rodzaj trywialnego zadania w moim ojczystym świecie C #, ale jeszcze nie robię tego w SQL i wolałbym rozwiązywać go w oparciu o zestaw (bez kursorów). Zestaw wyników powinien pochodzić z takiego zapytania.

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

Jak to powinno działać

Wysyłam te trzy parametry do UDF.
UDF wewnętrznie używa parametrów, aby pobrać z widoku powiązane <= 90 dni starsze wiersze.
Funkcja UDF przechodzi przez „MyDate” i zwraca 1, jeśli powinna zostać uwzględniona w obliczeniach całkowitych.
Jeśli nie, to zwraca 0. Nazwany tutaj jako „kwalifikujący się”.

Co zrobi udf

Wyświetl wiersze w kolejności dat. Oblicz dni między rzędami. Pierwszy rząd w zestawie wyników domyślnie wynosi Hit = 1. Jeśli różnica wynosi do 90, - następnie przejdź do następnego rzędu, aż suma przerw wyniesie 90 dni (musi minąć 90 dzień) Po osiągnięciu ustaw Hit na 1 i zresetuj lukę na 0 Można także zamiast tego pominąć wiersz wyniku.

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

W powyższej tabeli kolumna MaxDiff to odstęp od daty w poprzednim wierszu. Problem z moimi dotychczasowymi próbami polega na tym, że nie mogę zignorować drugiego rzędu w powyższej próbce.

[EDYCJA]
Zgodnie z komentarzem dodaję tag, a także wklejam udf, który właśnie skompilowałem. Chociaż jest to tylko symbol zastępczy i nie przyniesie użytecznego rezultatu.

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

Mam inne zapytanie, które osobno definiuję, które jest bardziej zbliżone do tego, czego potrzebuję, ale zablokowane przez fakt, że nie mogę obliczyć kolumn w okienkach. Próbowałem też jednego podobnego, który daje mniej więcej taki sam wynik tylko z LAG () nad MyDate, otoczony datownikiem.

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;
Niezależny
źródło
Komentarze nie są przeznaczone do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
Paul White przywraca Monikę

Odpowiedzi:

22

Gdy czytam pytanie, podstawowy wymagany algorytm rekurencyjny to:

  1. Zwraca wiersz z najwcześniejszą datą w zestawie
  2. Ustaw tę datę jako „bieżącą”
  3. Znajdź wiersz z najwcześniejszą datą ponad 90 dni po bieżącej dacie
  4. Powtarzaj od kroku 2, aż nie będzie już więcej wierszy

Jest to stosunkowo łatwe do wdrożenia z rekurencyjnym wspólnym wyrażeniem tabelowym.

Na przykład przy użyciu następujących przykładowych danych (na podstawie pytania):

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

Kod rekurencyjny to:

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

Wyniki są następujące:

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

Z indeksem mającym TheDateklucz wiodący plan wykonania jest bardzo wydajny:

Plan wykonania

Możesz zawinąć to w funkcję i wykonać bezpośrednio w widoku wymienionym w pytaniu, ale moje instynkty są temu przeciwne. Zwykle wydajność jest lepsza, gdy wybierzesz wiersze z widoku do tabeli tymczasowej, podaj odpowiedni indeks w tabeli tymczasowej, a następnie zastosuj powyższą logikę. Szczegóły zależą od szczegółów widoku, ale takie jest moje ogólne doświadczenie.

Dla kompletności (i podpowiedzi przez odpowiedź ypercube) powinienem wspomnieć, że moim innym rozwiązaniem tego typu problemu (dopóki T-SQL nie otrzyma poprawnie uporządkowanych funkcji zestawu) jest kursor SQLCLR ( zobacz moją odpowiedź tutaj dla przykładu techniki ). Działa to znacznie lepiej niż kursor T-SQL i jest wygodne dla osób posiadających umiejętności posługiwania się językiem .NET i możliwość uruchamiania SQLCLR w środowisku produkcyjnym. W tym scenariuszu może nie oferować wiele w porównaniu z rozwiązaniem rekurencyjnym, ponieważ większość kosztów jest tego rodzaju, ale warto o tym wspomnieć.

Paul White przywraca Monikę
źródło
9

Ponieważ tak jest to pytanie dotyczące programu SQL Server 2014, równie dobrze mogę dodać natywnie skompilowaną wersję „kursora” procedury składowanej.

Tabela źródłowa z niektórymi danymi:

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

Typ tabeli, który jest parametrem procedury składowanej. Dostosuj bucket_countodpowiednio .

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

I procedura składowana, która zapętla parametr o wartości tabeli i zbiera wiersze @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

Kod wypełniający zmienną tabelową zoptymalizowaną pod kątem pamięci, która jest używana jako parametr w natywnie skompilowanej procedurze składowanej i wywołuje tę procedurę.

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;

Wynik:

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

Aktualizacja:

Jeśli z jakiegoś powodu nie musisz odwiedzać każdego wiersza w tabeli, możesz wykonać odpowiednik wersji „przejdź do następnej daty”, która została zaimplementowana w rekurencyjnym CTE przez Paula White'a.

Typ danych nie wymaga kolumny identyfikatora i nie należy używać indeksu skrótu.

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

Procedura przechowywana używa select top(1) ..następnej wartości.

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
źródło
Rozwiązania korzystające z DATEADD i DATEDIFF mogą zwracać różne wyniki w zależności od początkowego zestawu danych.
Pavel Nefyodov,
@PavelNefyodov Nie widzę tego. Czy możesz wyjaśnić lub podać przykład?
Mikael Eriksson,
Czy możesz to sprawdzić w takich datach („01.01.2014 00: 00: 00.000”), („01.04.2014 01: 00: 00.000”), proszę? Więcej informacji można znaleźć w mojej odpowiedzi.
Pavel Nefyodov,
@PavelNefyodov Ach, rozumiem. Więc jeśli zmienię drugą na T.TheDate >= dateadd(day, 91, @CurDate)wszystkie, byłoby dobrze, prawda?
Mikael Eriksson,
Lub, jeśli jest to odpowiednie dla OP, zmień typ danych TheDatena TTypena Date.
Mikael Eriksson,
5

Rozwiązanie wykorzystujące kursor.
(po pierwsze, niektóre potrzebne tabele i zmienne) :

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

Rzeczywisty kursor:

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

I uzyskiwanie wyników:

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

Testowane w SQLFiddle

ypercubeᵀᴹ
źródło
+1 do tego rozwiązania w górę, ale nie dlatego, że jest to najbardziej efektywny sposób robienia rzeczy.
Pavel Nefyodov,
@PavelNefyodov, powinniśmy przetestować wydajność!
ypercubeᵀᴹ
Ufam Paulowi White'owi. Moje doświadczenie z testowaniem wydajności nie jest aż tak imponujące. Znów nie powstrzymuje mnie to od głosowania nad odpowiedzią.
Pavel Nefyodov
Dzięki Ypercube. Zgodnie z oczekiwaniami szybko w ograniczonej liczbie wierszy. W 13000 wierszach CTE i to działały mniej więcej tak samo. W 130 000 rzędach różnica wynosiła 600%. Na 13m minęło 15 minut na moim urządzeniu testowym. Musiałem także usunąć klucz podstawowy, co może nieco wpłynąć na wydajność.
Niezależny
Dzięki za testowanie. Możesz również przetestować, modyfikując, aby robić INSERT @cdtylko wtedy @Qualify=1(a tym samym nie wstawiać 13M wierszy, jeśli nie potrzebujesz wszystkich z nich na wyjściu). Rozwiązanie zależy od znalezienia indeksu TheDate. Jeśli nie ma, nie będzie wydajne.
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 

Wynik

wprowadź opis zdjęcia tutaj

Zobacz także, jak obliczyć całkowitą liczbę uruchomień w programie SQL Server

aktualizacja: patrz poniżej wyniki testów wydajności.

Z powodu odmiennej logiki znalezienia „90 dni przerwy” ypercube i moje rozwiązania, jeśli pozostawione nietknięte, mogą zwrócić różne wyniki do rozwiązania Paula White'a. Wynika to z użycia odpowiednio funkcji DATEDIFF i DATEADD .

Na przykład:

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

zwraca „2014-04-01 00: 00: 00.000”, co oznacza, że ​​„2014-04-01 01: 00: 00.000” przekracza 90 dni przerwy

ale

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

Zwraca „90”, co oznacza, że ​​nadal znajduje się w luce.

Rozważ przykład sprzedawcy. W takim przypadku sprzedaż łatwo psującego się produktu, który sprzedał się przed datą „2014-01-01” o godzinie „2014-01-01 23: 59: 59: 999”, jest w porządku. Zatem wartość DATEDIFF (DZIEŃ, ...) w tym przypadku jest OK.

Innym przykładem jest pacjent czekający na wizytę. Dla kogoś, kto przychodzi o godzinie „2014-01-01 00: 00: 00: 000” i odchodzi o godzinie „2014-01-01 23: 59: 59: 999”, jeśli DATEDIFF jest używany, to 0 (zero) dni faktyczne oczekiwanie trwało prawie 24 godziny. Znowu pacjent, który przychodzi o godzinie „01.01.2014 23:59:59” i odchodzi o godzinie „2014-01-02 00:00:01” czekał na dzień, jeśli zostanie użyty DATEDIFF.

Ale dygresję.

Zostawiłem rozwiązania DATEDIFF, a nawet przetestowałem je pod kątem wydajności, ale naprawdę powinny być w swojej lidze.

Zauważono również, że w przypadku dużych zestawów danych nie można uniknąć wartości tego samego dnia. Więc jeśli powiedzmy, że 13 milionów rekordów obejmuje 2 lata danych, w końcu będziemy mieć więcej niż jeden rekord przez kilka dni. Te rekordy są filtrowane przy najbliższej okazji w moich rozwiązaniach DATEDIFF mojego i ypercube. Mam nadzieję, że Ypercube nie ma nic przeciwko temu.

Rozwiązania przetestowano w poniższej tabeli

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

z dwoma różnymi indeksami klastrowymi (w tym przypadku mydate):

CREATE CLUSTERED INDEX CI_mydate on vGetVisits(mydate) 
GO

Tabela została wypełniona w następujący sposób

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

W przypadku wielomilionowego przypadku wiersza zmieniono INSERT w taki sposób, że losowo dodawano wpisy 0-20 minut.

Wszystkie rozwiązania zostały starannie zapakowane w następujący kod

SET NOCOUNT ON
GO

DECLARE @StartDate DATETIME

SET @StartDate = GETDATE()

--- Code goes here

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

Rzeczywiste kody testowane (bez określonej kolejności):

Rozwiązanie DATEDIFF firmy 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 ;

Rozwiązanie DATEADD firmy 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 ;

Rozwiązanie Paula White'a ( 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);

Moje rozwiązanie 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 ;

Moje rozwiązanie 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]

Korzystam z programu SQL Server 2012, więc przepraszam Mikaela Erikssona, ale jego kod nie będzie tutaj testowany. Nadal oczekiwałbym, że jego rozwiązania z DATADIFF i DATEADD zwrócą różne wartości w niektórych zestawach danych.

Rzeczywiste wyniki to: wprowadź opis zdjęcia tutaj

Pavel Nefyodov
źródło
Dziękuję Pavel. Tak naprawdę nie dostałem wyniku twojego rozwiązania w czasie. Zmniejszam moje dane testowe do 1000 wierszy, aż do czasu wykonania w 25 sekund. Kiedy dodałem grupę według daty i przekonwertowałem na daty w zaznaczeniu, otrzymałem poprawny wynik! Na wszelki wypadek pozwoliłem, aby zapytanie kontynuowało moją małą tabelę danych testowych (13 tys. Wierszy) i uzyskałem ponad 12 minut, co oznacza wydajność większą niż o (nx)! Wygląda więc przydatnie na zestawy, które na pewno będą małe.
Niezależny
Jakiej tabeli użyłeś w testach? Ile rzędów? Nie jestem pewien, dlaczego musiałeś dodać grupę według daty, aby uzyskać poprawny wynik. Opublikuj swoje fundusze w ramach pytania (zaktualizowanego).
Pavel Nefyodov,
Cześć! Dodam to jutro. Grupa według miała połączyć zduplikowane daty. Ale spieszyłem się (późno w nocy) i być może już to zrobiłem, dodając konwersję (data, z). Ilość wierszy jest w moim komentarzu. Wypróbowałem 1000 wierszy z twoim rozwiązaniem. Próbowałem także 13 000 wierszy z 12-minutowym wykonaniem. Pauls i Ypercubes również byli kuszeni do stołu 130 000 i 13 milionów. Stół był zwykłym stołem z losowymi datami utworzonymi z wczoraj i -2 lata wstecz. Indeks klastrowany w polu daty.
Niezależny
0

Ok, czy coś przeoczyłem lub dlaczego po prostu nie pominąłeś rekurencji i nie wróciłeś do siebie? Jeśli data jest kluczem podstawowym, musi być unikalna i chronologicznie, jeśli planujesz obliczać przesunięcie do następnego wiersza

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

Wydajność

wprowadź opis zdjęcia tutaj

Chyba że całkowicie przegapiłem coś ważnego ....

Szabla
źródło
2
Prawdopodobnie chcesz to zmienić, WHERE [TheDate] > [T1].[TheDate]aby uwzględnić 90-dniowy próg różnicy. Ale nadal twoja produkcja nie jest pożądana.
ypercubeᵀᴹ
Ważne: kod powinien mieć gdzieś „90”.
Pavel Nefyodov,