簡體   English   中英

我可以使用SQL Server CTE合並相交日期嗎?

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM