2016-10-20 2 views
0

Ich habe Roboter mit Zertifikaten. Es gibt zwei Arten von Zertifikaten. Für jede Art von Zertifikat (identifiziert durch Certif_ID) benötige ich für jeden Roboter den neuesten zertifizierten Datumsbereich.SQL für Inseln und Lücken: Inseln können überlappen

Aktualisierung zur besseren Übersicht: Datumsspannen, die sich nicht überschneiden, aber zusammenhängend sind, werden als eine einzelne Zeitspanne behandelt. Sehen Sie sich die ersten beiden Datensätze in der Beispieltabelle an, die sich am Anfang des Codes befinden.

Datumsspannen können sich überlappen! Diese müssen wie eine einzelne Spanne behandelt werden. Hier habe ich ein Problem.

Führen Sie in SQL Server 2012 diesen Code aus, um zu sehen, was passiert.

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 

sollte das Endergebnis sein:

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 

Ich habe mit den Datumsvergleichen wurde das Hantieren. In diesem Stück von den cte, sieht die -1 wie es für eine eintägige taumelt in datums Spannweiten ermöglicht:

   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 

Ich fühle mich sicher, dass dies der Punkt ist, dass die Festsetzung muss. Ich habe versucht, das Datum im Vergleich zu >= zu ändern. (Dies erfordert, dass ich mich mit der maximalen Rekursion befasse.) Die Gruppierung ändert sich, ist aber nicht korrekt.

+2

Bitte zeigen Sie das erwartete Ergebnis. –

+0

Das Ausführen des Codes zeigt dies an. Allerdings werde ich am Morgen posten. – Smandoli

Antwort

2

Dies ist keine einfache Aufgabe. Ich hoffe, dass dies das Problem lösen wird.

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 
+0

@Smandoli, Ich habe den Code aktualisiert, um überlappende und zusammenhängende Datumsbereiche zu berücksichtigen. Ich habe es auch für einige Zeiträume getestet. Ich hoffe, es wird für dich funktionieren. –

1

Dies war ein Kopfkratzer, weil es nicht Ihre typischen Lücken-und-Inseln ist, so dämmerte es mir, die Lücken und Inseln vor der Datumsdimension zuerst zu erstellen.

Jetzt habe ich eine zusätzliche Insel als vielleicht Sie erwartet hatten. Aber egal, wie ich es betrachte, es scheint wahr zu sein.

Ich sollte auch beachten, dass ich eine TVF (Tabellenwert benutzerdefinierte Funktion) verwenden, um dynamische Datumsbereiche zu erstellen. Diese Logik könnte leicht in eine vorläufige cte portiert werden. Eine Tally-/Kalender-Tabelle würde auch zum Trick führen.

Die 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 

Returns

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 

Die if UDF benötigt

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<[email protected] 
) 
/* 
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) 
*/ 
+0

Diese Lösung löst das dargestellte Problem und ist lehrreich (+1). @Ahmed Saeed hat die akzeptierte Antwort, weil der Code portabler ist (keine UDF) und, noch wichtiger, weil er erfolgreich auf meinen Produktionsdaten läuft, die umfangreich sind. – Smandoli

+0

@Smandoli Ich habe ein sehr einfaches Barometer ... Besser gewinnt jedes Mal.Ich schätze jedoch die Rückmeldung. –

Verwandte Themen