我可以使用 SQL Server CTE 合并相交日期吗?

Joh*_*zen 5 sql sql-server common-table-expression sql-server-2008

我正在编写一个应用程序来处理我们一些员工的休息时间。作为其中的一部分,我需要计算他们全天要求休假的分钟数。

在此工具的第一个版本中,我们不允许重叠的休假请求,因为我们希望能够将所有请求的总和StartTime减去EndTime。防止重叠使此计算非常快。

这已经成为问题,因为经理们现在想要安排团队会议,但当有人已经要求请假时无法这样做。

因此,在该工具的新版本中,我们要求允许重叠请求。

这是一组示例数据,例如我们拥有的数据:

UserId | StartDate | EndDate
----------------------------
 1     | 2:00      | 4:00
 1     | 3:00      | 5:00
 1     | 3:45      | 9:00
 2     | 6:00      | 9:00
 2     | 7:00      | 8:00
 3     | 2:00      | 3:00
 3     | 4:00      | 5:00
 4     | 1:00      | 7:00
Run Code Online (Sandbox Code Playgroud)

我需要尽可能高效地得到的结果是:

UserId | StartDate | EndDate
----------------------------
 1     | 2:00      | 9:00
 2     | 6:00      | 9:00
 3     | 2:00      | 3:00
 3     | 4:00      | 5:00
 4     | 1:00      | 7:00
Run Code Online (Sandbox Code Playgroud)

我们可以轻松地检测到与此查询的重叠:

select
    *
from
    requests r1
cross join
    requests r2
where
    r1.RequestId < r2.RequestId
  and
    r1.StartTime < r2.EndTime
  and
    r2.StartTime < r1.EndTime
Run Code Online (Sandbox Code Playgroud)

事实上,这就是我们最初检测和预防问题的方式。

现在,我们正在尝试合并重叠的项目,但我已经达到了我的 SQL 忍者技能的极限。

想出一种使用临时表的方法并不太难,但我们希望尽可能避免这种情况。

是否有基于集合的方法来合并重叠的行?


编辑:

显示所有行也是可以接受的,只要它们折叠到它们的时间即可。例如,如果有人想从三到五,从四到六,他们可以有两排,一排三到五,下一排五到六或一排三到四,并且接下来从四点到六点。

另外,这里有一个小测试台:

DECLARE @requests TABLE
(
    UserId int,
    StartDate time,
    EndDate time
)

INSERT INTO @requests (UserId, StartDate, EndDate) VALUES
(1, '2:00', '4:00'),
(1, '3:00', '5:00'),
(1, '3:45', '9:00'),
(2, '6:00', '9:00'),
(2, '7:00', '8:00'),
(3, '2:00', '3:00'),
(3, '4:00', '5:00'),
(4, '1:00', '7:00');
Run Code Online (Sandbox Code Playgroud)

Joh*_*zen 4

好的,可以用 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
Run Code Online (Sandbox Code Playgroud)

在这里,“anchor”语句只是每个用户的第一个实例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
Run Code Online (Sandbox Code Playgroud)

我们按上述“第一个相交行”(ConnectedGroup在此查询中调用)进行分组,并找到该组中的最小开始时间和最大结束时间。

使用以下语句传播第一个相交行:

CASE WHEN n.EndDate >= s.StartDate
    THEN n.ConnectedGroup
    ELSE s.Instance
END ConnectedGroup
Run Code Online (Sandbox Code Playgroud)

这基本上是说,“如果此行与前一行相交(基于我们按开始日期排序),则认为此行与前一行具有相同的‘行分组’。否则,使用此行自己的行号作为“行分组”本身。”

这正是我们所寻找的。

编辑

当我最初在白板上想到这一点时,我知道我必须推进EndDate每行的 ,以确保它与下一行相交(如果连接组中的任何先前行相交)。我不小心遗漏了这一点。此问题已得到纠正。