[英]Can I use a SQL Server CTE to merge intersecting dates?
我正在編寫一個應用程序來處理我們的一些員工的安排時間。 作為其中的一部分,我需要計算他們要求關閉的一整天。
在此工具的第一個版本中,我們不允許重疊請求的時間,因為我們希望能夠將所有請求的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
我需要盡可能高效地獲得的結果是:
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
我們可以使用此查詢輕松檢測重疊:
select
*
from
requests r1
cross join
requests r2
where
r1.RequestId < r2.RequestId
and
r1.StartTime < r2.EndTime
and
r2.StartTime < r1.EndTime
事實上,這就是我們如何檢測和預防最初的問題。
現在,我們正在嘗試合並重疊項目,但我已達到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');
好的,可以用CTE做。 我不知道如何在夜間使用它們,但這是我研究的結果:
遞歸CTE有兩部分,“錨”語句和“遞歸”語句。
關於遞歸語句的關鍵部分是,在計算它時,只有尚未評估的行才會顯示在遞歸中。
因此,例如,如果我們想要使用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
這里,“anchor”語句只是每個用戶的第一個實例, WHERE Instance = 1
。
“recursive”語句使用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
這基本上說,“如果此行與前一行相交(基於我們按開始日期排序),則認為此行與前一行具有相同的”行分組“。否則,請使用此行自己的行號作為“行分組”本身。“
這給了我們正是我們想要的東西。
編輯
當我最初在我的白板上想到這一點時,我知道我必須提前每行的EndDate
,以確保它與下一行相交,如果連接組中的任何先前行將相交。 我不小心把它弄出來了。 這已得到糾正。
;WITH new_grp AS (
SELECT r1.UserId, r1.StartTime
FROM @requests r1
WHERE NOT EXISTS (
SELECT *
FROM @requests r2
WHERE r1.UserId = r2.UserId
AND r2.StartTime < r1.StartTime
AND r2.EndTime >= r1.StartTime)
GROUP BY r1.UserId, r1.StartTime -- there can be > 1
),r AS (
SELECT r.RequestId, r.UserId, r.StartTime, r.EndTime
,count(*) AS grp -- guaranteed to be 1+
FROM @requests r
JOIN new_grp n ON n.UserId = r.UserId AND n.StartTime <= r.StartTime
GROUP BY r.RequestId, r.UserId, r.StartTime, r.EndTime
)
SELECT min(RequestId) AS RequestId
,UserId
,min(StartTime) AS StartTime
,max(EndTime) AS EndTime
FROM r
GROUP BY UserId, grp
ORDER BY UserId, grp
現在生成請求的結果並真正涵蓋所有可能的情況,包括析取子組和重復。 在data.SE上查看工作演示中對測試數據的注釋 。
CTE 1
找到一組新的重疊間隔開始的(唯一!)時間點。
CTE 2
計算新組的開始直到(並包括)每個單獨的間隔,從而形成每個用戶的唯一組編號。
最后的選擇
合並小組,為小組開始和最后結束。
我遇到了一些困難,因為T-SQL窗口函數max()
或sum()
不接受窗口中的ORDER BY
子句。 它們每個分區只能計算一個值,這使得無法計算每個分區的運行總和/計數。 可以在PostgreSQL或Oracle中工作(當然不能在MySQL中工作 - 它既沒有窗口函數也沒有CTE)。
最終解決方案使用一個額外的CTE,應該同樣快。
這適用於postgres。 Microsoft可能需要進行一些修改。
SET search_path='tmp';
DROP TABLE tmp.schedule CASCADE;
CREATE TABLE tmp.schedule
( person_id INTEGER NOT NULL
, dt_from timestamp with time zone
, dt_to timestamp with time zone
);
INSERT INTO schedule( person_id, dt_from, dt_to) VALUES
( 1, '2011-12-03 02:00:00' , '2011-12-03 04:00:00' )
, ( 1, '2011-12-03 03:00:00' , '2011-12-03 05:00:00' )
, ( 1, '2011-12-03 03:45:00' , '2011-12-03 09:00:00' )
, ( 2, '2011-12-03 06:00:00' , '2011-12-03 09:00:00' )
, ( 2, '2011-12-03 07:00:00' , '2011-12-03 08:00:00' )
, ( 3, '2011-12-03 02:00:00' , '2011-12-03 03:00:00' )
, ( 3, '2011-12-03 04:00:00' , '2011-12-03 05:00:00' )
, ( 4, '2011-12-03 01:00:00' , '2011-12-03 07:00:00' );
ALTER TABLE schedule ADD PRIMARY KEY (person_id,dt_from)
;
CREATE UNIQUE INDEX ON schedule (person_id,dt_to);
SELECT * FROM schedule ORDER BY person_id, dt_from;
WITH RECURSIVE ztree AS (
-- Terminal part
SELECT p1.person_id AS person_id
, p1.dt_from AS dt_from
, p1.dt_to AS dt_to
FROM schedule p1
UNION
-- Recursive part
SELECT p2.person_id AS person_id
, LEAST(p2.dt_from, zzt.dt_from) AS dt_from
, GREATEST(p2.dt_to, zzt.dt_to) AS dt_to
FROM ztree AS zzt
, schedule AS p2
WHERE 1=1
AND p2.person_id = zzt.person_id
AND (p2.dt_from < zzt.dt_from AND p2.dt_to >= zzt.dt_from)
)
SELECT *
FROM ztree zt
WHERE NOT EXISTS (
SELECT * FROM ztree nx
WHERE nx.person_id = zt.person_id
-- the recursive query returns *all possible combinations of
-- touching or overlapping intervals
-- we'll have to filter, keeping only the biggest ones
-- (the ones for which there is no bigger overlapping interval)
AND ( (nx.dt_from <= zt.dt_from AND nx.dt_to > zt.dt_to)
OR (nx.dt_from < zt.dt_from AND nx.dt_to >= zt.dt_to)
)
)
ORDER BY zt.person_id,zt.dt_from
;
結果:
DROP TABLE
CREATE TABLE
INSERT 0 8
NOTICE: ALTER TABLE / ADD PRIMARY KEY will create implicit index "schedule_pkey" for table "schedule"
ALTER TABLE
CREATE INDEX
person_id | dt_from | dt_to
-----------+------------------------+------------------------
1 | 2011-12-03 02:00:00+01 | 2011-12-03 04:00:00+01
1 | 2011-12-03 03:00:00+01 | 2011-12-03 05:00:00+01
1 | 2011-12-03 03:45:00+01 | 2011-12-03 09:00:00+01
2 | 2011-12-03 06:00:00+01 | 2011-12-03 09:00:00+01
2 | 2011-12-03 07:00:00+01 | 2011-12-03 08:00:00+01
3 | 2011-12-03 02:00:00+01 | 2011-12-03 03:00:00+01
3 | 2011-12-03 04:00:00+01 | 2011-12-03 05:00:00+01
4 | 2011-12-03 01:00:00+01 | 2011-12-03 07:00:00+01
(8 rows)
person_id | dt_from | dt_to
-----------+------------------------+------------------------
1 | 2011-12-03 02:00:00+01 | 2011-12-03 09:00:00+01
2 | 2011-12-03 06:00:00+01 | 2011-12-03 09:00:00+01
3 | 2011-12-03 02:00:00+01 | 2011-12-03 03:00:00+01
3 | 2011-12-03 04:00:00+01 | 2011-12-03 05:00:00+01
4 | 2011-12-03 01:00:00+01 | 2011-12-03 07:00:00+01
(5 rows)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.