วิธีหาช่องว่างซ้ำ ๆ เมื่อ 90 วันที่ผ่านไประหว่างแถว


17

นี่เป็นงานที่ไม่สำคัญใน C # โฮมเวิร์ลดของฉัน แต่ฉันยังไม่ได้ทำใน SQL และต้องการที่จะแก้ปัญหาการตั้งค่า (โดยไม่มีเคอร์เซอร์) ชุดผลลัพธ์ควรมาจากข้อความค้นหาเช่นนี้

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

มันควรจะทำงานอย่างไร

ฉันส่ง params ทั้งสามนี้ไปยัง UDF
UDF ใช้ params ภายในเพื่อดึงข้อมูลแถว <= 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
)

ฉันมีอีกแบบสอบถามที่ฉันกำหนดแยกต่างหากซึ่งใกล้เคียงกับสิ่งที่ฉันต้องการมากกว่า แต่ถูกบล็อกด้วยความจริงที่ฉันไม่สามารถคำนวณในคอลัมน์ที่มีหน้าต่าง ฉันยังลองหนึ่งคล้ายที่ให้ผลผลิตมากขึ้นหรือน้อยลงเพียงแค่มีความล่าช้า () มากกว่า 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;

ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
Paul White Reinstate Monica

คำตอบ:


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) ฉันควรพูดถึงวิธีแก้ปัญหาแบบ go-to อื่น ๆ ของฉัน (จนกระทั่ง T-SQL ได้รับฟังก์ชั่นการตั้งค่าที่เหมาะสม) เป็นเคอร์เซอร์ SQLCLR ( ดูคำตอบของฉันที่นี่สำหรับตัวอย่างของเทคนิค ) สิ่งนี้ทำงานได้ดีกว่าเคอร์เซอร์ T-SQL และสะดวกสำหรับผู้ที่มีทักษะในภาษา. NET และความสามารถในการรัน SQLCLR ในสภาพแวดล้อมการใช้งานจริง มันอาจไม่ได้เสนออะไรมากมายในสถานการณ์นี้เหนือโซลูชันแบบเรียกซ้ำเนื่องจากค่าใช้จ่ายส่วนใหญ่เป็นการเรียงลำดับ แต่มันก็คุ้มค่าที่จะกล่าวถึง


9

เนื่องจากนี่เป็นคำถามของ SQL Server 2014 ฉันก็อาจเพิ่ม "เคอร์เซอร์" ที่เก็บรวบรวมไว้ในโพรซีเดอร์แบบดั้งเดิม

ตารางที่มาพร้อมกับข้อมูลบางอย่าง:

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

และขั้นตอนการจัดเก็บที่ loops @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 แบบเรียกซ้ำโดย Paul White

ชนิดข้อมูลไม่จำเป็นต้องมีคอลัมน์ ID และคุณไม่ควรใช้ดัชนีแฮช

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 อาจให้ผลลัพธ์ที่แตกต่างกันขึ้นอยู่กับชุดข้อมูลเริ่มต้น
Pavel Nefyodov

@PavelNefyodov ฉันไม่เห็นว่า คุณสามารถอธิบายหรือยกตัวอย่างได้หรือไม่?
Mikael Eriksson

คุณช่วยตรวจสอบในวันที่แบบนี้ได้ไหม ('2014-01-01 00: 00: 00.000'), ('2014-04-01 01: 00: 00.000') ได้ไหม? ข้อมูลเพิ่มเติมสามารถพบได้ในคำตอบของฉัน
Pavel Nefyodov

@PavelNefyodov อ่าฉันเข้าใจแล้ว ดังนั้นถ้าฉันเปลี่ยนวินาทีเป็นT.TheDate >= dateadd(day, 91, @CurDate)ทั้งหมดจะไม่เป็นไร
Mikael Eriksson

หรือถ้าเหมาะสมที่จะ OP เปลี่ยนประเภทข้อมูลของTheDateในการTType Date
Mikael Eriksson

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 กับโซลูชันนี้ แต่ไม่ใช่เพราะเป็นวิธีที่มีประสิทธิภาพที่สุดในการทำสิ่งต่าง ๆ
Pavel Nefyodov

