So finden Sie rekursiv Lücken zwischen den Zeilen, in denen 90 Tage vergangen sind


17

Dies ist eine Art triviale Aufgabe in meiner C # -Homeworld, aber ich habe es noch nicht in SQL geschafft und würde es vorziehen, sie satzbasiert (ohne Cursor) zu lösen. Eine Ergebnismenge sollte aus einer Abfrage wie dieser stammen.

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

Wie sollte es funktionieren

Ich schicke diese drei Parameter in eine UDF.
Die UDF verwendet intern params, um verknüpfte <= 90 Tage ältere Zeilen aus einer Ansicht abzurufen.
Die UDF durchläuft 'MyDate' und gibt 1 zurück, wenn sie in eine Gesamtberechnung einbezogen werden soll.
Sollte dies nicht der Fall sein, wird 0 zurückgegeben. Wird hier als "qualifizierend" bezeichnet.

Was die UdF machen wird

Listen Sie die Zeilen in Datumsreihenfolge auf. Berechnen Sie die Tage zwischen den Zeilen. Die erste Zeile in der Ergebnismenge ist standardmäßig Treffer = 1. Wenn die Differenz bis zu 90 beträgt, - gehen Sie zur nächsten Zeile über, bis die Summe der Lücken 90 Tage beträgt (der 90. Tag muss vergehen). Setzen Sie bei Erreichen Treffer auf 1 und Lücke auf 0 zurück Es würde auch funktionieren, stattdessen die Zeile aus dem Ergebnis wegzulassen.

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

In der obigen Tabelle ist die MaxDiff-Spalte die Lücke zum Datum in der vorherigen Zeile. Das Problem bei meinen bisherigen Versuchen ist, dass ich die vorletzte Zeile im obigen Beispiel nicht ignorieren kann.

[BEARBEITEN]
Gemäß Kommentar füge ich ein Tag hinzu und füge auch das udf ein, das ich gerade zusammengestellt habe. Dies ist jedoch nur ein Platzhalter und liefert kein nützliches Ergebnis.

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

Ich habe eine andere Abfrage, die ich separat definiere, die eher dem entspricht, was ich brauche, aber blockiert ist, weil ich nicht mit Fensterspalten rechnen kann. Ich habe auch ein ähnliches Modell ausprobiert, das mehr oder weniger die gleiche Ausgabe liefert, nur mit einer LAG () über MyDate, umgeben von einem Datum.

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;

Kommentare sind nicht für längere Diskussionen gedacht. Diese Unterhaltung wurde in den Chat verschoben .
Paul White sagt GoFundMonica

Antworten:


22

Während ich die Frage lese, ist der grundlegende erforderliche rekursive Algorithmus:

  1. Geben Sie die Zeile mit dem frühesten Datum im Satz zurück
  2. Setzen Sie dieses Datum als "aktuell"
  3. Suchen Sie die Zeile mit dem frühesten Datum, das mehr als 90 Tage nach dem aktuellen Datum liegt
  4. Wiederholen Sie ab Schritt 2, bis keine Zeilen mehr gefunden werden

Dies ist mit einem rekursiven allgemeinen Tabellenausdruck relativ einfach zu implementieren.

Verwenden Sie beispielsweise die folgenden Beispieldaten (basierend auf der Frage):

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

Der rekursive Code lautet:

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

Die Ergebnisse sind:

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

Mit einem Index TheDateals führendem Schlüssel ist der Ausführungsplan sehr effizient:

Ausführungsplan

Sie könnten dies in eine Funktion einschließen und direkt gegen die in der Frage erwähnte Ansicht ausführen, aber meine Instinkte sind dagegen. In der Regel ist die Leistung besser, wenn Sie Zeilen aus einer Ansicht in eine temporäre Tabelle auswählen, den entsprechenden Index für die temporäre Tabelle bereitstellen und dann die obige Logik anwenden. Die Details hängen von den Details der Ansicht ab, aber dies ist meine allgemeine Erfahrung.

Der Vollständigkeit halber (und aufgrund der Antwort von ypercube) sollte ich erwähnen, dass meine andere Lösung für diese Art von Problem (bis T-SQL ordnungsgemäß geordnete Mengenfunktionen erhält) ein SQLCLR-Cursor ist ( siehe meine Antwort hier für ein Beispiel der Technik) ). Dies ist wesentlich leistungsfähiger als ein T-SQL-Cursor und eignet sich für Benutzer mit Kenntnissen in .NET-Sprachen und der Fähigkeit, SQLCLR in ihrer Produktionsumgebung auszuführen. In diesem Szenario bietet es möglicherweise nicht viel mehr als die rekursive Lösung, da der Großteil der Kosten von der Art ist, aber es ist erwähnenswert.


9

Da es sich um eine SQL Server 2014-Frage handelt, kann ich auch eine nativ kompilierte gespeicherte Prozedurversion eines "Cursors" hinzufügen.

Quelltabelle mit einigen Daten:

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

