Cara menemukan celah secara rekursif di mana 90 hari berlalu, di antara baris

17

Ini adalah semacam tugas sepele di homeworld C # saya, tapi saya belum membuatnya dalam SQL dan lebih suka menyelesaikannya set-based (tanpa kursor). Set hasil harus berasal dari kueri seperti ini.

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

Bagaimana cara kerjanya?

Saya mengirim ketiga param itu ke UDF.
UDF secara internal menggunakan params untuk mengambil baris terkait <= 90 hari yang lebih lama, dari tampilan.
UDF melintasi 'MyDate' dan mengembalikan 1 jika harus dimasukkan dalam perhitungan total.
Jika tidak, maka mengembalikan 0. Dinamakan di sini sebagai "kualifikasi".

Apa yang akan dilakukan udf

Daftar baris dalam urutan tanggal. Hitung hari di antara baris. Baris pertama dalam resultset default ke Hit = 1. Jika selisihnya mencapai 90, - lalu lanjutkan ke baris berikutnya hingga jumlah kesenjangan adalah 90 hari (hari ke-90 harus lewat) Ketika tercapai, atur Hit ke 1 dan reset celah ke 0 Ini juga akan berfungsi untuk menghilangkan baris dari hasil.

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

Dalam tabel di atas, kolom MaxDiff adalah jarak dari tanggal di baris sebelumnya. Masalah dengan upaya saya sejauh ini adalah bahwa saya tidak dapat mengabaikan baris terakhir kedua dalam contoh di atas.

[EDIT]
Sesuai komentar saya menambahkan tag dan juga menempelkan udf yang telah saya kompilasi tadi. Padahal, hanya penampung dan tidak akan memberikan hasil yang bermanfaat.

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

Saya memiliki permintaan lain yang saya tentukan secara terpisah yang lebih dekat dengan yang saya butuhkan, tetapi diblokir dengan fakta yang tidak dapat saya hitung pada kolom berjendela. Saya juga mencoba satu serupa yang memberikan hasil yang kurang lebih sama hanya dengan LAG () lebih dari MyDate, dikelilingi dengan tanggaliff.

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;
Independen
sumber
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Paul White Reinstate Monica

Jawaban:

22

Ketika saya membaca pertanyaan, algoritma rekursif dasar yang diperlukan adalah:

  1. Kembalikan baris dengan tanggal paling awal di set
  2. Tetapkan tanggal itu sebagai "saat ini"
  3. Temukan baris dengan tanggal paling awal lebih dari 90 hari setelah tanggal saat ini
  4. Ulangi dari langkah 2 hingga tidak ada lagi baris yang ditemukan

Ini relatif mudah diimplementasikan dengan ekspresi tabel umum rekursif.

Misalnya, menggunakan data sampel berikut (berdasarkan pertanyaan):

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

Kode rekursif adalah:

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

Hasilnya adalah:

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

Dengan indeks TheDatesebagai kunci utama, rencana eksekusi sangat efisien:

Rencana eksekusi

Anda dapat memilih untuk membungkus ini dalam suatu fungsi dan menjalankannya langsung terhadap pandangan yang disebutkan dalam pertanyaan, tetapi naluri saya menentangnya. Biasanya, kinerja lebih baik ketika Anda memilih baris dari tampilan ke tabel sementara, memberikan indeks yang sesuai pada tabel sementara, lalu menerapkan logika di atas. Detailnya tergantung pada detail tampilan, tetapi ini adalah pengalaman umum saya.

Untuk kelengkapan (dan diminta oleh jawaban ypercube) Saya harus menyebutkan bahwa solusi masuk saya yang lain untuk jenis masalah ini (sampai T-SQL mendapatkan fungsi set yang tepat) adalah kursor SQLCLR ( lihat jawaban saya di sini untuk contoh teknik ini ). Ini melakukan jauh lebih baik daripada kursor T-SQL, dan nyaman bagi mereka yang memiliki keterampilan dalam bahasa .NET dan kemampuan untuk menjalankan SQLCLR di lingkungan produksi mereka. Ini mungkin tidak menawarkan banyak dalam skenario ini daripada solusi rekursif karena mayoritas biayanya adalah semacam itu, tetapi perlu disebutkan.

Paul White Reinstate Monica
sumber
9

Karena ini adalah pertanyaan SQL Server 2014 saya mungkin juga menambahkan versi prosedur tersimpan yang disimpan secara asli dari "kursor".

Tabel sumber dengan beberapa data:

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

Jenis tabel yang merupakan parameter untuk prosedur tersimpan. Sesuaikan dengan bucket_counttepat .

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

Dan prosedur tersimpan yang loop melalui tabel bernilai parameter dan mengumpulkan baris @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

Kode untuk mengisi variabel tabel dioptimalkan memori yang digunakan sebagai parameter untuk prosedur tersimpan yang disusun secara asli dan memanggil prosedur.

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;

