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)
好的,可以用 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每行的 ,以确保它与下一行相交(如果连接组中的任何先前行相交)。我不小心遗漏了这一点。此问题已得到纠正。
| 归档时间: |
|
| 查看次数: |
3732 次 |
| 最近记录: |