【问题标题】:Counting intersecting time intervals in T-SQL计算 T-SQL 中的相交时间间隔
【发布时间】:2016-08-18 03:42:26
【问题描述】:

代码:

CREATE TABLE #Temp1 (CoachID INT, BusyST DATETIME, BusyET DATETIME)
CREATE TABLE #Temp2 (CoachID INT, AvailableST DATETIME, AvailableET DATETIME)

INSERT INTO #Temp1 (CoachID, BusyST, BusyET)
SELECT 1,'2016-08-17 09:12:00','2016-08-17 10:11:00'
UNION
SELECT 3,'2016-08-17 09:30:00','2016-08-17 10:00:00'
UNION
SELECT 4,'2016-08-17 12:07:00','2016-08-17 13:10:00'

INSERT INTO #Temp2 (CoachID, AvailableST, AvailableET)
SELECT 1,'2016-08-17 09:07:00','2016-08-17 11:09:00'
UNION
SELECT 2,'2016-08-17 09:11:00','2016-08-17 09:30:00'
UNION
SELECT 3,'2016-08-17 09:24:00','2016-08-17 13:08:00'
UNION
SELECT 1,'2016-08-17 11:34:00','2016-08-17 12:27:00'
UNION
SELECT 4,'2016-08-17 09:34:00','2016-08-17 13:00:00'
UNION
SELECT 5,'2016-08-17 09:10:00','2016-08-17 09:55:00'

--RESULT-SET QUERY GOES HERE

DROP TABLE #Temp1
DROP TABLE #Temp2

期望的输出:

CoachID CanCoachST                  CanCoachET                  NumOfCoaches
1       2016-08-17 09:12:00.000     2016-08-17 09:24:00.000     2 --(ID2 = 2,5)
1       2016-08-17 09:24:00.000     2016-08-17 09:30:00.000     3 --(ID2 = 2,3,5)
1       2016-08-17 09:30:00.000     2016-08-17 09:34:00.000     1 --(ID2 = 5)
1       2016-08-17 09:34:00.000     2016-08-17 09:55:00.000     2 --(ID2 = 4,5)
1       2016-08-17 09:55:00.000     2016-08-17 10:00:00.000     1 --(ID2 = 4)
1       2016-08-17 10:00:00.000     2016-08-17 10:11:00.000     2 --(ID2 = 3,4)
3       2016-08-17 09:30:00.000     2016-08-17 09:34:00.000     1 --(ID2 = 5)
3       2016-08-17 09:34:00.000     2016-08-17 09:55:00.000     2 --(ID2 = 4,5)
3       2016-08-17 09:55:00.000     2016-08-17 10:00:00.000     1 --(ID2 = 4)
4       2016-08-17 12:07:00.000     2016-08-17 12:27:00.000     2 --(ID2 = 1,3)
4       2016-08-17 12:27:00.000     2016-08-17 13:08:00.000     1 --(ID2 = 3)
4       2016-08-17 13:08:00.000     2016-08-17 13:10:00.000     0 --(No one is available)

目标: 将#Temp1 视为团队教练 (ID1) 及其会议时间表(ST1 = 会议开始时间和 ET1 = 会议结束时间)。 将#Temp2 视为团队教练 (ID2) 及其总可用时间(ST2 = 可用开始时间和 ET2 = 可用结束时间)的表。

现在,我们的目标是找到来自#Temp2 的所有可能的教练,他们可以在来自#Temp1 的教练开会期间担任教练。

