SQL для островов и промежутков: острова могут перекрываться

У меня есть роботы с сертификатами. Есть два вида сертификатов. Для каждого типа сертификата (обозначенного Certif_ID) для каждого робота мне нужен самый последний сертифицированный интервал дат.

Обновление для ясности: интервалы дат, которые не перекрываются, но являются смежными, рассматриваются как один интервал. См. Первые две записи в таблице-образце вверху кода.

Интервалы дат могут перекрываться! Их следует рассматривать как один диапазон. Вот где у меня проблема.

в SQL Server 2012 запустите этот код как есть, чтобы узнать, что происходит.

BEGIN   -- #certif_span
    IF OBJECT_ID('TEMPDB..#certif_span') IS NOT NULL DROP TABLE #certif_span;

    CREATE TABLE #certif_span
        (     Robot_ID  CHAR(3)
            , Certif_ID SMALLINT
            , d_Start   SMALLDATETIME
            , d_End     SMALLDATETIME       );

  INSERT INTO #certif_span VALUES ('210', '1', '2000-01-01', '2001-02-02');
  INSERT INTO #certif_span VALUES ('210', '1', '2001-02-03', '2001-12-31');
  INSERT INTO #certif_span VALUES ('210', '1', '2000-01-01', '2000-12-31');
  INSERT INTO #certif_span VALUES ('880', '1', '2001-01-01', '2001-12-31');
  INSERT INTO #certif_span VALUES ('880', '1', '2002-02-02', '2003-02-01');
  INSERT INTO #certif_span VALUES ('880', '1', '2003-01-01', '2004-12-31'); -- *
  INSERT INTO #certif_span VALUES ('880', '7', '2010-05-05', '2011-05-04');
  INSERT INTO #certif_span VALUES ('880', '7', '2011-05-05', '2012-02-10');
  INSERT INTO #certif_span VALUES ('880', '7', '2013-03-03', '2013-04-04');
  INSERT INTO #certif_span VALUES ('880', '7', '2013-04-01', '2013-05-05'); -- *
    --   *  This line has dates that overlap with the line above
END

SELECT Robot_ID
     , Certif_ID
     , d_Start = FORMAT(d_Start, 'yyyy-MM-dd')
     , d_End = FORMAT(d_End, 'yyyy-MM-dd')
     , commentary = 'Here is the raw data'
FROM #certif_span AS cs
ORDER BY Robot_ID
     , Certif_ID
     , d_End

IF OBJECT_ID('TEMPDB..#prac_date_span') IS NOT NULL DROP TABLE #prac_date_span;

SELECT DISTINCT
    cs.Robot_ID
  , cs.Certif_ID
  , cs.d_Start
  , cs.d_End
INTO
    --DROP TABLE --SELECT * FROM
       #prac_date_span
FROM
    #certif_span AS cs
GROUP BY
    cs.Robot_ID
  , cs.Certif_ID
  , cs.d_Start
  , cs.d_End
ORDER BY    1, 2, 3;

BEGIN

    IF OBJECT_ID('TEMPDB..#prac_date_span_grp') IS NOT NULL 
               DROP TABLE #prac_date_span_grp;

    WITH cte as (
         SELECT
                a.Robot_ID, a.Certif_ID
              , a.d_Start, a.d_End
             FROM
                #prac_date_span a
             LEFT JOIN #prac_date_span b
                ON a.Robot_ID = b.Robot_ID
                   AND b.Certif_ID      = a.Certif_ID
                   AND a.d_Start - 1    = b.d_End
             WHERE
                b.Robot_ID IS NULL
         UNION ALL    -----------------------------
         SELECT
                a.Robot_ID, a.Certif_ID
              , a.d_Start, b.d_End
             FROM
                cte a
             JOIN
                #prac_date_span b
                ON a.Robot_ID = b.Robot_ID
                   AND b.Certif_ID      = a.Certif_ID
                   AND b.d_Start - 1    = a.d_End
    )
    SELECT
        Robot_ID
        , Certif_ID
        , d_Start
        , d_End = MAX(d_End)
    INTO
        --drop table --select * from
                        #prac_date_span_grp
    FROM cte
    GROUP BY Robot_ID, Certif_ID, d_Start
    ORDER BY Robot_ID, Certif_ID;
END

SELECT
       Robot_ID
     , Certif_ID
     , d_Start  = FORMAT(d_Start, 'yyyy-MM-dd')
     , d_End    = FORMAT(d_End, 'yyyy-MM-dd')
     , commentary = 'Here is the grouped data (flawed)'
FROM #prac_date_span_grp