@PavelNefyodov แล้วเราควรทดสอบประสิทธิภาพ!
ypercubeᵀᴹ

ฉันเชื่อใน Paul White ประสบการณ์ของฉันกับการทดสอบประสิทธิภาพนั้นไม่น่าประทับใจ อีกครั้งนี้ไม่ได้หยุดฉันจากการโหวตคำตอบของคุณ
Pavel Nefyodov

ขอบคุณ ypercube ตามคาดอย่างรวดเร็วในจำนวนแถวที่ จำกัด ใน 13000 แถว CTE และสิ่งนี้ดำเนินการเหมือนกันมากหรือน้อย ใน 130.000 แถวมีความแตกต่าง 600% เมื่อวันที่ 13m ผ่านไป 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 และคำตอบของฉันหากการทิ้งไว้เหมือนเดิมอาจส่งคืนผลลัพธ์ที่แตกต่างไปยังโซลูชันของ Paul White นี่เป็นเพราะการใช้งานของ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: 59: 999' เป็นเรื่องปกติ ดังนั้นมูลค่า DATEDIFF (วัน, ... ) ในกรณีนี้ก็โอเค

อีกตัวอย่างหนึ่งคือผู้ป่วยที่รอการมองเห็น สำหรับคนที่มาที่ '2014-01-01 00: 00: 00: 000' และออกที่ '2014-01-01 23: 59: 59: 999: 999' มันเป็น 0 (ศูนย์) วันหากใช้ DATEDIFF แม้ว่า รอที่เกิดขึ้นจริงคือเกือบ 24 ชั่วโมง ผู้ป่วยอีกรายที่มาที่ '2014-01-01 23:59:59' และเดินไปที่ '2014-01-02 00:00:01' รอหนึ่งวันหากใช้ DATEDIFF

แต่ฉันเชือนแช

ฉันออกจากโซลูชั่น DATEDIFF และแม้กระทั่งการทดสอบประสิทธิภาพเหล่านั้น แต่พวกเขาควรจะอยู่ในลีกของตัวเอง

นอกจากนี้ยังมีข้อสังเกตว่าสำหรับชุดข้อมูลขนาดใหญ่เป็นไปไม่ได้ที่จะหลีกเลี่ยงค่าวันเดียวกัน ดังนั้นถ้าเราบอกว่ามี 13 ล้านบันทึกที่ครอบคลุมข้อมูล 2 ปีเราก็จะมีมากกว่าหนึ่งบันทึกในบางวัน บันทึกเหล่านั้นจะถูกกรองออกโดยเร็วที่สุดในโซลูชัน DATEDIFF ของฉันและ 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()))

ทดสอบรหัสจริง (ไม่เรียงตามลำดับ):

โซลูชัน DATEDIFF ของ Ypercube ( YPC, DATEDIFF )

DECLARE @cd TABLE
(   TheDate datetime PRIMARY KEY,
    Qualify INT NOT NULL
);

DECLARE
    @TheDate DATETIME,
    @Qualify     INT = 0,
    @PreviousCheckDate DATETIME = '1799-01-01 00:00:00' 


DECLARE c CURSOR
    LOCAL STATIC FORWARD_ONLY READ_ONLY
    FOR
SELECT 
   mydate
FROM 
 (SELECT
       RowNum = ROW_NUMBER() OVER(PARTITION BY cast(mydate as date) ORDER BY mydate)
       , mydate
   FROM 
       dbo.vGetVisits) Actions
WHERE
   RowNum = 1
ORDER BY 
  mydate;

OPEN c ;

FETCH NEXT FROM c INTO @TheDate ;

WHILE @@FETCH_STATUS = 0
BEGIN

    SET @Qualify = CASE WHEN DATEDIFF(day, @PreviousCheckDate, @Thedate) > 90 THEN 1 ELSE 0 END ;
    IF  @Qualify=1
    BEGIN
        INSERT @cd (TheDate, Qualify)
        SELECT @TheDate, @Qualify ;
        SET @PreviousCheckDate=@TheDate 
    END
    FETCH NEXT FROM c INTO @TheDate ;
