SQL Server拆分重叠日期范围

时间:2018-04-03 13:27:46

标签: sql-server-2008-r2

我需要拆分重叠的日期范围。我有一个主表(我在本例中将其称为“就业”),我需要从此表中返回一个人的所有Begin-End日期范围。我也有多个子表(由Car和Food表示),我想在主表中给出的时间内返回子表中活动的值。这将涉及在子表项更改时拆分主表日期范围。

我不想返回不在主表中的日期的子表信息。

DECLARE @Employment TABLE
( Person_ID INT, Employment VARCHAR(50), Begin_Date DATE, End_Date DATE )

DECLARE @Car TABLE
( Person_ID INT, Car VARCHAR(50), Begin_Date DATE, End_Date DATE )

DECLARE @Food TABLE
( Person_ID INT, Food VARCHAR(50), Begin_Date DATE, End_Date DATE )


INSERT INTO @Employment ( [Person_ID], [Employment], [Begin_Date], [End_Date] )
VALUES  ( 123 , 'ACME' , '1986-01-01' , '1990-12-31' )
,       ( 123 , 'Office Corp' , '1995-05-15' , '1998-10-03' )
,       ( 123 , 'Job 3' , '1998-10-04' , '2999-12-31' )

INSERT INTO @Car ( [Person_ID] , [Car] , [Begin_Date] , [End_Date] )
VALUES  ( 123, 'Red Car', '1986-05-01', '1997-06-23' )
,       ( 123, 'Blue Car', '1997-07-03', '2999-12-31' )

INSERT INTO @Food ( [Person_ID], [Food], [Begin_Date], [End_Date] )
VALUES  ( 123, 'Eggs', '1997-01-01', '1997-03-09' )
,       ( 123, 'Donuts', '2001-02-23', '2001-02-25' )

对于上述数据,结果应为:

Person_ID    Employment      Food        Car        Begin_Date    End_Date
123          ACME                                   1986-01-01    1986-04-30
123          ACME                        Red Car    1986-05-01    1990-12-31
123          Office Corp                 Red Car    1995-05-15    1996-12-31
123          Office Corp     Eggs        Red Car    1997-01-01    1997-03-09
123          Office Corp                 Red Car    1997-03-10    1997-06-23
123          Office Corp                            1997-06-24    1997-07-02
123          Office Corp                 Blue Car   1997-07-03    1998-10-03
123          Job 3                       Blue Car   1998-10-04    2001-02-22
123          Job 3           Donuts      Blue Car   2001-02-23    2001-02-25
123          Job 3                       Blue Car   2001-02-26    2999-12-31

第一排是他在ACME工作的时间,在那里他没有汽车或奇怪的食物痴迷。在第二排,他购买了一辆汽车,仍然在ACME工作。在第三排,他将工作换成了Office Corp,但仍然拥有红色汽车。请注意,即使他有红色汽车,我们在失业期间也不会返回数据。我们只想知道就业表中有值的时候汽车和食品表中的内容。

我找到了一个使用LEAD / LAG功能来实现这一目标的SQL Server 2012解决方案,但我仍然坚持使用2008 R2。

2 个答案:

答案 0 :(得分:1)

要将2012年的解决方案从该博客更改为使用2008,您需要在以下内容中替换LEAD

with
ValidDates as …
,
ValidDateRanges1 as
(
select EmployeeNo, Date as ValidFrom, lead(Date,1) over (partition by EmployeeNo order by Date) ValidTo
from ValidDates
)

有很多方法可以做到这一点,但是一个例子是自相连到同一个表+ 1行(这实际上是一个领导者)。一种方法是通过添加另一个中间CTE(例如ValidDatesWithRowno)将rownumber放在上一个表上(因此很容易找到下一行)。然后对该表执行左外连接,其中EmployeeNo相同且rowno = rowno + 1,并使用该值替换潜在客户。如果你想要一个领先2,你会加入到rowno + 2等等。所以2008版本看起来像

with
ValidDates as …
,
ValidDatesWithRowno as  --This is the ValidDates + a RowNo for easy self joining below
(
select EmployeeNo, Date, ROW_NUMBER() OVER (ORDER BY EmployeeNo, Date) as RowNo from ValidDates
)
,
ValidDateRanges1 as
(
select VD.EmployeeNo, VD.Date as ValidFrom, VDLead1.Date as ValidTo
from ValidDatesWithRowno VD
    left outer join ValidDatesWithRowno VDLead1 on VDLead1.EmployeeNo = VD.EmployeeNo 
        and VDLead1.RowNo = VD.RowNo + 1
)

所描述的解决方案的其余部分看起来像是你想要的2008年。

答案 1 :(得分:0)