Hasil:

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

Memperbarui:

Jika karena alasan tertentu Anda tidak perlu mengunjungi setiap baris dalam tabel, Anda dapat melakukan yang setara dengan versi "lompat ke tanggal berikutnya" yang diterapkan dalam CTE rekursif oleh Paul White.

Tipe data tidak perlu kolom ID dan Anda tidak harus menggunakan indeks hash.

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

Dan prosedur tersimpan menggunakan a select top(1) ..untuk menemukan nilai selanjutnya.

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
sumber
Solusi Anda menggunakan DATEADD dan DATEIFF dapat memberikan hasil yang berbeda tergantung pada dataset awal.
Pavel Nefyodov
@ PavelNefyodov Saya tidak melihat itu. Bisakah Anda menjelaskan atau memberi contoh?
Mikael Eriksson
Bisakah Anda memeriksanya pada tanggal seperti ini ('2014-01-01 00: 00: 00.000'), ('2014-04-01 01: 00: 00.000'), tolong? Info lebih lanjut dapat ditemukan di jawaban saya.
Pavel Nefyodov
@ PavelNefyodov Ah, begitu. Jadi jika saya mengubah yang kedua T.TheDate >= dateadd(day, 91, @CurDate)menjadi baik-baik saja, bukan?
Mikael Eriksson
Atau jika tepat untuk OP, mengubah datatype dari TheDatedalam TTypeuntuk Date.
Mikael Eriksson
5

Solusi yang menggunakan kursor.
(pertama, beberapa tabel dan variabel yang diperlukan) :

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

Kursor aktual:

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

Dan mendapatkan hasilnya:

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

Diuji di SQLFiddle

ypercubeᵀᴹ
sumber
Beri +1 pada solusi ini tetapi bukan karena ini adalah cara paling efisien dalam melakukan sesuatu.
Pavel Nefyodov
@PavelNefyodov maka kita harus menguji kinerja!
ypercubeᵀᴹ
Saya percaya pada Paul White tentang hal itu. Pengalaman saya dengan pengujian kinerja tidak begitu mengesankan. Sekali lagi ini tidak menghentikan saya untuk memilih jawaban Anda.
Pavel Nefyodov
Terima kasih, ypercube. Seperti yang diharapkan cepat pada jumlah baris yang terbatas. Pada 13000 baris, CTE dan ini dilakukan kurang lebih sama. Pada 130.000 baris ada perbedaan 600%. Pada 13 m itu melewati 15 menit pada alat uji saya. Saya juga harus menghapus kunci utama, yang mungkin mempengaruhi kinerja sedikit.
Independen
Thnx untuk pengujian. Anda juga dapat menguji dengan memodifikasi INSERT @cdhanya ketika @Qualify=1(dan dengan demikian tidak memasukkan baris 13M jika Anda tidak membutuhkan semuanya dalam output). Dan solusinya tergantung pada menemukan indeks TheDate. Jika tidak ada, itu tidak akan efisien.
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 

Hasil

masukkan deskripsi gambar di sini

Juga lihat Cara Menghitung Menjalankan Total di SQL Server

pembaruan: silakan lihat di bawah hasil pengujian kinerja.

Karena logika yang berbeda yang digunakan dalam menemukan "celah 90 hari" solusi ypercube dan saya jika dibiarkan utuh dapat mengembalikan hasil yang berbeda dengan solusi Paul White. Ini karena penggunaan DATEIFF dan DATEADD fungsi masing-masing.

Sebagai contoh:

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

mengembalikan '2014-04-01 00: 00: 00.000' yang berarti bahwa '2014-04-01 01: 00: 00.000' melebihi batas 90 hari

tapi

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

Mengembalikan '90' yang berarti bahwa itu masih dalam celah.

Pertimbangkan contoh pengecer. Dalam hal ini menjual produk yang mudah rusak yang telah terjual berdasarkan tanggal '2014-01-01' di '2014-01-01 23: 59: 59: 999' tidak masalah. Jadi nilai DATEDIFF (HARI, ...) dalam hal ini OK.

Contoh lain adalah pasien menunggu untuk dilihat. Untuk seseorang yang datang pada '2014-01-01 00: 00: 00: 000' dan pergi pada '2014-01-01 23: 59: 59: 999' itu adalah 0 (nol) hari jika DATEIFF digunakan meskipun menunggu sebenarnya hampir 24 jam. Sekali lagi pasien yang datang di '2014-01-01 23:59:59' dan berjalan pergi di '2014-01-02 00:00:01' menunggu satu hari jika DATEIFF digunakan.

Tapi saya ngelantur.

Saya meninggalkan solusi DATEIFF dan bahkan menguji kinerja mereka tetapi mereka harus benar-benar di liga mereka sendiri.