END

CLOSE c;
DEALLOCATE c;


SELECT TheDate
    FROM @cd
    ORDER BY TheDate ;

โซลูชัน DATEADD ของ Ypercube ( YPC, DATEADD )

DECLARE @cd TABLE
(   TheDate datetime PRIMARY KEY,
    Qualify INT NOT NULL
);

DECLARE
    @TheDate DATETIME,
    @Next_Date DATETIME,
    @Interesting_Date DATETIME,
    @Qualify     INT = 0

DECLARE c CURSOR
    LOCAL STATIC FORWARD_ONLY READ_ONLY
    FOR
  SELECT 
  [mydate]
  FROM [test].[dbo].[vGetVisits]
  ORDER BY mydate
  ;

OPEN c ;

FETCH NEXT FROM c INTO @TheDate ;

SET @Interesting_Date=@TheDate

INSERT @cd (TheDate, Qualify)
SELECT @TheDate, @Qualify ;

WHILE @@FETCH_STATUS = 0
BEGIN

    IF @TheDate>DATEADD(DAY, 90, @Interesting_Date)
    BEGIN
        INSERT @cd (TheDate, Qualify)
        SELECT @TheDate, @Qualify ;
        SET @Interesting_Date=@TheDate;
    END

    FETCH NEXT FROM c INTO @TheDate;
END

CLOSE c;
DEALLOCATE c;


SELECT TheDate
    FROM @cd
    ORDER BY TheDate ;

ทางออกของ 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);

โซลูชัน 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 ดังนั้นขอโทษ Mikael Eriksson แต่รหัสของเขาจะไม่ถูกทดสอบที่นี่ ฉันยังคงคาดหวังว่าโซลูชันของเขากับ DATADIFF และ DATEADD จะคืนค่าที่แตกต่างกันในชุดข้อมูลบางชุด

และผลลัพธ์ที่แท้จริงคือ: ป้อนคำอธิบายรูปภาพที่นี่


ขอบคุณ Pavel ฉันไม่ได้รับผลการแก้ไขภายในเวลาที่กำหนด ฉันเล็ก testdata ลงไปที่ 1,000 แถวจนกระทั่งฉันมีเวลาดำเนินการ 25 วินาที เมื่อฉันเพิ่มกลุ่มตามวันที่และเปลี่ยนเป็นวันที่ในตัวเลือกฉันได้ผลลัพธ์ที่ถูกต้อง! เพียงเพื่อประโยชน์ของฉันฉันปล่อยให้แบบสอบถามดำเนินการกับตารางทดสอบขนาดเล็กของฉัน (13k แถว) และใช้เวลามากกว่า 12 นาทีซึ่งหมายถึงประสิทธิภาพที่มากกว่า o (nx)! ดังนั้นจึงมีประโยชน์สำหรับชุดที่แน่นอนจะมีขนาดเล็ก
อิสระ

ตารางที่คุณใช้ในการทดสอบคืออะไร มีกี่แถว ไม่แน่ใจว่าทำไมคุณต้องเพิ่มกลุ่มตามวันที่เพื่อให้ได้ผลลัพธ์ที่ถูกต้อง โปรดเผยแพร่เงินทุนของคุณเป็นส่วนหนึ่งของคำถามของคุณ (อัปเดต)
Pavel Nefyodov

Hi! ฉันจะเพิ่มในวันพรุ่งนี้ กลุ่มตามคือการรวมวันที่ซ้ำกัน แต่ฉันกำลังรีบ (ดึกดื่น) และบางทีมันก็ทำไปแล้วโดยการเพิ่มผู้เปลี่ยนใจ (date, z) ปริมาณของแถวอยู่ในความคิดเห็นของฉัน ฉันลอง 1,000 แถวกับโซลูชันของคุณ ลองอีกครั้งด้วยแถว 13.000 พร้อมการดำเนินการ 12 นาที Pauls และ Ypercubes ถูกล่อลวงไปยังตาราง 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" อยู่
Pavel Nefyodov
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.