在不分解每个组合的情况下找到未覆盖的时期

Find uncovered periods without exploding each combination

我有以下两个tables

人物

+--------+---------------+-------------+
|  Name  | ContractStart | ContractEnd |
+--------+---------------+-------------+
| Kate   |      20180101 |    20181231 |
| Sawyer |      20180101 |    20181231 |
| Ben    |      20170601 |    20181231 |
+--------+---------------+-------------+

班次

+---------+--------+------------+----------+
| Station |  Name  | ShiftStart | ShiftEnd |
+---------+--------+------------+----------+
| Swan    | Kate   |   20180101 | 20180131 |
| Arrow   | Kate   |   20180301 | 20180331 |
| Arrow   | Kate   |   20180401 | 20181231 |
| Flame   | Sawyer |   20180101 | 20181231 |
| Swan    | Ben    |   20180101 | 20181231 |
+---------+--------+------------+----------+

这意味着,例如,Kate将在20180101至20181231期间有空。在此期间,她将在20180101至20180131期间在Swan车站工作,在20180301至20180331期间以及20180401至20181231期间在Arrow车站工作。

我的目标是达到以下table

+------+---------------+-------------+
|      | VacationStart | VacationEnd |
+------+---------------+-------------+
| Kate |      20180201 |    20180228 |
| Ben  |      20170601 |    20171231 |
+------+---------------+-------------+

这意味着 Kate 从 20180201 到 20180228 期间有空。

我的第一个想法是用 2017 年和 2018 年的每一天创建一个 table,比方说 CalTable,然后 JOIN table 和 People 来查找每个人都应该有空的每一天。此时再次 JOIN 结果 table 和 Shifts 以得到 NOT BETWEEN ShiftStart AND ShiftEnd 天的证据。 考虑到我有将近 1.000.000 的人,通常在 ContractStartContractEnd 之间,即 10-20 年,这个步骤给了我正确的结果但非常慢。

以更聪明、更快速的方式获得结果的正确方法是什么?

谢谢。 This is the data of the example on db<>Fiddle


对于@A_Name_Does_Not_Matter这是我的尝试

CREATE TABLE #CalTable([ID] VARCHAR(8) NOT NULL)

DECLARE @num int
SET @num = 20170101

WHILE (@num <= 20181231)
BEGIN
    INSERT INTO #CalTable([ID])
    SELECT @num AS [ID]

    SET @num = @num + 1
END

SELECT X.[Name], X.[TIMEID]
FROM (
    -- All day availables
    SELECT DISTINCT A.[Name],B.[ID] AS [TIMEID]
    FROM #People A INNER JOIN #CalTable B
    ON B.[ID] BETWEEN A.[ContractStart] AND A.[ContractEnd]
) X
LEFT JOIN (
    -- Working day
    SELECT DISTINCT A.[Name],B.[ID] AS [TIMEID]
    FROM #People A INNER JOIN #CalTable B
    ON B.[ID] BETWEEN A.[ContractStart] AND A.[ContractEnd]
    INNER JOIN #Shifts C ON A.[Name]=C.[Name] AND B.[ID] BETWEEN C.[ShiftStart] AND C.[ShiftEnd]
) Z
ON X.[Name]=Z.[Name] AND X.[TIMEID]=Z.[TIMEID]
WHERE Z.[Name] IS NULL
ORDER BY X.[Name],X.[TIMEID]

然后汇总 .

的日期

所以一个人的开始日期可能是假期的开始,您可以通过使用 CROSS APPLY 获得 TOP 1 班次找到他们的第一个轮班日期(减去 1 天)来找到假期的结束, 按日期排序

在没有轮班的特殊情况下,他们的假期在合同结束日期结束。

未来假期则在轮班后的一天开始,在下班次的前一天结束(可以通过 OUTER APPLY 找到),如果没有进一步的轮班,则默认为约定的结束日期

SELECT p.name, p.contractStart vacationstart, p.ContractEnd vacationend from people p WHERE not exists(select 1 from shifts s where p.name = s.name)
UNION
SELECT p2.name,
        p2.contractStart vacationstart, 
        dateadd(day,-1,DQ.ShiftStart) as vacationend 
            from PEOPLE P2
            CROSS APPLY 
                (SELECT TOP 1 s2.ShiftStart FROM shifts s2 WHERE p2.name = s2.name  order by sfiftstart) DQ
                WHERE DQ.ShiftStart > p2.contractstart

UNION
select P3.NAME,
       dateadd(day,1,s3.ShiftEnd) vacationstart,
       COALESCE(dateadd(day,-1,  DQ2.shiftStart),P3.ContractEnd)  --you might have to add handling yourself for  removing a case where they work on their contract end date
       FROM people p3 JOIN shifts s3 on p3.name = s3.name
       OUTER APPLY (SELECT TOP 1 s4.shiftStart 
                                    from shifts s4
                                        where s4.name = p3.name 
                                              and 
                                              s4.shiftstart > s3.shiftstart 
                                        order by s4.shiftstart) DQ2

没有测试数据我很难验证。 对于员工,我追求的是

合同开始,Shift1Start - 1

Shift1End + 1, Shift2Start - 1

Shift2End + 1, Shift3Start - 1

Shift3End + 1, ContractEnd

然后添加 'no shifts' 的大小写 finally shifts 可能是连续的,导致假期的持续时间为零或更短 - 您可以通过将查询设为子查询来过滤这些,然后简单地过滤