Ein Tabellentyp, der der Parameter für die gespeicherte Prozedur ist. Passen Sie das bucket_countentsprechend an .

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

Und eine gespeicherte Prozedur, die den Tabellenwertparameter durchläuft und die Zeilen in sammelt @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

Code zum Füllen einer speicheroptimierten Tabellenvariablen, die als Parameter für die nativ kompilierte gespeicherte Prozedur verwendet wird, und Aufrufen der Prozedur.

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;

Ergebnis:

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

Aktualisieren:

Wenn Sie aus irgendeinem Grund nicht jede Zeile in der Tabelle besuchen müssen, können Sie das Gleiche tun wie die Version "Zum nächsten Datum springen", die in der rekursiven CTE von Paul White implementiert ist.

Der Datentyp benötigt keine ID-Spalte und Sie sollten keinen Hash-Index verwenden.

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

Und die gespeicherte Prozedur verwendet a select top(1) .., um den nächsten Wert zu finden.

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

Ihre Lösungen, die DATEADD und DATEDIFF verwenden, geben je nach ursprünglichem Datensatz möglicherweise unterschiedliche Ergebnisse zurück.
Pavel Nefyodov

@PavelNefyodov Das sehe ich nicht. Können Sie ein Beispiel nennen oder erklären?
Mikael Eriksson

Könnten Sie dies bitte an folgenden Daten überprüfen ('2014-01-01 00: 00: 00.000'), ('2014-04-01 01: 00: 00.000')? Weitere Informationen finden Sie in meiner Antwort.
Pavel Nefyodov

@PavelNefyodov Ah, ich verstehe. Also, wenn ich die zweite auf T.TheDate >= dateadd(day, 91, @CurDate)alle ändere, wäre das in Ordnung, oder?
Mikael Eriksson

Oder ändern Sie bei Bedarf den Datentyp von TheDatein TTypezu OP Date.
Mikael Eriksson

5

Eine Lösung, die einen Cursor verwendet.
(zuerst einige benötigte Tabellen und Variablen) :

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

Der eigentliche Cursor:

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

Und die Ergebnisse erhalten:

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

Getestet bei SQLFiddle


+1 zu dieser Lösung, aber nicht, weil es die effizienteste Art ist, Dinge zu tun.
Pavel Nefyodov

@PavelNefyodov dann sollten wir die Leistung testen!
ypercubeᵀᴹ

Darauf vertraue ich Paul White. Meine Erfahrung mit Leistungstests ist nicht so beeindruckend. Auch dies hindert mich nicht daran, Ihre Antwort abzustimmen.
Pavel Nefyodov

Vielen Dank, ypercube. Wie erwartet schnell bei begrenzter Zeilenanzahl. Bei 13000 Zeilen war die Leistung von CTE und CTE ungefähr gleich. Bei 130.000 Zeilen ergab sich ein Unterschied von 600%. Auf 13m vergehen auf meinem Testgerät 15 Minuten. Außerdem musste ich den Primärschlüssel entfernen, was die Leistung etwas beeinträchtigen könnte.
Unabhängige

Danke fürs Testen. Sie können auch testen, indem Sie ändern, um INSERT @cdnur zu tun , wenn @Qualify=1(und somit keine 13 Millionen Zeilen einfügen, wenn Sie nicht alle in der Ausgabe benötigen). Und die Lösung hängt von der Suche nach einem Index ab TheDate. Wenn es keine gibt, wird es nicht effizient sein.
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 

Ergebnis

Bildbeschreibung hier eingeben

Schauen Sie sich auch an, wie die laufende Summe in SQL Server berechnet wird

Update: siehe unten die Ergebnisse der Leistungstests.

Wegen der unterschiedlichen Logik beim Finden von "90 Tage Lücke" können ypercubes und meine Lösungen, wenn sie intakt bleiben, unterschiedliche Ergebnisse zu Paul Whites Lösung zurückgeben. Dies ist auf die Verwendung der Funktionen DATEDIFF und DATEADD zurückzuführen .

Beispielsweise:

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

gibt '2014-04-01 00: 00: 00.000' zurück, was bedeutet, dass '2014-04-01 01: 00: 00.000' eine Lücke von mehr als 90 Tagen aufweist

aber

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

Gibt '90' zurück, was bedeutet, dass es sich noch innerhalb der Lücke befindet.

Betrachten Sie ein Beispiel eines Einzelhändlers. In diesem Fall ist der Verkauf eines verderblichen Produkts mit dem Verkaufsdatum "2014-01-01" am "2014-01-01 23: 59: 59: 999" in Ordnung. Der Wert DATEDIFF (DAY, ...) ist in diesem Fall OK.

Ein anderes Beispiel ist ein Patient, der darauf wartet, gesehen zu werden. Für jemanden, der am '2014-01-01 00: 00: 00: 000' ankommt und am '2014-01-01 23: 59: 59: 999' abreist, sind es 0 (null) Tage, wenn DATEDIFF verwendet wird, obwohl das Die tatsächliche Wartezeit betrug fast 24 Stunden. Wieder wartete ein Patient, der am '2014-01-01 23:59:59' eintrifft und am '2014-01-02 00:00:01' weggeht, auf einen Tag, wenn DATEDIFF verwendet wird.