这是我想出的答案。它有效,但它不是很漂亮。

它是两波,首先拆分任何重叠的就业/汽车日期,然后再次运行相同的SQL添加食物日期并再次拆分任何重叠。

DECLARE @Employment TABLE
( Person_ID INT, Employment VARCHAR(50), Begin_Date DATE, End_Date DATE )

DECLARE @Car TABLE
( Person_ID INT, Car VARCHAR(50), Begin_Date DATE, End_Date DATE )

DECLARE @Food TABLE
( Person_ID INT, Food VARCHAR(50), Begin_Date DATE, End_Date DATE )


INSERT INTO @Employment ( [Person_ID], [Employment], [Begin_Date], [End_Date] )
VALUES  ( 123 , 'ACME' , '1986-01-01' , '1990-12-31' )
,       ( 123 , 'Office Corp' , '1995-05-15' , '1998-10-03' )
,       ( 123 , 'Job 3' , '1998-10-04' , '2999-12-31' )

INSERT INTO @Car ( [Person_ID] , [Car] , [Begin_Date] , [End_Date] )
VALUES  ( 123, 'Red Car', '1986-05-01', '1997-06-23' )
,       ( 123, 'Blue Car', '1997-07-03', '2999-12-31' )

INSERT INTO @Food ( [Person_ID], [Food], [Begin_Date], [End_Date] )
VALUES  ( 123, 'Eggs', '1997-01-01', '1997-03-09' )
,       ( 123, 'Donuts', '2001-02-23', '2001-02-25' )


DECLARE @Person_ID INT = 123;


--A table to hold date ranges that need to be merged together
DECLARE @DatesToMerge TABLE 
(
   ID INT,
   Person_ID INT,
   Date_Type VARCHAR(10),
   Begin_Date DATETIME,
   End_Date DATETIME
)

INSERT INTO @DatesToMerge
SELECT ROW_NUMBER() OVER(ORDER BY [Car])
     , Person_ID
     , 'Car'
     , Begin_Date
     , End_Date
FROM @Car
WHERE Person_ID = @Person_ID

INSERT INTO @DatesToMerge 
SELECT ROW_NUMBER() OVER(ORDER BY [Employment])
     , Person_ID
     , 'Employment'
     , Begin_Date
     , End_Date
FROM @Employment
WHERE Person_ID = @Person_ID;


--A table to hold the merged @Employment and Car records
DECLARE @EmploymentAndCar TABLE
(
    RowNumber INT,
    Person_ID INT,
    Begin_Date DATETIME,
    End_Date DATETIME
)

;
WITH CarCTE AS 
(--This CTE grabs just the Car rows so we can compare and split dates from them
    SELECT  ID,
            Person_ID,
            Date_Type,
            Begin_Date,
            End_Date
    FROM @DatesToMerge
    WHERE Date_Type = 'Car'
),

NewRowsCTE AS 
( --This CTE creates just new rows starting after the Car dates for each @Employment date range
    SELECT  a.ID,
            a.Person_ID,
            a.Date_Type,
            DATEADD(DAY,1,b.End_Date) AS Begin_Date,
            a.End_Date
    FROM @DatesToMerge a
    INNER JOIN CarCTE b
    ON a.Begin_Date <= b.Begin_Date
    AND a.End_Date > b.Begin_Date
    AND a.End_Date > b.End_Date -- This is needed because if both the Car and @Employment end on the same date, there is split row after 
),

UnionCTE AS 
( -- This CTE merges the new rows with the existing ones
    SELECT  ID,
            Person_ID,
            Date_Type,
            Begin_Date,
            End_Date
    FROM @DatesToMerge
    UNION ALL
    SELECT  ID,
            Person_ID,
            Date_Type,
            Begin_Date,
            End_Date
    FROM NewRowsCTE
),

FixEndDateCTE AS 
(
    SELECT  CONVERT (CHAR,c.ID)+CONVERT (CHAR,c.Begin_Date) AS FixID, 
            MIN(d.Begin_Date) AS Begin_Date
    FROM UnionCTE c
    LEFT OUTER JOIN CarCTE d
    ON c.Begin_Date < d.Begin_Date
    AND c.End_Date >= d.Begin_Date
    WHERE c.Date_Type <> 'Car'
    GROUP BY CONVERT (CHAR,c.ID)+CONVERT (CHAR,c.Begin_Date)
),

Finalize AS
(
    SELECT  ROW_NUMBER() OVER (ORDER BY e.Begin_Date) AS RowNumber,
            e.Person_ID,
            e.Begin_Date,
            CASE    WHEN f.Begin_Date IS NULL THEN e.End_Date
                    ELSE DATEADD (DAY,-1,f.Begin_Date)
                    END AS EndDate
    FROM UnionCTE e
    LEFT OUTER JOIN FixEndDateCTE f
    ON (CONVERT (CHAR,e.ID)+CONVERT (CHAR,e.Begin_Date)) = f.FixID 
)