例如,对于 ID1 = 1 的教练,他在 9:12 到 10:11 之间忙碌(如果该信息很重要,数据可以跨越多天),我们有 教练 ID2 = 2 和 5,可以在 9:12 到 9:24 之间执教 , 教练 ID2 = 2,3 和 5 可以在 9:24 和 9:30 之间进行教练 , 教练 ID2 = 5 可以在 9:30 到 9:34 之间执教 , 教练 ID2 = 4 和 5 可以在 9:34 和 9:55 之间进行教练 , 教练 ID2 = 4 可以在 9:55 到 10:00 之间进行教练 ,并且教练 ID2 = 3 和 4 可以在 10:00 和 10:11 之间进行教练(请注意 ID 3,虽然在 9:24 和 13:08 之间的 #Temp2 表中可用,但它不能在 ID1 = 1 之间进行教练9:24 和 10:00,因为 9:30 到 10:00 之间也很忙。

到目前为止我的努力:到目前为止,只处理打破#Temp1 的时间范围。仍然需要弄清楚 A)如何从输出中删除该非繁忙时间窗口 B)添加一个字段/将其映射到正确的 T1 的 CoachID。

;WITH ED
AS (SELECT BusyET, CoachID FROM #Temp1  
    UNION ALL   
    SELECT BusyST, CoachID FROM #Temp1
    )
,Brackets
AS (SELECT MIN(BusyST) AS BusyST
        ,(  SELECT MIN(BusyET)
            FROM ED e
            WHERE e.BusyET > MIN(BusyST)
            ) AS BusyET
    FROM #Temp1 T   
    UNION ALL   
    SELECT B.BusyET
        ,e.BusyET
    FROM Brackets B
    INNER JOIN ED E ON B.BusyET < E.BusyET
    WHERE NOT EXISTS (
            SELECT *
            FROM ED E2
            WHERE E2.BusyET > B.BusyET
                AND E2.BusyET < E.BusyET
            )
    )
SELECT *
FROM Brackets
ORDER BY BusyST;

我认为我需要参与比较两个 ID 不匹配的表之间的 ST/ET 日期。但我无法弄清楚如何实际获取会议时间窗口和唯一计数。

更新为更好的架构/数据集。另请注意,即使 CoachID 4 没有“预定”可用,他在最后几分钟仍被列为忙碌。并且可能会出现在此期间没有其他人可以工作的情况,在这种情况下,我们可以返回 0 cnt 记录(或者如果它真的很复杂,则不返回它)。

同样,目标是找到所有可用 CoachID 的计数和组合,以及它们的可用时间窗口,这些时间窗口可以指导繁忙表中列出的 CoachID。

更新了更多与示例数据匹配的示例描述。

【问题讨论】:

  • 您的示例数据令人困惑。您说只有 ID2 2、3、5 可以填写 ID1 的前半小时,但您的示例数据显示 ID2 1 在那段时间可用...与预期输出相比,其余示例数据同样令人困惑...
  • 我认为#Temp2 只是每个教练的工作时间/班次。
  • @ZLK,是的,TT1是教练的开会时间,TT2是教练可能的工作时间。所以当我们返回 ID1 = 1 的结果集时,我们不应该为 ID2 = 1 从 TT2 返回那个时间。
  • 您是否有想要安排某人的最小间隔/持续时间?例如30 分钟还是 15 分钟?
  • @JohnDaCosta,没有最小/最大/设置的持续时间。 :(

标签: sql sql-server tsql sql-server-2012


【解决方案1】:

此答案中的查询受到 Itzik Ben-Gan 的 Packing Intervals 的启发。


起初我不了解要求的全部复杂性,并假设Table1Table2 中的间隔不重叠。我假设同一位教练不可能同时忙碌和有空。

事实证明,我的假设是错误的,因此我要在下面留下的查询的第一个变体必须通过初步步骤进行扩展,即从存储在 Table2 中的间隔中减去存储在 Table1 中的所有间隔。

它使用了类似的想法。 “可用”间隔的每个开始都用 +1 EventType 标记,“可用”间隔的结束用 -1 EventType 标记。对于“忙碌”间隔,标记是相反的。 “忙碌”间隔以 -1 开始,以 +1 结束。这是在C1_Subtract 中完成的。

然后运行总计告诉我们“真正”可用的时间间隔在哪里 (C2_Subtract)。最后,CTE_Available 只留下“真正”可用的间隔。

样本数据

我添加了几行来说明如果没有可用的教练会发生什么。我还添加了CoachID=9,它不在查询的第一个变体的初始结果中。

CREATE TABLE #Temp1 (CoachID INT, BusyST DATETIME, BusyET DATETIME);
CREATE TABLE #Temp2 (CoachID INT, AvailableST DATETIME, AvailableET DATETIME);
-- Start time is inclusive
-- End time is exclusive

INSERT INTO #Temp1 (CoachID, BusyST, BusyET) VALUES
(1, '2016-08-17 09:12:00','2016-08-17 10:11:00'),
(3, '2016-08-17 09:30:00','2016-08-17 10:00:00'),
(4, '2016-08-17 12:07:00','2016-08-17 13:10:00'),

(6, '2016-08-17 15:00:00','2016-08-17 16:00:00'),
(9, '2016-08-17 15:00:00','2016-08-17 16:00:00');

INSERT INTO #Temp2 (CoachID, AvailableST, AvailableET) VALUES
(1,'2016-08-17 09:07:00','2016-08-17 11:09:00'),
(2,'2016-08-17 09:11:00','2016-08-17 09:30:00'),
(3,'2016-08-17 09:24:00','2016-08-17 13:08:00'),
(1,'2016-08-17 11:34:00','2016-08-17 12:27:00'),
(4,'2016-08-17 09:34:00','2016-08-17 13:00:00'),
(5,'2016-08-17 09:10:00','2016-08-17 09:55:00'),

(7,'2016-08-17 15:10:00','2016-08-17 15:20:00'),
(8,'2016-08-17 15:15:00','2016-08-17 15:25:00'),
(7,'2016-08-17 15:40:00','2016-08-17 15:55:00'),
(9,'2016-08-17 15:05:00','2016-08-17 15:07:00'),
(9,'2016-08-17 15:40:00','2016-08-17 16:55:00');

CTE_Available的中间结果

+---------+-------------------------+-------------------------+
| CoachID |       AvailableST       |       AvailableET       |
+---------+-------------------------+-------------------------+
|       1 | 2016-08-17 09:07:00.000 | 2016-08-17 09:12:00.000 |
|       1 | 2016-08-17 10:11:00.000 | 2016-08-17 11:09:00.000 |
|       1 | 2016-08-17 11:34:00.000 | 2016-08-17 12:27:00.000 |
|       2 | 2016-08-17 09:11:00.000 | 2016-08-17 09:30:00.000 |
|       3 | 2016-08-17 09:24:00.000 | 2016-08-17 09:30:00.000 |
|       3 | 2016-08-17 10:00:00.000 | 2016-08-17 13:08:00.000 |
|       4 | 2016-08-17 09:34:00.000 | 2016-08-17 12:07:00.000 |
|       5 | 2016-08-17 09:10:00.000 | 2016-08-17 09:55:00.000 |
|       7 | 2016-08-17 15:10:00.000 | 2016-08-17 15:20:00.000 |
|       7 | 2016-08-17 15:40:00.000 | 2016-08-17 15:55:00.000 |
|       8 | 2016-08-17 15:15:00.000 | 2016-08-17 15:25:00.000 |
|       9 | 2016-08-17 16:00:00.000 | 2016-08-17 16:55:00.000 |
+---------+-------------------------+-------------------------+

现在我们可以在查询的第一个变体中使用CTE_Available 的这些中间结果而不是#Temp2。请参阅查询的第一个变体下方的详细说明。

完整查询

WITH
C1_Subtract
AS
(
    SELECT
        CoachID
        ,AvailableST AS ts
        ,+1 AS EventType
    FROM #Temp2

    UNION ALL

    SELECT
        CoachID
        ,AvailableET AS ts
        ,-1 AS EventType
    FROM #Temp2

    UNION ALL

    SELECT
        CoachID
        ,BusyST AS ts
        ,-1 AS EventType
    FROM #Temp1

    UNION ALL

    SELECT
        CoachID
        ,BusyET AS ts
        ,+1 AS EventType
    FROM #Temp1
)
,C2_Subtract AS
(
    SELECT
        C1_Subtract.*
        ,SUM(EventType)
            OVER (
            PARTITION BY CoachID
            ORDER BY ts, EventType DESC
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
        AS cnt
        ,LEAD(ts) 
            OVER (
            PARTITION BY CoachID
            ORDER BY ts, EventType DESC)
        AS NextTS
    FROM C1_Subtract
)
,CTE_Available
AS
(
    SELECT
        C2_Subtract.CoachID
        ,C2_Subtract.ts AS AvailableST
        ,C2_Subtract.NextTS AS AvailableET
    FROM C2_Subtract
    WHERE cnt > 0
)
,CTE_Intervals
AS
(
    SELECT
        TBusy.CoachID AS BusyCoachID
        ,TBusy.BusyST
        ,TBusy.BusyET
        ,CA.CoachID AS AvailableCoachID
        ,CA.AvailableST
        ,CA.AvailableET
        -- max of start time
        ,CASE WHEN CA.AvailableST < TBusy.BusyST
        THEN TBusy.BusyST
        ELSE CA.AvailableST 
        END AS ST
        -- min of end time
        ,CASE WHEN CA.AvailableET > TBusy.BusyET
        THEN TBusy.BusyET
        ELSE CA.AvailableET
        END AS ET
    FROM
        #Temp1 AS TBusy
        CROSS APPLY
        (
            SELECT
                TAvailable.*
            FROM
                CTE_Available AS TAvailable
            WHERE
                -- the same coach can't be available and busy
                TAvailable.CoachID <> TBusy.CoachID
                -- intervals intersect
                AND TAvailable.AvailableST < TBusy.BusyET
                AND TAvailable.AvailableET > TBusy.BusyST
        ) AS CA
)
,C1 AS
(
    SELECT
        BusyCoachID
        ,AvailableCoachID
        ,ST AS ts
        ,+1 AS EventType
    FROM CTE_Intervals

    UNION ALL

    SELECT
        BusyCoachID
        ,AvailableCoachID
        ,ET AS ts
        ,-1 AS EventType
    FROM CTE_Intervals

    UNION ALL

    SELECT
        CoachID AS BusyCoachID
        ,CoachID AS AvailableCoachID
        ,BusyST AS ts
        ,+1 AS EventType
    FROM #Temp1

    UNION ALL

    SELECT
        CoachID AS BusyCoachID
        ,CoachID AS AvailableCoachID
        ,BusyET AS ts
        ,-1 AS EventType
    FROM #Temp1
)
,C2 AS
(
    SELECT
        C1.*
        ,SUM(EventType)
            OVER (
            PARTITION BY BusyCoachID
            ORDER BY ts, EventType DESC, AvailableCoachID
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
        - 1 AS cnt
        ,LEAD(ts) 
            OVER (
            PARTITION BY BusyCoachID 
            ORDER BY ts, EventType DESC, AvailableCoachID) 
        AS NextTS
    FROM C1
)
SELECT
    BusyCoachID AS CoachID
    ,ts AS CanCoachST
    ,NextTS AS CanCoachET
    ,cnt AS NumOfCoaches
FROM C2
WHERE ts <> NextTS
ORDER BY BusyCoachID, CanCoachST
;

最终结果

+---------+-------------------------+-------------------------+--------------+
| CoachID |       CanCoachST        |       CanCoachET        | NumOfCoaches |
+---------+-------------------------+-------------------------+--------------+
|       1 | 2016-08-17 09:12:00.000 | 2016-08-17 09:24:00.000 |            2 |
|       1 | 2016-08-17 09:24:00.000 | 2016-08-17 09:30:00.000 |            3 |
|       1 | 2016-08-17 09:30:00.000 | 2016-08-17 09:34:00.000 |            1 |
|       1 | 2016-08-17 09:34:00.000 | 2016-08-17 09:55:00.000 |            2 |
|       1 | 2016-08-17 09:55:00.000 | 2016-08-17 10:00:00.000 |            1 |
|       1 | 2016-08-17 10:00:00.000 | 2016-08-17 10:11:00.000 |            2 |
|       3 | 2016-08-17 09:30:00.000 | 2016-08-17 09:34:00.000 |            1 |
|       3 | 2016-08-17 09:34:00.000 | 2016-08-17 09:55:00.000 |            2 |
|       3 | 2016-08-17 09:55:00.000 | 2016-08-17 10:00:00.000 |            1 |
|       4 | 2016-08-17 12:07:00.000 | 2016-08-17 12:27:00.000 |            2 |
|       4 | 2016-08-17 12:27:00.000 | 2016-08-17 13:08:00.000 |            1 |
|       4 | 2016-08-17 13:08:00.000 | 2016-08-17 13:10:00.000 |            0 |
|       6 | 2016-08-17 15:00:00.000 | 2016-08-17 15:10:00.000 |            0 |
|       6 | 2016-08-17 15:10:00.000 | 2016-08-17 15:15:00.000 |            1 |
|       6 | 2016-08-17 15:15:00.000 | 2016-08-17 15:20:00.000 |            2 |
|       6 | 2016-08-17 15:20:00.000 | 2016-08-17 15:25:00.000 |            1 |
|       6 | 2016-08-17 15:25:00.000 | 2016-08-17 15:40:00.000 |            0 |
|       6 | 2016-08-17 15:40:00.000 | 2016-08-17 15:55:00.000 |            1 |
|       6 | 2016-08-17 15:55:00.000 | 2016-08-17 16:00:00.000 |            0 |
|       9 | 2016-08-17 15:00:00.000 | 2016-08-17 15:10:00.000 |            0 |
|       9 | 2016-08-17 15:10:00.000 | 2016-08-17 15:15:00.000 |            1 |
|       9 | 2016-08-17 15:15:00.000 | 2016-08-17 15:20:00.000 |            2 |
|       9 | 2016-08-17 15:20:00.000 | 2016-08-17 15:25:00.000 |            1 |
|       9 | 2016-08-17 15:25:00.000 | 2016-08-17 15:40:00.000 |            0 |
|       9 | 2016-08-17 15:40:00.000 | 2016-08-17 15:55:00.000 |            1 |
|       9 | 2016-08-17 15:55:00.000 | 2016-08-17 16:00:00.000 |            0 |
+---------+-------------------------+-------------------------+--------------+

我建议创建以下索引以避免执行计划中的某些排序。

CREATE UNIQUE NONCLUSTERED INDEX [IX_CoachID_BusyST] ON #Temp1
(
    CoachID ASC,
    BusyST ASC
);

CREATE UNIQUE NONCLUSTERED INDEX [IX_CoachID_BusyET] ON #Temp1
(
    CoachID ASC,
    BusyET ASC
);

CREATE UNIQUE NONCLUSTERED INDEX [IX_CoachID_AvailableST] ON #Temp2
(
    CoachID ASC,
    AvailableST ASC
);

CREATE UNIQUE NONCLUSTERED INDEX [IX_CoachID_AvailableET] ON #Temp2
(
    CoachID ASC,
    AvailableET ASC
);

但是,在真实数据上,瓶颈可能在其他地方,这可能取决于数据分布。查询相当复杂,在没有真实数据的情况下对其进行调整将是太多的猜测。


查询的第一个变体

逐步运行查询,逐个 CTE 并检查中间结果以了解其工作原理。

CTE_Intervals 为我们提供了与繁忙间隔相交的可用间隔列表。 C1 将开始时间和结束时间与对应的EventType 放在同一列中。这将帮助我们跟踪间隔何时开始或结束。 EventType 的运行总数给出了可用教练的数量。 C1 将忙碌的教练联合起来,以便在没有教练可用时正确计算病例数。

WITH
CTE_Intervals
AS
(
    SELECT
        TBusy.CoachID AS BusyCoachID
        ,TBusy.BusyST
        ,TBusy.BusyET
        ,CA.CoachID AS AvailableCoachID
        ,CA.AvailableST
        ,CA.AvailableET
        -- max of start time
        ,CASE WHEN CA.AvailableST < TBusy.BusyST
        THEN TBusy.BusyST
        ELSE CA.AvailableST 
        END AS ST
        -- min of end time
        ,CASE WHEN CA.AvailableET > TBusy.BusyET
        THEN TBusy.BusyET
        ELSE CA.AvailableET
        END AS ET
    FROM
        #Temp1 AS TBusy
        CROSS APPLY
        (
            SELECT
                TAvailable.*
            FROM
                #Temp2 AS TAvailable
            WHERE
                -- the same coach can't be available and busy
                TAvailable.CoachID <> TBusy.CoachID
                -- intervals intersect
                AND TAvailable.AvailableST < TBusy.BusyET
                AND TAvailable.AvailableET > TBusy.BusyST
        ) AS CA
)
,C1 AS
(
    SELECT
        BusyCoachID
        ,AvailableCoachID
        ,ST AS ts
        ,+1 AS EventType
    FROM CTE_Intervals

    UNION ALL

    SELECT
        BusyCoachID
        ,AvailableCoachID
        ,ET AS ts
        ,-1 AS EventType
    FROM CTE_Intervals

    UNION ALL

    SELECT
        CoachID AS BusyCoachID
        ,CoachID AS AvailableCoachID
        ,BusyST AS ts
        ,+1 AS EventType
    FROM #Temp1

    UNION ALL

    SELECT
        CoachID AS BusyCoachID
        ,CoachID AS AvailableCoachID
        ,BusyET AS ts
        ,-1 AS EventType
    FROM #Temp1
)
,C2 AS
(
    SELECT
        C1.*
        ,SUM(EventType)
            OVER (
            PARTITION BY BusyCoachID
            ORDER BY ts, EventType DESC, AvailableCoachID
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
        - 1 AS cnt
        ,LEAD(ts) 
            OVER (
            PARTITION BY BusyCoachID 
            ORDER BY ts, EventType DESC, AvailableCoachID) 
        AS NextTS
    FROM C1
)
SELECT
    BusyCoachID AS CoachID
    ,ts AS CanCoachST
    ,NextTS AS CanCoachET
    ,cnt AS NumOfCoaches
FROM C2
WHERE ts <> NextTS
ORDER BY BusyCoachID, CanCoachST
;

DROP TABLE #Temp1;
DROP TABLE #Temp2;

结果

我为每条线路添加了 cmets,其中包含已计算的可用教练的 ID。

现在我明白为什么我的初始结果与您的预期结果不同了。

+---------+---------------------+---------------------+--------------+
| CoachID |       CanCoachST    |       CanCoachET    | NumOfCoaches |
+---------+---------------------+---------------------+--------------+
|       1 | 2016-08-17 09:12:00 | 2016-08-17 09:24:00 |            2 |  2,5
|       1 | 2016-08-17 09:24:00 | 2016-08-17 09:30:00 |            3 |  2,3,5
|       1 | 2016-08-17 09:30:00 | 2016-08-17 09:34:00 |            2 |  3,5
|       1 | 2016-08-17 09:34:00 | 2016-08-17 09:55:00 |            3 |  3,4,5
|       1 | 2016-08-17 09:55:00 | 2016-08-17 10:11:00 |            2 |  3,4
|       3 | 2016-08-17 09:30:00 | 2016-08-17 09:34:00 |            2 |  1,5
|       3 | 2016-08-17 09:34:00 | 2016-08-17 09:55:00 |            3 |  1,4,5
|       3 | 2016-08-17 09:55:00 | 2016-08-17 10:00:00 |            2 |  1,4
|       4 | 2016-08-17 12:07:00 | 2016-08-17 12:27:00 |            2 |  3,1
|       4 | 2016-08-17 12:27:00 | 2016-08-17 13:08:00 |            1 |  3
|       4 | 2016-08-17 13:08:00 | 2016-08-17 13:10:00 |            0 |  none
|       6 | 2016-08-17 15:00:00 | 2016-08-17 15:10:00 |            0 |  none
|       6 | 2016-08-17 15:10:00 | 2016-08-17 15:15:00 |            1 |  7
|       6 | 2016-08-17 15:15:00 | 2016-08-17 15:20:00 |            2 |  7,8
|       6 | 2016-08-17 15:20:00 | 2016-08-17 15:25:00 |            1 |  8
|       6 | 2016-08-17 15:25:00 | 2016-08-17 15:40:00 |            0 |  none
|       6 | 2016-08-17 15:40:00 | 2016-08-17 15:55:00 |            1 |  7
|       6 | 2016-08-17 15:55:00 | 2016-08-17 16:00:00 |            0 |  none
+---------+---------------------+---------------------+--------------+

【讨论】:

  • 弗拉德,这几乎就是我想要的。只有一件事,#Temp2.​​CoachID 3 在 09:24 到 13:08 之间可用。但是因为他/她在 9:30 到 10:00 之间很忙 (#Temp1.CoachID = 3),所以它不应该是 CoachID 1 在 9:30 到 9:34 以及 9:34 到 9:55 之间的输出的一部分和 9:55 和 10:00。然而,它在 9:24 和 9:30 以及 10:00 和 10:00 之间可用。其他 ID 类似。否则,这真是太棒了。另外,感谢 Itzik 的博文。
  • @007,嗯。因此,您在两个表中有冲突的数据。 Table2 说有教练,但 Table1 反驳说它很忙。此外,根据您的评论,“忙碌”表具有更高优先级的隐含规则。你应该尝试纠正这个设计。这非常令人困惑。当这种冲突不存在时,尝试将您的数据转换为更简单的排列。我的回答假设这样的冲突不存在。这是一个重要的信息,应该包含在问题中。
  • @007,您真的要在查询中“即时”清理和解决同一教练的繁忙时间和可用时间之间的这些冲突吗?本质上,您需要从存储在 Table2 中的间隔中减去存储在 Table1 中的间隔。这应该是可能的,但每次都在运行中这样做可能效率很低。
  • @007,坦率地说,从“可用”间隔中减去“忙碌”间隔的查询本身就是一个好问题。无论如何,我在答案中扩展了查询以执行此初步步骤。现在最终结果与您在问题中的预期结果相同。
  • @VladmirBaranov,将#T2 视为每个教练桌的工作时间,将#T1 视为,无论工作时间是什么时间,她/他在这些时间都很忙(无论是否安排教练)。现在,我们需要找到,谁可以掩护那段时间忙碌的教练?您的 sln 按预期工作,非常感谢您更新它。至于效率低下,我将不得不看一下它,因为此查询将经常用于处理庞大的数据集。再次感谢您!
【解决方案2】:

此查询将进行计算:

SELECT TT1.ID1
 , case when TT2.ST2 < TT1.ST1 THEN TT1.ST1 ELSE TT2.ST2 END
 , case when TT2.ET2 > TT1.ET1 THEN TT1.ET1 ELSE TT2.ET2 END
 , COUNT(distinct TT2.id2) 
FROM #Temp1 TT1 INNER JOIN #Temp2 TT2
ON TT1.ET1 > TT2.ST2 AND TT1.ST1 < TT2.ET2 AND TT1.ID1 <> TT2.ID2
GROUP BY TT1.ID1
 , case when TT2.ST2 < TT1.ST1 THEN TT1.ST1 ELSE TT2.ST2 END
 , case when TT2.ET2 > TT1.ET1 THEN TT1.ET1 ELSE TT2.ET2 END

但是,结果将包括教练填补完整时间段的时段,例如,对于 1 号教练,将有 3 个时段:从 9:00 到 9:30,有 2 号替补教练,从 9 号开始: 30 至 10:00 替补教练 #4 以及 9:00 至 10:00 与替补教练 #3 和 #4 的时间段。这是整个结果:

ID1                                                         
----------- ----------------------- ----------------------- -----------
1           2016-08-17 09:00:00.000 2016-08-17 09:30:00.000 1
1           2016-08-17 09:00:00.000 2016-08-17 10:00:00.000 2
1           2016-08-17 09:30:00.000 2016-08-17 10:00:00.000 1
3           2016-08-17 09:30:00.000 2016-08-17 10:00:00.000 3
4           2016-08-17 12:00:00.000 2016-08-17 12:30:00.000 1
4           2016-08-17 12:00:00.000 2016-08-17 13:00:00.000 1

【讨论】:

  • 感谢@cha 您的意见。唯一的问题是,部分/完整计数会导致混淆,因此我们需要获得唯一的部分计数。
【解决方案3】:

据我所知,您正在寻找的是这样的:

;WITH CTE AS (
    SELECT ID1, ST1, DATEADD(MINUTE, 30, ST1) ET1
    FROM #Temp1
    UNION ALL
    SELECT C.ID1, C.ET1, DATEADD(MINUTE, 30, C.ET1)
    FROM CTE C
    JOIN #Temp1 T ON T.ID1 = C.ID1
    WHERE T.ET1 >= DATEADD(MINUTE, 30, C.ET1))
SELECT *
FROM CTE C
OUTER APPLY (
    SELECT COUNT(*) ID2Cnt
    FROM #Temp2 T
    WHERE ST2 <= C.ST1
    AND ET2 >= C.ET1
    AND ID2 <> C.ID1
    AND NOT EXISTS (
        SELECT 1
        FROM CTE
        WHERE ID1 = T.ID2
        AND ST1 <= C.ST1
        AND ET1 >= C.ET1)) T
ORDER BY ID1, ST1;

CTE 会将您的 #Temp1 教练分成半小时时段,然后我假设您想找到 #Temp2 中所有 ID 不同且轮班开始较早或在同一时间,之后或同时结束... 注意: 我假设这里的块只能是半小时。

编辑:没关系...我刚刚意识到您还想从结果集中排除#Temp1 中忙碌的人,所以我在申请中添加了一个不存在的子句...

【讨论】:

  • 很快就会检查,但问题是块可以是任意分钟数..:(
  • @007 如果不是半小时的块,你将如何决定会有什么样的块?他们总是会在半小时结束/开始吗? (例如,如果有人在 9 点 15 分到 9 点 40 分很忙,您是要找一个 15 分钟和一个 10 分钟的时段吗?还是只找一个 25 分钟的时段?)
  • 用更好的数据集更新了 OP。基本上,我们需要查看忙碌表 (#Temp2),对于所有 CoachID 及其忙碌时间,找到不忙碌(不在 #Temp1 中)和可以工作(在 #Temp2 中)的教练的时间窗口组合) 和计数。是的,只要可以计算额外的教练,时间就会进入每个时间窗口组合,反之亦然。这有帮助吗?
【解决方案4】:

我建议使用间隔/时隙表的概念。 另一种解释方式是考虑“时间维度表”

定义您的所有时间,然后以您关心的粒度参考时间间隔记录您的事实。因为您的时间以 7 分钟和 11 分钟结束,所以我选择了 1 分钟的间隔,但我推荐 15-30 分钟的间隔。

这样做可以很容易地加入/比较表。

考虑下面的设计/实现:

--维表

-- drop table #intervals
create table #intervals(intervalId  int identity(1,1) not null primary key clustered,intervalStartTime datetime unique)
declare @s datetime, @e datetime, @i int 
set @s = '2016-08-16'
set @e = '2016-08-18'
set @i = 1
while (@s <= @e )
begin 
 insert into #intervals(intervalStartTime) values(@s)
 set @s = dateadd(minute, @i, @s)
end 

--事实表:

-- drop table #Fact

创建表#Fact(intervalId int, coachid int, isBusy int default(0) , isAvailable int default(0))

--记录每个教练的时间

   insert into #Fact(coachid,intervalId)
select distinct c.coachid, i.intervalId  
from 
(
select distinct coachid from #temp1
union
select distinct coachid from #temp2
) c cross join #intervals i

-- record free / busy info
update f set isbusy = 1 
from #intervals i inner join #fact f on i.intervalId  = f.intervalId  
inner join #temp1 t on f.coachid = t.coachid and i.intervalStartTime  between t.BusyST and t.BusyET

    -- record free / busy info
update f set isAvailable = 1 
from #intervals i inner join #fact f on i.intervalId  = f.intervalId  
inner join #temp2 t on f.coachid = t.coachid and i.intervalStartTime  between t.AvailableST and t.AvailableET

-- 构建您的查询以查找常见时间等

select * from #intervals i inner join #Fact f on i.intervalId = f.intervalId

-- 显示可用教练数与免费教练数的示例结果

选择 i.intervalId, i.intervalStartTime, sum(isBusy) 作为coachesBusy, sum(isAvailable) 作为coachesAvailable 从#intervals i inner join #Fact f on i.intervalId = f.intervalId 按 i.intervalId、i.intervalStartTime 分组 有 sum(isBusy)

然后,您可以根据需要查找常见或唯一的间隔 ID。

如果您需要进一步说明,请告诉我。

【讨论】:

  • 我建议使用 15 分钟或 30 分钟的时间间隔,我预计不会有任何类型的辅导时间可能一次少于 15 分钟。根据您示例中的数据,我选择了 1 分钟粒度,但我认为这太详细了,除非您有特殊需要/关注
【解决方案5】:

我正在使用一个小数字表...日期不需要任何东西,只需数字即可。我在这里构建的东西比你在真实场景中使用的要小。

CREATE TABLE dbo.Numbers (Num INT PRIMARY KEY CLUSTERED);

WITH E1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))     AS t(N))
,E2 AS (SELECT N = 1 FROM E1 AS a, E1 AS b)
,E4 AS (SELECT N = 1 FROM E2 AS a, E2 AS b)
,cteTally AS (SELECT N = 0 UNION ALL
                SELECT N = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM     E4)
INSERT INTO dbo.Numbers (Num)
SELECT N FROM cteTally;

请注意下面的@startDate ...它人为地接近您正在处理的日期,并且在真正的生产场景中,您会将该日期更早地与您更大的数字表一起使用。

这是您的问题的解决方案,它适用于较旧的 SQL Server 版本(以及您标记的 2012):

DECLARE @startDate DATETIME = '20160817';
WITH cteBusy AS
(
SELECT  num.Num
    ,   busy.CoachID
FROM #Temp1 AS busy
JOIN dbo.Numbers AS num
    ON num.Num >= DATEDIFF(MINUTE, @startDate, busy.BusyST)
    AND num.Num < DATEDIFF(MINUTE, @startDate, busy.BusyET)
)
, cteAvailable AS
(
SELECT  num.Num
    ,   avail.CoachID
FROM #Temp2 AS avail
JOIN dbo.Numbers AS num
    ON num.Num >= DATEDIFF(MINUTE, @startDate, avail.AvailableST)
    AND num.Num < DATEDIFF(MINUTE, @startDate, avail.AvailableET)
LEFT JOIN cteBusy AS b
    ON b.Num = num.Num
    AND b.CoachID = avail.CoachID
WHERE b.Num IS NULL 
)
,   cteGrouping AS
(
SELECT  b.Num
    ,   b.CoachID
    ,   NumOfCoaches = COUNT(a.CoachID)
FROM cteBusy AS b
LEFT JOIN cteAvailable AS a
    ON a.Num = b.Num
GROUP BY b.Num, b.CoachID
)
,   cteFinal AS
(
SELECT  cte.Num
    ,   cte.CoachID
    ,   cte.NumOfCoaches
    ,   block = cte.Num - ROW_NUMBER() OVER(PARTITION BY cte.CoachID, cte.NumOfCoaches ORDER BY cte.Num)
FROM cteGrouping AS cte
)
SELECT  cte.CoachID
,   CanCoachST = DATEADD(MINUTE, MIN(cte.Num), @startDate)
,   CanCoachET = DATEADD(MINUTE, MAX(cte.Num) + 1, @startDate)
,   cte.NumOfCoaches
FROM cteFinal AS cte
GROUP BY cte.CoachId, cte.NumOfCoaches, cte.block
ORDER BY cte.CoachID, CanCoachST;

享受吧!

【讨论】:

  • 看起来像一个计数表类型的方法..有趣!感谢@btberry 的这个输入。干杯
  • 它完全符合您的要求,性能比您选择的正确答案要高得多。你的选择标准很奇怪。
【解决方案6】:

我相信以下查询会起作用,但我不能对性能做出任何承诺。

CREATE TABLE #Temp1 (CoachID INT, BusyST DATETIME, BusyET DATETIME)
CREATE TABLE #Temp2 (CoachID INT, AvailableST DATETIME, AvailableET DATETIME)

INSERT INTO #Temp1 (CoachID, BusyST, BusyET)
SELECT 1,'2016-08-17 09:12:00','2016-08-17 10:11:00'
UNION
SELECT 3,'2016-08-17 09:30:00','2016-08-17 10:00:00'
UNION
SELECT 4,'2016-08-17 12:07:00','2016-08-17 13:10:00'

INSERT INTO #Temp2 (CoachID, AvailableST, AvailableET)
SELECT 1,'2016-08-17 09:07:00','2016-08-17 11:09:00'
UNION
SELECT 2,'2016-08-17 09:11:00','2016-08-17 09:30:00'
UNION
SELECT 3,'2016-08-17 09:24:00','2016-08-17 13:08:00'
UNION
SELECT 1,'2016-08-17 11:34:00','2016-08-17 12:27:00'
UNION
SELECT 4,'2016-08-17 09:34:00','2016-08-17 13:00:00'
UNION
SELECT 5,'2016-08-17 09:10:00','2016-08-17 09:55:00'


;WITH WorkScheduleWithID    -- Select work schedule (#Temp2 – available times) and generate ID for each schedule entry. 
AS
(
    SELECT   ROW_NUMBER() OVER (ORDER BY [CoachID]) AS [ID]
            ,[WS].[CoachID]
            ,[WS].[AvailableST] AS [Start]
            ,[WS].[AvailableET] As [End]
    FROM    #Temp2 [WS]
), SchedulesIntersect -- Determine where work schedule and meeting schedule (busy times) intersect.
AS
(
    SELECT   [ID]
            ,[CoachID]
            ,[Start]
            ,[End]
            ,[IntersectTime]
            ,SUM([Availability]) OVER (PARTITION BY [ID] ORDER BY [IntersectTime]) AS GroupID
    FROM    (
                SELECT       [WS].[ID]
                            ,[WS].[CoachID]
                            ,[WS].[Start]
                            ,[WS].[End]
                            ,[MS1].[BusyST] AS [IntersectTime]
                            ,0 AS [Availability]
                FROM        WorkScheduleWithID [WS]
                INNER JOIN  #Temp1 [MS1] ON ([MS1].[CoachID] = [WS].[CoachID]) 
                                                        AND
                                                     ( ([MS1].[BusyST] > [WS].[Start]) AND ([MS1].[BusyST] < [WS].[End]) ) -- Meeting start contained with in work schedule
                UNION ALL
                SELECT       [WS].[ID]
                            ,[WS].[CoachID]
                            ,[WS].[Start]
                            ,[WS].[End]
                            ,[MS2].[BusyET] AS [IntersectTime]
                            ,1 AS [Availability]
                FROM        WorkScheduleWithID [WS]
                INNER JOIN  #Temp1 [MS2] ON ([MS2].[CoachID] = [WS].[CoachID]) 
                                                        AND
                                                     ( ([MS2].BusyET > [WS].[Start]) AND ([MS2].BusyET < [WS].[End]) )  -- Meeting end contained with in work schedule
            ) Intersects
),ActualAvailability -- Determine actual availability of each coach based on work schedule and busy time.
AS
(
    SELECT   [ID]
            ,[CoachID]
            ,(
                CASE
                    WHEN [GroupID] = 0 THEN [Start]
                    ELSE MIN([IntersectTime]) 
                END
             ) AS [Start]
            ,(
                CASE
                    WHEN ( ([GroupID] > 0) AND (MIN([IntersectTime]) = MAX([IntersectTime])) ) THEN [End]
                    ELSE MAX([IntersectTime]) 
                END
             ) AS [End]
    FROM    SchedulesIntersect
    GROUP BY [ID], [CoachID], [Start], [End], [GroupID]
    UNION ALL
    SELECT  [ID]
            ,[CoachID]
            ,[Start]
            ,[End]
    FROM    WorkScheduleWithID WS
    WHERE   WS.ID NOT IN (SELECT ID FROM SchedulesIntersect)
),TimeIntervals -- Determine time intervals for which each coach’s availability will be checked against.
AS
(
    SELECT DISTINCT *
    FROM    (
                SELECT       MS.CoachID
                            ,MS.BusyST
                            ,MS.BusyET
                            ,(
                                CASE
                                    WHEN AC.Start < MS.BusyST THEN  MS.BusyST 
                                    ELSE AC.Start
                                END
                             )  AS [TS]
                FROM        #Temp1 MS
                LEFT OUTER JOIN ActualAvailability AC ON (AC.CoachID <> MS.CoachID) 
                            AND 
                            (
                                ( (MS.[BusyST] <= AC.[Start]) AND (MS.[BusyET] >= AC.[End]) ) OR                                -- Meeting covers entire work schedule 
                                ( (MS.[BusyST] > AC.[Start]) AND (MS.[BusyET] < AC.[End]) ) OR                              -- Meeting is contained with in work schedule
                                ( (MS.[BusyST] < AC.[Start]) AND (MS.[BusyET] > AC.[Start])  AND ([MS].[BusyET] < AC.[End]) ) OR   -- Meeting ends within work schedule (partial overlap)
                                ( (MS.[BusyST] > AC.[Start]) AND (MS.[BusyST] < AC.[End])  AND ([MS].[BusyET] > AC.[End]) ) -- Meeting starts within work schedule (partial overlap)
                            ) 
                UNION ALL
                SELECT       MS.CoachID
                            ,MS.BusyST
                            ,MS.BusyET
                            ,(
                                CASE
                                    WHEN AC.[End] > MS.BusyET THEN  MS.BusyET 
                                    ELSE AC.[End]
                                END
                             )  AS [TS]
                FROM        #Temp1 MS
                LEFT OUTER JOIN ActualAvailability AC ON (AC.CoachID <> MS.CoachID) 
                            AND 
                            (
                                ( (MS.[BusyST] <= AC.[Start]) AND (MS.[BusyET] >= AC.[End]) ) OR                                -- Meeting covers entire work schedule 
                                ( (MS.[BusyST] > AC.[Start]) AND (MS.[BusyET] < AC.[End]) ) OR                              -- Meeting is contained with in work schedule
                                ( (MS.[BusyST] < AC.[Start]) AND (MS.[BusyET] > AC.[Start])  AND ([MS].[BusyET] < AC.[End]) ) OR   -- Meeting ends within work schedule (partial overlap)
                                ( (MS.[BusyST] > AC.[Start]) AND (MS.[BusyST] < AC.[End])  AND ([MS].[BusyET] > AC.[End]) ) -- Meeting starts within work schedule (partial overlap)
                            ) 
            ) Intervals
),AvailableCoachTimeSegments -- Determine each coach’s availability against each time interval being checked.
AS
(
    SELECT          ROW_NUMBER() OVER (PARTITION BY TI.CoachID ORDER BY TI.Start, AT.CoachID) AS RankAsc
                    ,ROW_NUMBER() OVER (PARTITION BY TI.CoachID ORDER BY TI.[End] DESC, AT.CoachID DESC) AS RankDesc
                    ,TI.CoachID
                    ,TI.BusyST
                    ,TI.BusyET
                    ,TI.Start
                    ,TI.[End]
                    ,AT.CoachID AS AvailableCoachID
                    ,AT.Start AS AvailableStart
                    ,AT.[End] AS AvailableEnd
                    ,(
                        CASE
                            WHEN (MIN(TI.[Start]) OVER (PARTITION BY TI.CoachID)) <> TI.BusyST THEN 1
                            ELSE 0
                        END
                     ) AS StartIncomplete
                    ,(
                        CASE
                            WHEN (MAX(TI.[End]) OVER (PARTITION BY TI.CoachID)) <> TI.BusyET THEN 1
                            ELSE 0
                        END
                     ) AS EndIncomplete
    FROM            (
                        SELECT   CoachID
                                ,BusyST
                                ,BusyET
                                ,TS AS [Start]
                                ,LEAD(TS, 1, TS) OVER (PARTITION BY CoachID ORDER BY TS) AS [End]
                        FROM    TimeIntervals
                    ) TI    
    LEFT OUTER JOIN  ActualAvailability AT ON 

                                (
                                    ( (AT.[Start] <= TI.[Start]) AND (AT.[End] >= TI.[End]) ) OR                                -- Coach availability covers entire time segment 
                                    ( (AT.[Start] > TI.[Start]) AND (AT.[End] < TI.[End]) ) OR                                  -- Coach availability is contained within the time segment 
                                    ( (AT.[Start] < TI.[Start]) AND (AT.[End] > TI.[Start])  AND (AT.[End] < TI.[End]) ) OR     -- Coach availability ends within the time segment (partial overlap)
                                    ( (AT.[Start] > TI.[Start]) AND (AT.[Start] < TI.[End])  AND (AT.[End] > TI.[End]) )        -- Coach availability starts within the time segment (partial overlap)
                                )  
)
-- Final result 
SELECT  CoachID
        ,BusyST
        ,BusyET
        ,Start AS CanCoachST
        ,[End] AS CanCoachET
        ,COUNT(AvailableCoachID) AS NumOfCoaches
         ,ISNULL(STUFF((
                    SELECT TOP 100 PERCENT ', ' + CAST(AvailableCoach.AvailableCoachID  AS VARCHAR(MAX))
                    FROM AvailableCoachTimeSegments  AvailableCoach
                    WHERE (AvailableCoach.CoachID = Results.CoachID AND AvailableCoach.Start = Results.Start AND  AvailableCoach.[End] = Results.[End]) 
                    ORDER BY AvailableCoach.AvailableCoachID
                    FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
                  ,1,2,''), '(No one is available)') AS AvailableCoaches
FROM    AvailableCoachTimeSegments Results
WHERE   [Start] <> [End]
GROUP   BY CoachID, BusyST, BusyET, Start, [End], StartIncomplete, EndIncomplete
UNION ALL -- Add any missing time segments at the start of the busy time or end of the busy time. 
SELECT  CoachID
        ,BusyST
        ,BusyET
        ,(
            CASE 
                WHEN StartIncomplete = 1 THEN  BusyST
                WHEN EndIncomplete = 1 THEN MAX([End])
                ELSE Start
            END
        ) AS CanCoachST
        ,(
            CASE 
                WHEN StartIncomplete = 1 THEN  Start
                WHEN EndIncomplete = 1 THEN BusyET
                ELSE [End]
            END
        ) AS CanCoachET
        ,0 AS NumOfCoaches
        ,'(No one is available)' AS AvailableCoaches
FROM    AvailableCoachTimeSegments Results
WHERE   [Start] <> [End] AND ( (StartIncomplete = 1 AND RankAsc = 1) OR (EndIncomplete = 1 AND RankDesc = 1) ) 
GROUP   BY CoachID, BusyST, BusyET, Start, [End], StartIncomplete, EndIncomplete
ORDER BY CoachID, CanCoachST


DROP TABLE #Temp1
DROP TABLE #Temp2

【讨论】:

  • 这也有效(也像格式化)。是的,性能是我真正需要研究的东西,因为这个查询会经常针对一个巨大的数据集运行。再次感谢这个 sln @EdmondQuinton。
【解决方案7】:

这是考虑到与可用教练重叠的忙碌教练的预期结果。

| CoachID | CanCoachST       | CanCoachET       | NumOfCoaches | CanCoach |
|---------|------------------|------------------|--------------|----------|
| 1       | 2016-08-17 09:12 | 2016-08-17 09:24 | 2            | 2, 5     |
| 1       | 2016-08-17 09:24 | 2016-08-17 09:30 | 3            | 2, 3, 5  |
| 1       | 2016-08-17 09:30 | 2016-08-17 09:34 | 1            | 5        |
| 1       | 2016-08-17 09:34 | 2016-08-17 09:55 | 2            | 4, 5     |
| 1       | 2016-08-17 09:55 | 2016-08-17 10:00 | 1            | 4        |
| 1       | 2016-08-17 10:00 | 2016-08-17 10:11 | 2            | 3, 4     |
| 3       | 2016-08-17 09:30 | 2016-08-17 09:34 | 1            | 5        |
| 3       | 2016-08-17 09:34 | 2016-08-17 09:55 | 2            | 4, 5     |
| 3       | 2016-08-17 09:55 | 2016-08-17 10:00 | 1            | 4        |
| 4       | 2016-08-17 12:07 | 2016-08-17 12:27 | 2            | 1, 3     |
| 4       | 2016-08-17 12:27 | 2016-08-17 13:08 | 1            | 3        |
| 4       | 2016-08-17 13:08 | 2016-08-17 13:10 | 0            | NULL     |

#Temp1 作为忙碌的教练:

| CoachID | BusyST           | BusyET           |
|---------|------------------|------------------|
| 1       | 2016-08-17 09:12 | 2016-08-17 10:11 |
| 3       | 2016-08-17 09:30 | 2016-08-17 10:00 |
| 4       | 2016-08-17 12:07 | 2016-08-17 13:10 |

#Temp2 作为可用教练:

| CoachID | AvailableST      | AvailableET      |
|---------|------------------|------------------|
| 1       | 2016-08-17 09:07 | 2016-08-17 11:09 |
| 1       | 2016-08-17 11:34 | 2016-08-17 12:27 |
| 2       | 2016-08-17 09:11 | 2016-08-17 09:30 |
| 3       | 2016-08-17 09:24 | 2016-08-17 13:08 |
| 4       | 2016-08-17 09:34 | 2016-08-17 13:00 |
| 5       | 2016-08-17 09:10 | 2016-08-17 09:55 |

下面的脚本有点长。

;
with
st 
(
    CoachID,
    CanCoachST
)
as
(
    select 
        bound.CoachID,
        s.BusyST 
    from 
        #Temp1 as s 
    cross apply
    (
        select
            b.CoachID,
            b.BusyST,
            b.BusyET
        from
            #Temp1 as b
        where 1 = 1
        and s.BusyST between b.BusyST and b.BusyET
    )
    as bound

    union all

    select 
        bound.CoachID,
        s.BusyET 
    from 
        #Temp1 as s 
    cross apply
    (
        select
            b.CoachID,
            b.BusyST,
            b.BusyET
        from
            #Temp1 as b
        where 1 = 1
        and s.BusyET between b.BusyST and b.BusyET
        and s.CoachID != b.CoachID
    )
    as bound

    union all

    select 
        bound.CoachID,
        s.AvailableST 
    from 
        #Temp2 as s 
    cross apply
    (
        select
            b.CoachID,
            b.BusyST,
            b.BusyET
        from
            #Temp1 as b
        where 1 = 1
        and s.AvailableST between b.BusyST and b.BusyET
    )
    as bound

    union all

    select 
        bound.CoachID,
        s.AvailableET 
    from 
        #Temp2 as s 
    cross apply
    (
        select
            b.CoachID,
            b.BusyST,
            b.BusyET
        from
            #Temp1 as b
        where 1 = 1
        and s.AvailableET between b.BusyST and b.BusyET
        and s.CoachID != b.CoachID
    )
    as bound
),
d as
(
    select distinct
        CoachID,
        CanCoachST
    from
        st
),
r as
(
    select
        row_number() over (order by CoachID, CanCoachST) as RowID,
        CoachID,
        CanCoachST
    from
        d
),
rng as
(
    select
        r1.RowID,
        r1.CoachID,
        r1.CanCoachST,
        case when r1.CoachID = r2.CoachID 
            then r2.CanCoachST else t.BusyET end as CanCoachET
    from
        r as r1
    left join
        r as r2
    on
        r1.RowID = r2.RowID - 1
    left join
        #Temp1 as t
    on
        t.CoachID = r1.CoachID
),
c as
(
    select
        rng.RowID,
        rng.CoachID,
        rng.CanCoachST,
        rng.CanCoachET,
        t2.CoachID as CanCoachID
    from
        rng
    cross join
        #Temp1 as t1
    cross join
        #Temp2 as t2
    where 1 = 1
    and t2.CoachID != rng.CoachID
    and t2.AvailableST <= rng.CanCoachST
    and t2.AvailableET >= rng.CanCoachET
),
b as
(
    select
        rng.RowID,
        rng.CoachID,
        rng.CanCoachST,
        rng.CanCoachET,
        t1.CoachID as BusyCoachID
    from
        rng
    cross join
        #Temp1 as t1
    where 1 = 1
    and t1.CoachID != rng.CoachID
    and t1.BusyST <= rng.CanCoachST
    and t1.BusyET >= rng.CanCoachET
),
e as
(
    select
        c.RowID,
        c.CoachID,
        c.CanCoachST,
        c.CanCoachET,
        c.CanCoachID
    from
        c

    except

    select
        b.RowID,
        b.CoachID,
        b.CanCoachST,
        b.CanCoachET,
        b.BusyCoachID
    from
        b
),
f as
(
    select
        rng.RowID,
        rng.CoachID,
        rng.CanCoachST,
        rng.CanCoachET,
        e.CanCoachID
    from
        rng
    left join   
        e
    on
        e.RowID = rng.RowID
)
select
    f.CoachID,
    f.CanCoachST,
    f.CanCoachET,
    count(f.CanCoachID) as NumOfCoaches,
    stuff
    (
        (
            select ', ' + cast(f1.CanCoachID as varchar(5))
                from f as f1 where f1.RowID = f.RowID
                for xml path('')
        ), 
        1, 2, ''
    )
    as CanCoach
from
    f
group by
    f.RowID,
    f.CoachID,
    f.CanCoachST,
    f.CanCoachET
order by
    1, 2

【讨论】:

  • 非常感谢您的意见。但与 Vlad 类似,我看到 CoachID 3 在 9:30 到 10:00 之间显示为可覆盖 CoachID 1,但它不应该(请参阅我对 Vlad 帖子的评论)。否则,非常接近。谢谢
猜你喜欢
  • 2020-04-11
  • 1970-01-01
  • 2022-10-05
  • 1970-01-01
  • 2011-07-02
  • 2014-02-18
  • 1970-01-01
  • 1970-01-01
  • 2012-08-14
相关资源
最近更新 更多