Подсчет пересекающихся временных интервалов в T-SQL
код:
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 (обратите внимание, как код 3, хотя и имеются в #Temp2 стол между 9:24 и 13:08, это не тренер для типа id1 = 1 между 9:24 и 10:00, потому что его же заняты, между 9:30 и 10:00.
мои усилия до сих пор: только дело с нарушением временной шкалы #Temp1 до сих пор. Еще нужно выяснить, а) как удалить это незанятое окно времени из вывода Б) добавьте поле/сопоставьте его с правыми Коачидами T1.
;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;
Я думаю, что мне нужно присоединиться к сопоставлению St / ET даты между двумя таблицами, где идентификаторы не совпадают друг с другом. Но мне трудно понять, как на самом деле получить только окно времени встречи и уникальный счетчик.
обновлено с лучшей схемой / набором данных. Также обратите внимание, что, хотя CoachID 4 не "запланирован", он по-прежнему указан как занят в течение последних нескольких минут. И может быть сценарий, когда никто другой не доступен для работы в течение этого времени, в этом случае мы можем вернуть 0 cnt record (или не вернуть его, если это очень сложно).
опять же, цель состоит в том, чтобы найти количество и комбинацию всех доступных CoachIDs и их доступное окно времени, которое может тренировать для CoachIDs, перечисленных в таблице занятости.
обновлено с более подробным описанием образца, соответствующим данным образца.
7 ответов
запрос в этом ответе был вдохновлен Упаковка Интервалов Ицик Бен-Ган.
сначала я не понимал всей сложности требований и предполагал, что интервалы в Table1
и Table2
не пересекаются. Я предположил, что один и тот же тренер может быть занят и доступен одновременно.
оказывается, что мое предположение было неправильным, поэтому первый вариант запроса, который я оставляю ниже, должен быть расширен с помощью предварительный шаг, который вычитает все интервалы, хранящиеся в Table1
из интервалов, хранящихся в Table2
.
он использует аналогичную идею. Каждый старт" доступного " интервала отмечен +1 EventType
и конец" доступного " интервала отмечен -1 EventType
. Для" занятых " интервалов отметки меняются местами. "Занят" интервал начинается с 1 и заканчивается +1. Это делается в C1_Subtract
.
затем запуск total сообщает нам, где "действительно" доступны интервалы (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
in первый вариант запроса. См. подробные объяснения ниже первого варианта запроса.
полный запрос
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-by-CTE и изучить промежуточные результаты для undestand, как это работает.
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;
результат
я добавил комментарии для каждой строки с идентификаторами доступных тренеров, которые были посчитаны.
теперь я понимаю, почему мой первоначальный результат не был таким же, как ваш ожидаемый результат.
+---------+---------------------+---------------------+--------------+
| 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
+---------+---------------------+---------------------+--------------+
этот запрос будет делать расчеты:
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
однако результат будет включать слоты, в которых тренеры заполняют полный рабочий день, e.g для тренера 1 будет три слота:с 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
насколько я могу судить, вы ищете что-то вроде этого:
;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, которые не являются тем же идентификатором и имеют смену, которая начинается раньше или в то же время и заканчивается после или в то же время... Примечание: Я предполагаю, что блоки могут быть только полчаса здесь.
EDIT: неважно... Я только что понял, что ты тоже этого хочешь. скидка занятых людей в #Temp1 из результирующего набора, поэтому я добавил предложение not exists в apply...
Я рекомендую использовать концепцию таблицы интервалов / временных интервалов. Другой способ объяснить это-рассмотреть "таблицу измерения времени"
определите все свое время, а затем запишите свои факты со ссылками на временные интервалы в той степени детализации, о которой вы заботитесь. Поскольку у вас было время, заканчивающееся на 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
создать таблицу #факт(intervalId инт coachid инт, инт isBusy по умолчанию(0) , доступные по умолчанию тип int(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
-- пример результата, показывающий # доступных тренеров vs бесплатно
выберите i.intervalId, i.intervalStartTime, sum (isBusy) как coachesBusy, sum (isAvailable) как coachesAvailable из # интервалов I внутреннее соединение #факт f на i.intervalId = f.intervalId группа по i.intervalId, i.intervalStartTime имея sum (isBusy)
затем вы можете искать общие или уникальные идентификаторы интервалов, которые вам нужны.
дайте мне знать, если вам потребуются дополнительные разъяснения.
Я использую небольшую таблицу чисел ... тебе не нужно что-то для свиданий, только цифры. То, что я строю здесь, меньше того, что вы использовали бы в реальном сценарии.
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 ниже ... это искусственно близко к датам, с которыми вы имеете дело, и в реальном сценарии prod у вас будет эта дата раньше, чтобы пойти вместе с вашей таблицей больших чисел.
вот решение вашей проблемы, и оно будет работать со старым 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;
наслаждайтесь!
Я считаю, что следующий запрос будет работать, однако я не могу обещать производительность.
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
это ваш ожидаемый результат, который учитывает занятые тренеры, которые перекрывают доступные тренеры.
| 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