此答案中的查询受到 Itzik Ben-Gan 的 Packing Intervals 的启发。
起初我不了解要求的全部复杂性,并假设Table1 和Table2 中的间隔不重叠。我假设同一位教练不可能同时忙碌和有空。
事实证明,我的假设是错误的,因此我要在下面留下的查询的第一个变体必须通过初步步骤进行扩展,即从存储在 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
+---------+---------------------+---------------------+--------------+