Хорошо, это можно сделать с CTE. Я не знал, как их использовать в начале ночи, но вот результаты моего исследования:
Рекурсивный CTE состоит из 2 частей: оператора привязки и оператора рекурсии.
Важнейшая часть рекурсивного оператора заключается в том, что при его оценке в рекурсии будут отображаться только строки, которые еще не были оценены.
Так, например, если мы хотим использовать CTE для получения полного списка раз для этих пользователей, мы могли бы использовать что-то вроде этого:
WITH
sorted_requests as (
SELECT
UserId, StartDate, EndDate,
ROW_NUMBER() OVER (PARTITION BY UserId ORDER BY StartDate, EndDate DESC) Instance
FROM @requests
),
no_overlap(UserId, StartDate, EndDate, Instance) as (
SELECT *
FROM sorted_requests
WHERE Instance = 1
UNION ALL
SELECT s.*
FROM sorted_requests s
INNER JOIN no_overlap n
ON s.UserId = n.UserId
AND s.Instance = n.Instance + 1
)
SELECT *
FROM no_overlap
Здесь оператор "привязки" - это только первый экземпляр для каждого пользователя, WHERE Instance = 1
.
"Рекурсивный" оператор соединяет каждую строку со следующей строкой в наборе, используя s.UserId = n.UserId AND s.Instance = n.Instance + 1
Теперь мы можем использовать свойство данных при сортировке по дате начала, что любая перекрывающаяся строка будет иметь дату начала, которая меньше даты окончания предыдущей строки. Если мы будем непрерывно распространять номер строки первой пересекающейся строки, каждая последующая перекрывающаяся строка будет использовать этот номер строки.
Используя этот запрос:
WITH
sorted_requests as (
SELECT
UserId, StartDate, EndDate,
ROW_NUMBER() OVER (PARTITION BY UserId ORDER BY StartDate, EndDate DESC) Instance
FROM
@requests
),
no_overlap(UserId, StartDate, EndDate, Instance, ConnectedGroup) as (
SELECT
UserId,
StartDate,
EndDate,
Instance,
Instance as ConnectedGroup
FROM sorted_requests
WHERE Instance = 1
UNION ALL
SELECT
s.UserId,
s.StartDate,
CASE WHEN n.EndDate >= s.EndDate
THEN n.EndDate
ELSE s.EndDate
END EndDate,
s.Instance,
CASE WHEN n.EndDate >= s.StartDate
THEN n.ConnectedGroup
ELSE s.Instance
END ConnectedGroup
FROM sorted_requests s
INNER JOIN no_overlap n
ON s.UserId = n.UserId AND s.Instance = n.Instance + 1
)
SELECT
UserId,
MIN(StartDate) StartDate,
MAX(EndDate) EndDate
FROM no_overlap
GROUP BY UserId, ConnectedGroup
ORDER BY UserId
Мы группируем по вышеупомянутой «первой пересекающейся строке» (называемой ConnectedGroup
в этом запросе) и находим минимальное время начала и максимальное время окончания в этой группе.
Первая пересекающаяся строка распространяется с помощью этого оператора:
CASE WHEN n.EndDate >= s.StartDate
THEN n.ConnectedGroup
ELSE s.Instance
END ConnectedGroup
Что в основном гласит: «если эта строка пересекается с предыдущей строкой (на основании того, что мы отсортированы по дате начала), то считается, что эта строка имеет ту же« группировку строк », что и предыдущая строка. В противном случае используйте собственную строку номер строки как «группировка строк» для себя. "
Это дает нам именно то, что мы искали.
EDIT
Когда я первоначально придумал это на своей доске, я знал, что мне придется продвигать EndDate
каждой строки, чтобы она пересекалась со следующей строкой, если какая-либо из предыдущих строк в подключенной группа бы пересеклась. Я случайно пропустил это. Это было исправлено.