Aber ich schweife ab.

Ich habe DATEDIFF-Lösungen verlassen und sogar die Leistung getestet, aber sie sollten wirklich in ihrer eigenen Liga sein.

Es wurde auch bemerkt, dass es für die großen Datensätze unmöglich ist, Werte am selben Tag zu vermeiden. Wenn wir also sagen, dass 13 Millionen Datensätze 2 Jahre lang Daten umfassen, werden wir für einige Tage mehr als einen Datensatz haben. Diese Datensätze werden in den DATEDIFF-Lösungen von my und ypercube zum frühestmöglichen Zeitpunkt herausgefiltert. Hoffe, dass ypercube nichts dagegen hat.

Die Lösungen wurden in der folgenden Tabelle getestet

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

mit zwei verschiedenen Clustered-Indizes (in diesem Fall mein Datum):

CREATE CLUSTERED INDEX CI_mydate on vGetVisits(mydate) 
GO

Die Tabelle wurde wie folgt ausgefüllt

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

Für einen Multimillionen-Zeilenfall wurde INSERT so geändert, dass 0-20-Minuten-Einträge zufällig hinzugefügt wurden.

Alle Lösungen wurden sorgfältig in den folgenden Code eingepackt

SET NOCOUNT ON
GO

DECLARE @StartDate DATETIME

SET @StartDate = GETDATE()

--- Code goes here

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

Getestete tatsächliche Codes (in keiner bestimmten Reihenfolge):

DATEDIFF-Lösung von 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-Lösung von 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 ;

Paul Whites Lösung ( 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);

Meine DATEADD-Lösung ( 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 ;

Meine DATEDIFF-Lösung ( 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]

Ich verwende SQL Server 2012, entschuldige mich also bei Mikael Eriksson, aber sein Code wird hier nicht getestet. Ich würde immer noch erwarten, dass seine Lösungen mit DATADIFF und DATEADD bei einigen Datensätzen unterschiedliche Werte zurückgeben.

Und die tatsächlichen Ergebnisse sind: Bildbeschreibung hier eingeben


Vielen Dank, Pavel. Ich habe nicht wirklich rechtzeitig ein Ergebnis Ihrer Lösung erhalten. Ich habe meine Testdaten auf 1000 Zeilen verkleinert, bis ich eine Ausführungszeit von 25 Sekunden habe. Wenn ich eine Gruppe nach Datum hinzufügte und in der Auswahl in Datumsangaben umwandelte, erhielt ich die richtige Ausgabe! Nur um der Sache willen habe ich die Abfrage mit meiner kleinen Testdatentabelle (13k Zeilen) fortgesetzt und bin über 12 Minuten gelaufen, was eine Leistung von mehr als o (nx) bedeutet! Es sieht also nützlich für Sets aus, die mit Sicherheit klein sein werden.
Unabhängige

Welchen Tisch haben Sie in Tests verwendet? Wie viele Zeilen? Ich bin mir nicht sicher, warum Sie das Datum gruppieren mussten, um die korrekte Ausgabe zu erhalten. Bitte zögern Sie nicht, Ihre Finanzierung als Teil Ihrer Frage zu veröffentlichen (aktualisiert).
Pavel Nefyodov

Hallo! Ich werde das morgen hinzufügen. Die Gruppe von sollte doppelte Daten kombinieren. Aber ich hatte es eilig (spät in der Nacht) und vielleicht wurde es bereits durch das Hinzufügen von convert (date, z) erledigt. Die Anzahl der Zeilen steht in meinem Kommentar. Ich habe 1000 Zeilen mit Ihrer Lösung ausprobiert. Auch 13.000 Zeilen mit 12 Minuten Ausführung ausprobiert. Pauls und Ypercubes waren auch versucht, an den 130.000 und 13 Millionen Tischen zu sitzen. Die Tabelle war eine einfache Tabelle mit zufälligen Daten, die von gestern und vor -2 Jahren erstellt wurden. Clustured Index auf dem Datumsfeld.
Independent

0

Ok, habe ich etwas verpasst oder warum würden Sie nicht einfach die Rekursion überspringen und wieder zu sich selbst zurückkehren? Wenn das Datum der Primärschlüssel ist, muss es eindeutig sein und in chronologischer Reihenfolge, wenn Sie den Versatz zur nächsten Zeile berechnen möchten

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

Ausbeuten

Bildbeschreibung hier eingeben

Es sei denn, ich habe etwas Wichtiges verpasst ...


2
Sie möchten dies wahrscheinlich ändern, WHERE [TheDate] > [T1].[TheDate]um den 90-Tage-Differenzschwellenwert zu berücksichtigen. Trotzdem ist Ihre Ausgabe nicht die gewünschte.
Ypercubeᵀᴹ

Wichtig: Ihr Code sollte irgendwo "90" haben.
Pavel Nefyodov
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.