INSERT INTO @EmploymentAndCar ( RowNumber, Person_ID, Begin_Date, End_Date )

SELECT F.RowNumber
     , F.Person_ID
     , F.Begin_Date
     , F.EndDate
FROM Finalize F
INNER JOIN @Employment Employment
ON F.Begin_Date BETWEEN Employment.Begin_Date AND Employment.End_Date AND Employment.Person_ID = @Person_ID
ORDER BY F.Begin_Date


--------------------------------------------------------------------------------------------------
--Now that the Employment and Car dates have been merged, empty the DatesToMerge table
DELETE FROM @DatesToMerge;

--Reload the DatesToMerge table with the newly-merged Employment and Car records, 
--and the Food records that still need to be merged
INSERT INTO @DatesToMerge
SELECT RowNumber
     , Person_ID
     , 'PtBCar'
     , Begin_Date
     , End_Date
FROM @EmploymentAndCar
WHERE Person_ID = @Person_ID

INSERT INTO @DatesToMerge 
SELECT ROW_NUMBER() OVER(ORDER BY [Food]) 
     , Person_ID
     , 'Food'
     , Begin_Date
     , End_Date
FROM @Food
WHERE Person_ID = @Person_ID


;
WITH CarCTE AS 
(--This CTE grabs just the Food rows so we can compare and split dates from them
    SELECT  ID,
            Person_ID,
            Date_Type,
            Begin_Date,
            End_Date
    FROM @DatesToMerge
    WHERE Date_Type = 'Food'
),

NewRowsCTE AS 
( --This CTE creates just new rows starting after the Food dates for each Employment date range
    SELECT  a.ID,
            a.Person_ID,
            a.Date_Type,
            DATEADD(DAY,1,b.End_Date) AS Begin_Date,
            a.End_Date
    FROM @DatesToMerge a
    INNER JOIN CarCTE b
    ON a.Begin_Date <= b.Begin_Date
    AND a.End_Date > b.Begin_Date
    AND a.End_Date > b.End_Date -- This is needed because if both the Food and Car/Employment end on the same date, there is split row after 
),

UnionCTE AS 
( -- This CTE merges the new rows with the existing ones
    SELECT  ID,
            Person_ID,
            Date_Type,
            Begin_Date,
            End_Date
    FROM @DatesToMerge
    UNION ALL
    SELECT  ID,
            Person_ID,
            Date_Type,
            Begin_Date,
            End_Date
    FROM NewRowsCTE
),

FixEndDateCTE AS 
(
    SELECT  CONVERT (CHAR,c.ID)+CONVERT (CHAR,c.Begin_Date) AS FixID, 
            MIN(d.Begin_Date) AS Begin_Date
    FROM UnionCTE c
    LEFT OUTER JOIN CarCTE d
    ON c.Begin_Date < d.Begin_Date
    AND c.End_Date >= d.Begin_Date
    WHERE c.Date_Type <> 'Food'
    GROUP BY CONVERT (CHAR,c.ID)+CONVERT (CHAR,c.Begin_Date)
),

Finalize AS
(
    SELECT  ROW_NUMBER() OVER (ORDER BY e.Begin_Date) AS RowNumber,
            e.Person_ID,
            e.Begin_Date,
            CASE    WHEN f.Begin_Date IS NULL THEN e.End_Date
                    ELSE DATEADD (DAY,-1,f.Begin_Date)
                    END AS EndDate
    FROM UnionCTE e
    LEFT OUTER JOIN FixEndDateCTE f
    ON (CONVERT (CHAR,e.ID)+CONVERT (CHAR,e.Begin_Date)) = f.FixID 
)

SELECT DISTINCT
       F.Person_ID
     , Employment
     , Car
     , Food
     , F.Begin_Date
     , F.EndDate
FROM Finalize F
INNER JOIN @Employment Employment
ON F.Begin_Date BETWEEN Employment.Begin_Date AND Employment.End_Date AND Employment.Person_ID = @Person_ID

LEFT JOIN @Car Car
ON Car.[Begin_Date] <= F.Begin_Date 
AND Car.[End_Date] >= F.[EndDate]
AND Car.Person_ID = @Person_ID

LEFT JOIN @Food Food
ON Food.[Begin_Date] <= F.[Begin_Date] 
AND Food.[End_Date] >= F.[EndDate]
AND Food.Person_ID = @Person_ID

ORDER BY F.Begin_Date

如果有人有更优雅的解决方案,我很乐意接受他们的回答。