90 일이 지난 행 사이의 간격을 재귀 적으로 찾는 방법


17

이것은 C # 홈 월드에서 일종의 사소한 작업이지만 아직 SQL로 작성하지 않았으며 커서를 사용하지 않고 세트 기반으로 해결하는 것을 선호합니다. 결과 집합은 이와 같은 쿼리에서 가져와야합니다.

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

작동 원리

이 세 가지 매개 변수를 UDF로 보냅니다.
UDF는 내부적으로 params를 사용하여 뷰에서 관련 <= ​​90 일 이전 행을 페치합니다.
UDF는 'MyDate'를 순회하고 총계 계산에 포함되어야하는 경우 1을 리턴합니다.
그렇지 않으면 0을 반환합니다. 여기에서 "qualifying"이라고 이름이 지정됩니다.

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
)

필자가 필요로하는 것에 더 가깝지만 별도로 열을 계산할 수 없다는 사실로 차단 된 다른 쿼리를 별도로 정의했습니다. 나는 또한 datediff로 둘러싸인 MyDate를 통해 LAG ()를 사용하여 거의 동일한 출력을 제공하는 비슷한 것을 시도했습니다.

SELECT
    t.Mydate, t.VisitCode, t.Cost, t.SomeId, t.otherkey, t.MaxDiff, t.DateDiff
FROM 
(
    SELECT *,
        MaxDiff = LAST_VALUE(Diff.Diff)  OVER (
            ORDER BY Diff.Mydate ASC
                ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
    FROM 
    (
        SELECT *,
            Diff =  ISNULL(DATEDIFF(DAY, LAST_VALUE(r.Mydate) OVER (
                        ORDER BY r.Mydate ASC
                            ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING), 
                                r.Mydate),0),
            DateDiff =  ISNULL(LAST_VALUE(r.Mydate) OVER (
                        ORDER BY r.Mydate ASC
                            ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING), 
                                r.Mydate)
        FROM dbo.vGetVisits AS r
        WHERE r.VisitCode = 3 AND r.SomeId = @SomeID AND r.otherkey = @otherkey
    ) AS Diff
) AS t
WHERE t.VisitCode = 3 AND t.SomeId = @SomeId AND t.otherkey = @otherkey
    AND t.Diff <= 90
ORDER BY
    t.Mydate ASC;

의견은 긴 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .
폴 화이트 복원 모니카

답변:


22

질문을 읽을 때 필요한 기본 재귀 알고리즘은 다음과 같습니다.

  1. 세트에서 가장 빠른 날짜를 가진 행을 반환
  2. 해당 날짜를 "현재"로 설정
  3. 현재 날짜로부터 90 일 이후 가장 빠른 날짜가있는 행 찾기
  4. 더 이상 행을 찾을 수 없을 때까지 2 단계부터 반복하십시오.

재귀 공통 테이블 식으로 구현하기가 비교적 쉽습니다.

예를 들어, 다음 샘플 데이터 사용 (질문에 기초) :

DECLARE @T AS table (TheDate datetime PRIMARY KEY);

INSERT @T (TheDate)
VALUES
    ('2014-01-01 11:00'),
    ('2014-01-03 10:00'),
    ('2014-01-04 09:30'),
    ('2014-04-01 10:00'),
    ('2014-05-01 11:00'),
    ('2014-07-01 09:00'),
    ('2014-07-31 08:00');

재귀 코드는 다음과 같습니다.

WITH CTE AS
(
    -- Anchor:
    -- Start with the earliest date in the table
    SELECT TOP (1)
        T.TheDate
    FROM @T AS T
    ORDER BY
        T.TheDate

    UNION ALL

    -- Recursive part   
    SELECT
        SQ1.TheDate
    FROM 
    (
        -- Recursively find the earliest date that is 
        -- more than 90 days after the "current" date
        -- and set the new date as "current".
        -- ROW_NUMBER + rn = 1 is a trick to get
        -- TOP in the recursive part of the CTE
        SELECT
            T.TheDate,
            rn = ROW_NUMBER() OVER (
                ORDER BY T.TheDate)
        FROM CTE
        JOIN @T AS T
            ON T.TheDate > DATEADD(DAY, 90, CTE.TheDate)
    ) AS SQ1
    WHERE
        SQ1.rn = 1
)
SELECT 
    CTE.TheDate 
FROM CTE
OPTION (MAXRECURSION 0);

결과는 다음과 같습니다.

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

TheDate주요 키가 있는 인덱스를 사용하면 실행 계획이 매우 효율적입니다.

실행 계획

이것을 함수로 감싸서 질문에 언급 된 견해에 대해 직접 실행할 수는 있지만 내 본능은 반대입니다. 일반적으로 뷰에서 임시 테이블로 행을 선택하고 임시 테이블에 적절한 인덱스를 제공 한 다음 위의 논리를 적용하면 성능이 향상됩니다. 세부 사항은보기의 세부 사항에 따라 다르지만 이것이 일반적인 경험입니다.

완성도 (ypercube의 답변에 의해 프롬프트 됨)를 위해이 유형의 문제에 대한 다른 해결책은 (T-SQL이 적절한 순서의 세트 함수를 얻을 때까지) SQLCLR 커서라는 점을 언급해야합니다 ( 기술의 예는 여기 에서 내 대답을 참조하십시오) ). 이는 T-SQL 커서보다 훨씬 성능이 뛰어나며 .NET 언어에 능숙하고 프로덕션 환경에서 SQLCLR을 실행할 수있는 기능이있는 사용자에게 편리합니다. 이 시나리오에서는 대부분의 비용이 일종이기 때문에 재귀 솔루션에 대해서는별로 제공하지 않지만 언급 할 가치가 있습니다.