SELECT
        Robot_ID
      , Certif_ID
      , d_Start = FORMAT(MAX(d_Start), 'yyyy-MM-dd')
      , d_End   = FORMAT(MAX(d_End), 'yyyy-MM-dd')
      , commentary = 'Final result: Start date ' + 
                CASE FORMAT(MAX(d_Start), 'yyyy-MM-dd') 
                    WHEN '2003-01-01' THEN 'should be 2002-02-02' 
                    WHEN '2013-04-01' THEN 'should be 2013-03-03' 
                                      ELSE 'good' END
FROM #prac_date_span_grp
GROUP BY Robot_ID, Certif_ID

Конечный результат должен быть:

 Robot_ID Certif_ID   d_Start     d_End
   210       1       2000-01-01  2001-12-31
   880       1       2002-02-02  2004-12-31
   880       7       2013-03-03  2013-05-05

Я возился со сравнением дат. В этом бите из cte -1 выглядит так, как будто он допускает однодневный сдвиг в датах:

               AND b.Certif_ID      = a.Certif_ID
               AND a.d_Start - 1    = b.d_End
                  ...
               AND b.Certif_ID      = a.Certif_ID
               AND b.d_Start - 1    = a.d_End

Я уверен, что это вопрос, который нужно исправить. Я пробовал изменить дату по сравнению с >=. (Это требует, чтобы я имел дело с максимальной рекурсией.) Группировка меняется, но неверна.


person Smandoli    schedule 20.10.2016    source источник
comment
Подскажите пожалуйста ожидаемый результат.   -  person Ahmed Saeed    schedule 21.10.2016
comment
Это покажет запуск кода. Однако я отправлю сообщение утром.   -  person Smandoli    schedule 21.10.2016


Ответы (2)


Это непростая задача. Надеюсь, это решит проблему.

Declare @certif_span TABLE(Robot_ID CHAR(3), Certif_ID SMALLINT, StartDate date, EndDate date);

  INSERT INTO @certif_span VALUES ('210', '1', '2000-01-01', '2001-02-02');
  INSERT INTO @certif_span VALUES ('210', '1', '2001-02-03', '2001-12-31');
  INSERT INTO @certif_span VALUES ('210', '1', '2000-01-01', '2000-12-31');
  INSERT INTO @certif_span VALUES ('880', '1', '2001-01-01', '2001-12-31');
  INSERT INTO @certif_span VALUES ('880', '1', '2002-02-02', '2003-02-01');
  INSERT INTO @certif_span VALUES ('880', '1', '2003-01-01', '2004-12-31'); -- *
  INSERT INTO @certif_span VALUES ('880', '7', '2010-05-05', '2011-05-04');
  INSERT INTO @certif_span VALUES ('880', '7', '2011-05-05', '2012-02-10');
  INSERT INTO @certif_span VALUES ('880', '7', '2013-03-03', '2013-04-04');
  INSERT INTO @certif_span VALUES ('880', '7', '2013-04-01', '2013-05-05'); -- *

;with Src as(
SELECT  ROW_NUMBER() Over(Partition by Robot_ID, Certif_ID order by StartDate, EndDate) as RN
                ,a.*
        FROM @certif_span as a
)

  , Islands as(
        SELECT RN, Robot_ID, Certif_ID, StartDate, EndDate, 0 as islandNo, EndDate AS MovingEnd
        FROM Src as a WHERE a.RN=1
        UNION ALL
        SELECT a.RN, a.Robot_ID, a.Certif_ID, a.StartDate, a.EndDate
             , b.islandNo + CASE WHEN DATEDIFF(d, a.StartDate, b.MovingEnd)>=-1 THEN 0 ELSE 1 END as IslandNO
             , CASE WHEN a.EndDate>b.MovingEnd THEN a.EndDate ELSE b.MovingEnd END as MovingEnd
        FROM Src as a 
        INNER JOIN Islands as b on a.Robot_ID=b.Robot_ID and a.Certif_ID=b.Certif_ID and a.RN=b.RN+1
    )   --  SELECT * FROM Islands order by Robot_ID, Certif_ID, IslandNo

  , LastIsland as(
        SELECT Robot_ID, Certif_ID, islandNo, MIN(StartDate) as startDate, MAX(EndDate) as EndDate
              ,ROW_NUMBER() over(partition by Robot_ID, Certif_ID order by IslandNO desc) as RN
        FROM Islands
        Group by Robot_ID, Certif_ID, islandNo
)
    SELECT Robot_ID, Certif_ID, startDate, EndDate 
    FROM   LastIsland
    where  RN=1
