在T-SQL中查找最大时间重叠

在T-SQL中查找最大时间重叠,sql,sql-server,tsql,sql-server-2008-r2,Sql,Sql Server,Tsql,Sql Server 2008 R2,我正在尝试在SQLServer2008R2上执行此操作 我有一个包含4列的表: parent_id INT child_id INT start_time TIME end_time TIME 您应该将子进程看作是为父程序运行的子进程。所有这些子进程每天运行一次,每个子进程在其给定的时间范围内运行。我想根据每个父进程的子进程的时间,找出每个父进程的时间间隔的最大重叠,也就是说,我想知道所有子进程运行的时间间隔的最长重叠。每个时间间隔每天重复的事实意味着,即使孩子的时间间隔跨越午夜(即23:00

我正在尝试在SQLServer2008R2上执行此操作

我有一个包含4列的表:

parent_id INT
child_id INT
start_time TIME
end_time TIME
您应该将子进程看作是为父程序运行的子进程。所有这些子进程每天运行一次,每个子进程在其给定的时间范围内运行。我想根据每个父进程的子进程的时间,找出每个父进程的时间间隔的最大重叠,也就是说,我想知道所有子进程运行的时间间隔的最长重叠。每个时间间隔每天重复的事实意味着,即使孩子的时间间隔跨越午夜(即23:00-10:00),它也可以与只在早上跑步的孩子重叠(即07:00-09:00),因为即使他们在“第一天”没有重叠,他们也会在随后的所有日子重叠

输出应如下所示:

parent_id INT
start_time TIME
end_time TIME
valid BIT
其中,如果发现重叠,则
valid=1
;如果未发现重叠,则
valid=0

以下是几条重要信息:

  • 时间间隔可以跨越午夜,即开始时间=23:00和结束时间=03:00,时间间隔为4小时
  • 两个时间间隔可能在两个不同的位置重叠,即
    开始时间1=13:00
    结束时间1=06:00
    开始时间2=04:00
    结束时间2=14:00
    。这将产生最大重叠,即04:00-06:00=2小时
  • 给定父级的子级可能没有常见的重叠,在这种情况下,该父级的输出将是
    start\u time=NULL
    end\u time=NULL
    valid=0
  • 如果一个子时段跨越了一整天,那么
    start\u time=NULL
    end\u time=NULL
    。选择此选项是为了避免将一天的时间设置为00:00-24:00,这会将跨越午夜的重叠部分一分为二,即下面的父3将有两个重叠部分(23:00-24:00和00:00-004:00),而不是一个重叠部分(23:00-04:00)
  • 如果时间间隔由父级的所有子级共享,则重叠仅为重叠
  • 一个孩子的时间跨度不能超过24小时
  • 举个例子:

    parent_id  child_id  start_time  end_time
        1         1           06:00     14:00
        1         2           13:00     09:00
        1         3           07:00     09:00
        2         1           12:00     17:00
        2         2           09:00     11:00
        3         1            NULL      NULL
        3         2           23:00     04:00
        4         1            NULL      NULL
        4         2            NULL      NULL
       10         1           06:11     14:00
       10         2           06:00     09:00
       10         3           05:00     08:44
       11         1           11:38     17:00
       11         2           09:02     12:11
    
    这些数据将产生以下结果集:

    parent_id  start_time  end_time  valid
        1           07:00     09:00    1
        2            NULL      NULL    0
        3           23:00     04:00    1
        4            NULL      NULL    1
       10           06:11     08:44    1
       11           11:38     12:11    1
    
    父对象的重叠是其所有子对象共享的时间间隔。因此,父级10的重叠是通过查找所有3个子级共享时间的重叠来找到的: 子1(06:11-14:00)和子2(06:00-09:00)从06:11到09:00重叠。该重叠时间间隔随后应用于子3(05:00-08:44),该时间间隔给出06:11到08:44的重叠,因为该时间间隔是所有3个子项共享公共时间的唯一时间间隔

    我希望这是有道理的

    我可以用光标来做,但我更愿意避免使用光标。我一直在绞尽脑汁想如何在没有光标的情况下做到这一点,但我还是做不到。有没有不用光标的方法

    编辑:扩展了第4条的文本,以解释将全天从0:00改为0:00的决定。 编辑:用另外两个案例扩展示例。新病例的父母ID为10和11。 编辑:插入如何找到父级10重叠的说明。
    编辑:澄清第3条。增加第5及6条。详细介绍了这一切。

    这可能是一种实现所需结果的非常详细的方法,但它适用于给定的数据集,尽管它应该使用更大的数据进行测试

    我只是简单地将表连接到它本身,
    parent\u id
    匹配,
    child\u id
    不同,以获得所有可能重叠的时间组合,然后在过滤和分组输出之前执行一些
    DATEDIFF
    来计算差异

    如果需要,您可以单独运行以下测试和调整:

    -- setup initial table
    CREATE TABLE #OverlapTable
        (
          [parent_id] INT ,
          [child_id] INT ,
          [start_time] TIME ,
          [end_time] TIME
        );
    
    -- insert dummy data
    INSERT  INTO #OverlapTable
            ( [parent_id], [child_id], [start_time], [end_time] )
    VALUES  ( 1, 1, '06:00', '14:00' ),
            ( 1, 2, '13:00', '09:00' ),
            ( 1, 3, '07:00', '09:00' ),
            ( 2, 1, '12:00', '17:00' ),
            ( 2, 2, '09:00', '11:00' ),
            ( 3, 1, NULL, NULL ),
            ( 3, 2, '23:00', '04:00' ),
            ( 4, 1, NULL, NULL ),
            ( 4, 2, NULL, NULL );
    
    -- insert all combinations into a new temp table #Results with overlap calculations
    SELECT  *
    INTO    #Results
    FROM    ( SELECT    t1.parent_id ,
                        t1.start_time ,
                        t1.end_time ,
                        t2.start_time AS t2_start_time ,
                        t2.end_time AS t2_end_time ,
                        CASE WHEN t1.start_time IS NULL
                                  AND t1.end_time IS NULL THEN 0
                             WHEN t1.start_time BETWEEN t2.start_time
                                                AND     t2.end_time
                             THEN DATEDIFF(HOUR, t1.start_time, t2.end_time)
                             WHEN t1.end_time BETWEEN t2.start_time AND t2.end_time
                             THEN DATEDIFF(HOUR, t2.start_time, t1.end_time)
                             ELSE NULL
                        END AS Overlap
              FROM      #OverlapTable t1
                        INNER JOIN #OverlapTable t2 ON t2.parent_id = t1.parent_id
                                                       AND t2.child_id != t1.child_id
            ) t
    
    -- SELECT * FROM #Results -- this shows intermediate results
    
    -- filter and group results with the largest overlaps and handle other cases
    SELECT DISTINCT
            r.parent_id ,
            CASE WHEN r.Overlap IS NULL THEN NULL
                 ELSE CASE WHEN r.start_time IS NULL THEN r.t2_start_time
                           ELSE r.start_time
                      END
            END start_time ,
            CASE WHEN r.Overlap IS NULL THEN NULL
                 ELSE CASE WHEN r.end_time IS NULL THEN r.t2_end_time
                           ELSE r.end_time
                      END
            END end_time ,
            CASE WHEN r.Overlap IS NULL THEN 0
                 ELSE 1
            END Valid
    FROM    #Results r
    WHERE   EXISTS ( SELECT parent_id ,
                            MAX(Overlap)
                     FROM   #Results
                     WHERE  r.parent_id = parent_id
                     GROUP BY parent_id
                     HAVING MAX(Overlap) = r.Overlap
                            OR ( MAX(Overlap) IS NULL
                                 AND r.Overlap IS NULL
                               ) )
    
    DROP TABLE #Results
    DROP TABLE #OverlapTable
    

    希望能有所帮助。

    基于您的问题,我认为您的输出应该是:

    parent_id   start_time  end_time    valid
    1           07:00       09:00       1
    2           NULL        NULL        0
    3           23:00       04:00       1
    4           NULL        NULL        1
    10          06:11       08:44       1
    11          11:38       12:11       1
    
    下面是一个基于集合的解决方案:

    DECLARE @Times TABLE
    (
        parent_id INT
        ,child_id INT
        ,start_time TIME
        ,end_time TIME
    );
    
    INSERT INTO @Times
    VALUES
        (1,         1,           '06:00',     '14:00')
        ,(1,         2,           '13:00',     '09:00')
        ,(1,         3,           '07:00',     '09:00')
        ,(2,         1,           '12:00',     '17:00')
        ,(2,         2,           '09:00',     '11:00')
        ,(3,         1,            NULL,      NULL)
        ,(3,         2,           '23:00',     '04:00')
        ,(4,         1,            NULL,      NULL)
        ,(4,         2,            NULL,      NULL)
        ,(10,         1,           '06:11',     '14:00')
        ,(10,         2,           '06:00',     '09:00')
        ,(10,         3,           '05:00',     '08:44')
        ,(11,         1,           '11:38',     '17:00')
        ,(11,         2,           '09:02',     '12:11');
    
    
    DECLARE @Parents TABLE
    (
        parent_id INT PRIMARY KEY
        ,ChildCount INT
    )
    INSERT INTO @Parents
    SELECT 
        parent_id
        ,COUNT(DISTINCT child_id) AS ChildCount
    FROM
        @Times
    GROUP BY 
        parent_id
    
    DECLARE @StartTime DATETIME2 = '00:00'
    DECLARE @MinutesInTwoDays INT = 2880
    DECLARE @Minutes TABLE(ThisMinute DATETIME2 PRIMARY KEY);
    
    WITH 
    MinutesCTE AS
    (
        SELECT 
            1 AS MinuteNumber
            ,@StartTime AS ThisMinute
    
        UNION ALL
    
        SELECT 
            NextMinuteNumber
            ,NextMinute
        FROM MinutesCTE
        CROSS APPLY (VALUES(MinuteNumber+1,DATEADD(MINUTE,1,ThisMinute))) NextDates(NextMinuteNumber,NextMinute)
        WHERE 
            NextMinuteNumber <= @MinutesInTwoDays
    )
    INSERT INTO @Minutes
    SELECT ThisMinute FROM MinutesCTE M OPTION (MAXRECURSION 2880);
    
    
    DECLARE @SharedMinutes TABLE
    (
        ThisMinute DATETIME2 
        ,parent_id INT
        ,UNIQUE(ThisMinute,parent_id)
    );
    
    WITH TimesCTE AS
    (
        SELECT
            Times.parent_id
            ,Times.child_id
            ,CAST(ISNULL(Times.start_time,'00:00') AS datetime2) AS start_time
            ,
            DATEADD
            (   
                DAY
                ,
                CASE 
                    WHEN Times.end_time IS NULL THEN 2
                    WHEN Times.start_time > Times.end_time THEN 1
                    ELSE 0 
                END
                ,CAST(ISNULL(Times.end_time,'00:00') AS datetime2)
            ) as end_time
        FROM
            @Times Times
    
    
        UNION ALL
    
        SELECT
            Times.parent_id
            ,Times.child_id
            ,DATEADD(DAY,1,CAST(Times.start_time as datetime2)) AS start_time
            ,DATEADD(DAY,1,CAST(Times.end_time AS datetime2)) AS end_time
        FROM
            @Times Times
        WHERE
            start_time < end_time
    
    )
    
    --Get minutes shared by all children of each parent
    INSERT INTO @SharedMinutes
    SELECT 
        M.ThisMinute
        ,P.parent_id
    FROM
        @Minutes M
    JOIN
        TimesCTE T
        ON 
            M.ThisMinute BETWEEN start_time AND end_time
    JOIN
        @Parents P
        ON T.parent_id = P.parent_id
    
    GROUP BY 
        M.ThisMinute
        ,P.parent_id
        ,P.ChildCount
    HAVING
        COUNT(DISTINCT T.child_id) = P.ChildCount
    
    --get results
    SELECT
        parent_id
        ,CAST(CASE WHEN start_time = '1900-01-01' AND end_time = '1900-01-02 23:59' THEN NULL ELSE start_time END AS TIME) AS start_time
        ,CAST(CASE WHEN start_time = '1900-01-01' AND end_time = '1900-01-02 23:59' THEN NULL ELSE end_time END AS TIME) AS end_time
        ,valid
    FROM
    (
        SELECT
            P.parent_id
            ,MIN(ThisMinute) AS start_time
            ,MAX(ThisMinute) AS end_time
            ,CASE WHEN MAX(ThisMinute) IS NOT NULL THEN 1 ELSE 0 END AS valid 
        FROM
            @Parents P
        LEFT JOIN
            @SharedMinutes SM
            ON P.parent_id = SM.parent_id
        GROUP BY
            P.parent_id
    
    ) Results
    
    DECLARE@Times表
    (
    父id INT
    ,child_id INT
    ,开始时间
    ,结束时间
    );
    插入@Times
    价值观
    (1,         1,           '06:00',     '14:00')
    ,(1,         2,           '13:00',     '09:00')
    ,(1,         3,           '07:00',     '09:00')
    ,(2,         1,           '12:00',     '17:00')
    ,(2,         2,           '09:00',     '11:00')
    ,(3,1,空,空)
    ,(3,         2,           '23:00',     '04:00')
    ,(4,1,空,空)
    ,(4,2,空,空)
    ,(10,         1,           '06:11',     '14:00')
    ,(10,         2,           '06:00',     '09:00')
    ,(10,         3,           '05:00',     '08:44')
    ,(11,         1,           '11:38',     '17:00')
    ,(11,         2,           '09:02',     '12:11');
    声明@Parents表
    (
    父\u id INT主键
    ,ChildCount INT
    )
    插入到@Parents中
    挑选
    家长id
    ,计数(不同的子项id)为ChildCount
    从…起
    @时代
    分组
    家长id
    声明@StartTime DATETIME2='00:00'
    声明@MinutesInTwoDays INT=2880
    声明@Minutes表(thisminutedatetime2主键);
    具有
    微小的
    (
    挑选
    1为分钟数
    ,@StartTime作为这一分钟
    联合所有
    挑选
    下一分钟
    ,下一分钟
    从小到大
    交叉应用(值(分钟数+1,日期添加(分钟,1,本分钟)))下一个日期(下一个分钟数,下一个分钟)
    哪里
    下一分钟数次。结束时间然后是1
    其他0
    结束
    ,强制转换(ISNULL(Times.end_time,'00:00')作为日期时间2)
    )作为结束时间
    从…起
    @时代
    联合所有
    挑选
    Times.parent\u id
    ,Times.child\u id
    ,DATEADD(第1天,将Times.start_time转换为datetime2))转换为start_time
    ,DATEADD(第1天,强制转换(Times.end_time为datetime2))为end_time
    从…起
    @时代
    哪里
    开始时间<结束时间
    )
    --分秒必争