Подсчет пересекающихся временных интервалов в 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)

enter image description here

затем вы можете искать общие или уникальные идентификаторы интервалов, которые вам нужны.

дайте мне знать, если вам потребуются дополнительные разъяснения.


Я использую небольшую таблицу чисел ... тебе не нужно что-то для свиданий, только цифры. То, что я строю здесь, меньше того, что вы использовали бы в реальном сценарии.

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