Juga dicatat bahwa untuk dataset besar tidak mungkin untuk menghindari nilai hari yang sama. Jadi jika kita mengatakan 13 Juta catatan yang mencakup 2 tahun data, maka kita akan memiliki lebih dari satu catatan selama beberapa hari. Catatan-catatan itu sedang disaring pada kesempatan paling awal dalam solusi DATEIFF saya dan ypercube. Semoga ypercube tidak keberatan dengan ini.

Solusi diuji pada tabel berikut

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

dengan dua indeks pengelompokan berbeda (tanggal saya dalam kasus ini):

CREATE CLUSTERED INDEX CI_mydate on vGetVisits(mydate) 
GO

Tabel diisi dengan cara berikut

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

Untuk kasus baris jutaan, INSERT diubah sedemikian rupa sehingga 0-20 menit entri ditambahkan secara acak.

Semua solusi dengan hati-hati terbungkus dalam kode berikut

SET NOCOUNT ON
GO

DECLARE @StartDate DATETIME

SET @StartDate = GETDATE()

--- Code goes here

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

Kode aktual diuji (tanpa urutan tertentu):

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

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

Solusi Paul White ( PW )

;WITH CTE AS
(
    SELECT TOP (1)
        T.[mydate]
    FROM dbo.vGetVisits AS T
    ORDER BY
        T.[mydate]

    UNION ALL

    SELECT
        SQ1.[mydate]
    FROM 
    (
        SELECT
            T.[mydate],
            rn = ROW_NUMBER() OVER (
                ORDER BY T.[mydate])
        FROM CTE
        JOIN dbo.vGetVisits AS T
            ON T.[mydate] > DATEADD(DAY, 90, CTE.[mydate])
    ) AS SQ1
    WHERE
        SQ1.rn = 1
)

SELECT 
    CTE.[mydate]
FROM CTE
OPTION (MAXRECURSION 0);

Solusi DATEADD saya ( 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 ;

Solusi DATEDIFF saya ( 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]

Saya menggunakan SQL Server 2012, jadi permintaan maaf kepada Mikael Eriksson, tetapi kodenya tidak akan diuji di sini. Saya masih mengharapkan solusinya dengan DATADIFF dan DATEADD untuk mengembalikan nilai yang berbeda pada beberapa dataset.

Dan hasil sebenarnya adalah: masukkan deskripsi gambar di sini

Pavel Nefyodov
sumber
Terima kasih Pavel. Saya tidak benar-benar mendapatkan hasil dari solusi Anda dalam waktu singkat. Saya memperkecil testdata saya menjadi 1000 baris sampai saya mendapatkan waktu eksekusi pada 25 detik. Ketika saya menambahkan grup berdasarkan tanggal dan mengonversi ke tanggal di pilih, saya mendapatkan hasil yang benar! Demi kepentingan, saya membiarkan kueri melanjutkan dengan tabel-testdata kecil (13k baris) dan mendapat lebih dari 12 menit, yang berarti kinerja lebih dari o (nx)! Jadi ini terlihat berguna untuk set yang pasti akan berukuran kecil.
Independen
Apa meja yang Anda gunakan dalam tes? Berapa banyak baris? Tidak yakin mengapa Anda harus menambahkan grup berdasarkan tanggal untuk mendapatkan hasil yang benar. Silakan mempublikasikan pendanaan Anda sebagai bagian dari pertanyaan Anda (diperbarui).
Pavel Nefyodov
Hai! Saya akan menambahkan itu besok. Grup oleh adalah untuk menggabungkan tanggal duplikat. Tetapi saya sedang terburu-buru (larut malam) dan mungkin itu sudah dilakukan dengan menambahkan orang yang insaf (tanggal, z). Kuantitas baris ada di komentar saya. Saya mencoba 1000 baris dengan solusi Anda. Juga mencoba 13.000 baris dengan eksekusi 12 menit. Pauls dan Ypercubes juga tergoda ke meja 130.000 dan 13 juta. Tabel itu adalah tabel sederhana dengan tanggal acak yang dibuat dari kemarin dan -2 tahun yang lalu. Indeks dikelompokkan pada bidang tanggal.
Independen
0

Ok, apakah saya melewatkan sesuatu atau mengapa Anda tidak akan melewatkan rekursi dan bergabung kembali dengan diri Anda sendiri? Jika tanggal adalah kunci utama, itu harus unik, dan dalam urutan kronologis jika Anda berencana menghitung offset ke baris berikutnya

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

Hasil

masukkan deskripsi gambar di sini

Kecuali saya benar-benar melewatkan sesuatu yang penting ....

Pedang
sumber
2
Anda mungkin ingin mengubahnya WHERE [TheDate] > [T1].[TheDate]untuk memperhitungkan ambang perbedaan 90 hari. Tapi tetap saja, output Anda bukan yang diinginkan.
ypercubeᵀᴹ
Penting: Kode Anda harus memiliki "90" di suatu tempat.
Pavel Nefyodov