person Ahmed Saeed    schedule 21.10.2016
comment
@Smandoli, я обновил код, чтобы учесть перекрывающиеся и непрерывные интервалы дат. Я также тестировал его для некоторых диапазонов дат. Надеюсь, у вас это сработает .. - person Ahmed Saeed; 21.10.2016

Это было головной болью, потому что это не типичные для вас промежутки и острова, поэтому меня осенило сначала создать промежутки и островки в измерении даты.

Теперь у меня есть еще один остров, чем вы, возможно, ожидали. Но, как бы я ни смотрел на это, похоже, это правда.

Следует также отметить, что я использую TVF (определяемую пользователем функцию с табличным значением) для создания динамических диапазонов дат. Эту логику можно легко перенести в предварительный cte. Счетная / календарная таблица тоже подойдет.

SQL

;with cte0 as(
                Select A.*,GrpSeq=RetSeq-Row_Number() over (Order by RetSeq)
                From (
                        Select Distinct RetSeq,RetVal
                         From  [dbo].[udf-Range-Date]((Select min(d_Start) from #certif_span),(Select max(d_End) from #certif_span),'DD',1) A
                         Join  #certif_span B on  A.RetVal between B.d_Start and B.d_End
                     ) A
             )
    , cte1 as(
                Select d_Start  = min(A.RetVal)
                      ,d_End    = max(A.RetVal)
                 From  cte0 A
                 Group By GrpSeq
             )
Select Robot_ID  = min(Robot_ID)
      ,Certif_ID = min(Certif_ID)
      ,A.d_Start
      ,A.d_End
 from cte1 A
 Join #certif_span B on B.d_Start Between A.d_Start and A.d_End
 Group By A.d_Start,A.d_End

Возврат

Robot_ID    Certif_ID   d_Start         d_End
210         1           2000-01-01      2001-12-31
880         1           2002-02-02      2004-12-31
880         7           2010-05-05      2012-02-10  << Extra Mentioned
880         7           2013-03-03      2013-05-05

UDF при необходимости

CREATE FUNCTION [dbo].[udf-Range-Date] (@R1 datetime,@R2 datetime,@Part varchar(10),@Incr int)
Returns Table
Return (
    with cte0(M)   As (Select 1+Case @Part When 'YY' then DateDiff(YY,@R1,@R2)/@Incr When 'QQ' then DateDiff(QQ,@R1,@R2)/@Incr When 'MM' then DateDiff(MM,@R1,@R2)/@Incr When 'WK' then DateDiff(WK,@R1,@R2)/@Incr When 'DD' then DateDiff(DD,@R1,@R2)/@Incr When 'HH' then DateDiff(HH,@R1,@R2)/@Incr When 'MI' then DateDiff(MI,@R1,@R2)/@Incr When 'SS' then DateDiff(SS,@R1,@R2)/@Incr End),
         cte1(N)   As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
         cte2(N)   As (Select Top (Select M from cte0) Row_Number() over (Order By (Select NULL)) From cte1 a, cte1 b, cte1 c, cte1 d, cte1 e, cte1 f, cte1 g, cte1 h ),
         cte3(N,D) As (Select 0,@R1 Union All Select N,Case @Part When 'YY' then DateAdd(YY, N*@Incr, @R1) When 'QQ' then DateAdd(QQ, N*@Incr, @R1) When 'MM' then DateAdd(MM, N*@Incr, @R1) When 'WK' then DateAdd(WK, N*@Incr, @R1) When 'DD' then DateAdd(DD, N*@Incr, @R1) When 'HH' then DateAdd(HH, N*@Incr, @R1) When 'MI' then DateAdd(MI, N*@Incr, @R1) When 'SS' then DateAdd(SS, N*@Incr, @R1) End From cte2 )

    Select RetSeq = N+1
          ,RetVal = D 
     From  cte3,cte0 
     Where D<=@R2
)
/*
Max 100 million observations -- Date Parts YY QQ MM WK DD HH MI SS
Syntax:
Select * from [dbo].[udf-Range-Date]('2016-10-01','2020-10-01','YY',1) 
Select * from [dbo].[udf-Range-Date]('2016-01-01','2017-01-01','MM',1) 
*/
person John Cappelletti    schedule 21.10.2016
comment
Это решение решает проблему в том виде, в котором она представлена, и является образовательным (+1). У @Ahmed Saeed есть принятый ответ, потому что код более переносимый (без UDF) и, что более важно, потому, что он успешно работает с моими объемными производственными данными. - person Smandoli; 21.10.2016
comment
@Smandoli У меня очень простой барометр ... Лучше каждый раз побеждает. Однако я ценю отзывы. - person John Cappelletti; 21.10.2016