9

이 때문에 입니다 으로 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);

그리고 테이블 값 매개 변수를 반복하고에서 행을 수집하는 저장 프로 시저입니다 @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

최신 정보:

어떤 이유로 테이블의 모든 행을 방문 할 필요가없는 경우 Paul White가 재귀 CTE에 구현 한 "다음 날짜로 점프"버전과 동일한 작업을 수행 할 수 있습니다.

데이터 유형에는 ID 열이 필요하지 않으며 해시 인덱스를 사용하지 않아야합니다.

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

저장 프로시 저는 a 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 '12

이 날짜 ( '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

영업 이익에 해당하는 경우의 데이터 유형을 변경 TheDateTTypeDate.
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ᵀᴹ

나는 폴 화이트를 믿습니다. 성능 테스트에 대한 나의 경험은 그리 인상적이지 않습니다. 다시 말하지만 이것이 귀하의 답변을 투표하는 것을 막지 않습니다.
Pavel Nefyodov

ypercube 감사합니다. 제한된 양의 행에서 예상대로 빠릅니다. 13000 행에서 CTE와 이것은 거의 동일하게 수행되었습니다. 130.000 개의 행에서 600 %의 차이가있었습니다. 13m에서는 테스트 장비에서 15 분이지나갑니다. 또한 기본 키를 제거해야했는데 성능에 약간 영향을 줄 수 있습니다.
독립

테스트를위한 Thnx. INSERT @cd때만 수행하도록 수정하여 테스트 할 수도 있습니다 @Qualify=1(따라서 출력에 13M 행이 모두 필요하지 않은 경우 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의 솔루션에 다른 결과를 반환 할 수 있습니다. DATEDIFFDATEADD 함수를 각각 사용하기 때문 입니다.

예를 들면 다음과 같습니다.

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

'2014-04-01 01 : 00 : 00.000'이 90 일 간격을 초과 함을 의미하는 '2014-04-01 00 : 00 : 00.000'을 반환합니다.

그러나

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

여전히 갭 내에 있음을 의미하는 '90'을 리턴합니다.

소매 업체의 예를 고려하십시오. 이 경우 날짜 '2014-01-01'에서 '2014-01-01 23 : 59 : 59 : 999'까지 판매 된 부패하기 쉬운 제품을 판매하는 것은 좋습니다. 따라서이 경우 DATEDIFF (DAY, ...) 값은 정상입니다.

또 다른 예는 환자의 진료를 기다리는 것입니다. '2014-01-01 00 : 00 : 00 : 000'에 와서 '2014-01-01 23 : 59 : 59 : 999'에 떠난 사람의 경우 DATEDIFF를 사용하더라도 0 일입니다. 실제 대기 시간은 거의 24 시간이었습니다. DATEDIFF를 사용하는 경우 '2014-01-01 23:59:59'에 다시 와서 '2014-01-02 00:00:01'에 걸어가는 환자가 하루를 기다렸습니다.

그러나 나는 산만하다.

나는 DATEDIFF 솔루션을 떠났고 성능조차도 테스트했지만 실제로는 자체 리그에 있어야합니다.

또한 큰 데이터 세트의 경우 같은 날 값을 피할 수 없다는 점에 주목했습니다. 따라서 2 년간의 데이터에 걸쳐 13 백만 개의 레코드가 있다고 말하면 며칠 동안 두 개 이상의 레코드를 갖게됩니다. 이 기록은 저와 ypercube의 DATEDIFF 솔루션에서 가장 빠른 시간에 필터링됩니다. 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

수백만 행의 경우 0-20 분 항목이 무작위로 추가되도록 INSERT가 변경되었습니다.

모든 솔루션은 다음 코드에서 신중하게 마무리되었습니다.

SET NOCOUNT ON
GO

DECLARE @StartDate DATETIME

SET @StartDate = GETDATE()

--- Code goes here

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

테스트 된 실제 코드 (특별한 순서 없음) :

Ypercube의 DATEDIFF 솔루션 ( YPC, DATEDIFF )

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

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


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

OPEN c ;

FETCH NEXT FROM c INTO @TheDate ;

WHILE @@FETCH_STATUS = 0
BEGIN

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

CLOSE c;
DEALLOCATE c;


SELECT TheDate
    FROM @cd
    ORDER BY TheDate ;

Ypercube의 DATEADD 솔루션 ( YPC, DATEADD )

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

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

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

OPEN c ;

FETCH NEXT FROM c INTO @TheDate ;

SET @Interesting_Date=@TheDate

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

WHILE @@FETCH_STATUS = 0
BEGIN

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

    FETCH NEXT FROM c INTO @TheDate;
END

CLOSE c;
DEALLOCATE c;


SELECT TheDate
    FROM @cd
    ORDER BY TheDate ;

폴 화이트의 솔루션 ( PW )

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

    UNION ALL

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

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

내 DATEADD 솔루션 ( PN, DATEADD )

DECLARE @cd TABLE
(   TheDate datetime PRIMARY KEY
);

DECLARE @TheDate DATETIME

SET @TheDate=(SELECT MIN(mydate) as mydate FROM [dbo].[vGetVisits])

WHILE (@TheDate IS NOT NULL)
    BEGIN

        INSERT @cd (TheDate) SELECT @TheDate;

        SET @TheDate=(  
            SELECT MIN(mydate) as mydate 
            FROM [dbo].[vGetVisits]
            WHERE mydate>DATEADD(DAY, 90, @TheDate)
                    )
    END

SELECT TheDate
    FROM @cd
    ORDER BY TheDate ;

내 DATEDIFF 솔루션 ( PN, DATEDIFF )

DECLARE @MinDate DATETIME;
SET @MinDate=(SELECT MIN(mydate) FROM dbo.vGetVisits);
    ;WITH gaps AS
    (
       SELECT 
       t1.[date]
       , t1.[MaxDiff]
       , SUM(t1.[MaxDiff]) OVER (ORDER BY t1.[date]) AS [Running Total]
            FROM
            (
                SELECT 
                mydate AS [date]
                , DATEDIFF(day,LAG(mydate, 1, mydate) OVER (ORDER BY mydate) , mydate) AS [MaxDiff] 
                FROM 
                    (SELECT
                    RowNum = ROW_NUMBER() OVER(PARTITION BY cast(mydate as date) ORDER BY mydate)
                    , mydate
                    FROM dbo.vGetVisits
                    ) Actions
                WHERE RowNum = 1
            ) t1
    )

    SELECT [date]
    FROM gaps t2
    WHERE                         
         ( ([Running Total])%91 - ([Running Total]- [MaxDiff])%91 <0 )      
         OR
         ( [MaxDiff] > 90) 
         OR
         ([date]=@MinDate)    
    ORDER BY [date]

나는 SQL Server 2012를 사용하고 있기 때문에 Mikael Eriksson에게 사과하지만 그의 코드는 여기서 테스트되지 않습니다. DATADIFF 및 DATEADD를 사용하는 그의 솔루션이 일부 데이터 세트에서 다른 값을 반환 할 것으로 예상합니다.

실제 결과는 다음과 같습니다. 여기에 이미지 설명을 입력하십시오


파벨 감사합니다. 시간 내에 솔루션의 결과를 얻지 못했습니다. 25 초에 실행 시간이 될 때까지 테스트 데이터를 1000 행으로 줄였습니다. 날짜별로 그룹을 추가하고 선택 항목에서 날짜로 변환하면 올바른 결과를 얻었습니다! 그냥 작은 쿼리 데이터 테이블 (13k 행)로 쿼리를 진행하고 12 분이 넘었으므로 o (nx) 이상의 성능을 의미합니다! 따라서 세트가 작을수록 유용합니다.
독립

테스트에 사용한 표는 무엇입니까? 행이 몇 개입니까? 올바른 출력을 얻으려면 날짜별로 그룹을 추가 해야하는 이유를 모르겠습니다. 질문의 일부로 자금을 발행하십시오 (업데이트 됨).
Pavel Nefyodov

안녕하세요! 내일 추가하겠습니다. 그룹은 중복 날짜를 결합하는 것이 었습니다. 그러나 나는 서둘러 (심야) 있었고 아마도 이미 convert (date, z)를 추가하여 수행했을 것입니다. 행의 수량은 내 의견에 있습니다. 귀하의 솔루션으로 1000 행을 시도했습니다. 12 분 실행으로 13.000 행을 시도했습니다. Pauls와 Ypercubes도 130.000과 13millions 테이블에 유혹되었습니다. 이 테이블은 어제와 -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.