【问题标题】:MySQL: sum datetimes without counting overlapping periods twiceMySQL:对日期时间求和而不计算重叠时段两次
【发布时间】:2013-04-03 07:40:22
【问题描述】:

我需要根据如下表格计算花费的总时间:

id |开始时间 |结束时间 |

期间可以重叠的地方。我只需要计算一次产卵期。

例如如果我有这样的时期:

*----A----* *-----C-----* *---------D---------- *
                  *-----B-----* *---E---*

总和将是:(A.end-A.start) + (C.end - B.start) + (D.end - D.start)

我对编写此查询应使用的方法有点困惑,我将不胜感激。

【问题讨论】:

  • 但是我假设C也可以在B结束之前结束,而C在B开始之后开始? (C 时间跨度完全在 B 时间跨度之内?)
  • @nl-x,是的,这也是可能的。我将更新示例。
  • 我已经更新了我之前的回答

标签: mysql


【解决方案1】:

好的,我强烈要求您在将其用于生产之前以各种方式对其进行测试。 特别是测试如果在 1 个时间跨度内有 MULTIPLE 重叠会发生什么。

此查询的作用是计算每个时间跨度的持续时间,以及与具有更高 id 的其他时间跨度存在多少重叠。

select
    t1.id,
    t1.start_time,
    t1.end_time,
    t1.end_time - t1.start_time as duration,
    sum(
          if(t2.start_time <  t1.start_time and t2.end_time >  t1.end_time  , t1.end_time - t1.start_time, 0) -- t2 completely around t1
        + if(t2.start_time >= t1.start_time and t2.end_time <= t1.end_time  , t2.end_time - t2.start_time, 0) -- t2 completely within t1
        + if(t2.start_time <  t1.start_time and t2.end_time >  t1.start_time and t2.end_time   < t1.end_time  , t2.end_time - t1.start_time, 0) -- t2 starts before t1 starts and overlaps partially
        + if(t2.start_time <  t1.end_time   and t2.end_time >  t1.end_time   and t2.start_time > t1.start_time, t1.end_time - t2.start_time, 0) -- t2 starts before t1 ends and overlaps partially
    ) as overlap
from
    times t1
    left join times t2 on
        t2.id > t1.id --  t2.id is greater than t1.id
        and (
               (t2.start_time <  t1.start_time and t2.end_time >  t1.end_time  ) -- t2 completely around t1
            or (t2.start_time >= t1.start_time and t2.end_time <= t1.end_time  ) -- t2 completely within t1
            or (t2.start_time <  t1.start_time and t2.end_time >  t1.start_time) -- t2 starts before t1 starts and overlaps
            or (t2.start_time <  t1.end_time   and t2.end_time >  t1.end_time  ) -- t2 starts before t1 ends and overlaps
        )
group by
    t1.id

那么你最终想要的是这样的:

select
    sum(t.duration) - sum(t.overlap) as filtered_duration
from
    (
        OTHER QUERY HERE
    ) as t

所以最后你有这个查询:

select
    sum(t.duration) - sum(t.overlap) as filtered_duration
from
    (
        select
            t1.id,
            t1.start_time,
            t1.end_time,
            t1.end_time - t1.start_time as duration,
            sum(
                  if(t2.start_time <  t1.start_time and t2.end_time >  t1.end_time  , t1.end_time - t1.start_time, 0) -- t2 completely around t1
                + if(t2.start_time >= t1.start_time and t2.end_time <= t1.end_time  , t2.end_time - t2.start_time, 0) -- t2 completely within t1
                + if(t2.start_time <  t1.start_time and t2.end_time >  t1.start_time and t2.end_time   < t1.end_time  , t2.end_time - t1.start_time, 0) -- t2 starts before t1 starts and overlaps partially
                + if(t2.start_time <  t1.end_time   and t2.end_time >  t1.end_time   and t2.start_time > t1.start_time, t1.end_time - t2.start_time, 0) -- t2 starts before t1 ends and overlaps partially
            ) as overlap
        from
            times t1
            left join times t2 on
                t2.id > t1.id --  t2.id is greater than t1.id
                and (
                       (t2.start_time <  t1.start_time and t2.end_time >  t1.end_time  ) -- t2 completely around t1
                    or (t2.start_time >= t1.start_time and t2.end_time <= t1.end_time  ) -- t2 completely within t1
                    or (t2.start_time <  t1.start_time and t2.end_time >  t1.start_time) -- t2 starts before t1 starts and overlaps
                    or (t2.start_time <  t1.end_time   and t2.end_time >  t1.end_time  ) -- t2 starts before t1 ends and overlaps
                )
        group by
            t1.id
    ) as t

【讨论】:

  • 谢谢,我试试看!我会尽快接受它。
  • @svz 我刚刚测试过,当同时有 3 个时间跨度处于活动状态时它确实会中断,因为重叠的时间跨度会被减去两次。
  • 但我认为这是最好的。所以我不知道你的情况是否有可能同时有 3 个时间跨度。
  • 无论如何,谢谢您的帮助。我会试着做点什么。
【解决方案2】:

我想建议另一种方法来获得时间,同时确保结果是正确的。但我不知道,如何使用 MySQL 完成这项工作。

我将在接下来的几个小时内重复使用上面的示例 - 甚至可能会有一个 3 级条目“F”:

1         3              7           12 13    (15 16)        20
|----A----|              |------C-----| |----------D----------|
                  |-----B-----|              |---E---|
                  5           9              14     17
                                                |F|
  1. 查询所有按时间排序的时间戳的组合列表,并添加每个“动作”的类型

    SELECT 1 as onoff, start_time as time FROM table
    UNION
    SELECT -1 as onoff, end_time as time FROM table
    ORDER BY time
    
  2. 通过带有临时计数器的循环 (?) 处理列表,该计数器在开始/登录时递增 1,在结束/注销时递减 1

    如果计数器从 0 变为 1,则计数器应使脚本向具有 tmp.start=&lt;time&gt; 的临时表添加新行 并在 temp 的上一行更新 tmp.end=&lt;time&gt;。表,如果它从 1 变为 0。

    对于上面的示例,该脚本将执行此操作,如下所示:

    QUERY                       TMP TABLE
    onoff | time  | ctr         ID | start | end
    1     | 01:00 | 1           1  | 01:00 |            (record 1 added,   ctr 0->1)
    -1    | 03:00 | 0           1  | 01:00 | 03:00      (record 1 updated, ctr 1->0)
    1     | 05:00 | 1           2  | 05:00 |            (record 2 added,   ctr 0->1)
    1     | 07:00 | 2                                   (nothing to do)
    -1    | 09:00 | 1                                   (nothing to do)
    -1    | 12:00 | 0           2  | 05:00 | 12:00      (record 2 updated, ctr 1->0)
    1     | 13:00 | 1           3  | 13:00 |            (record 3 added,   ctr 0->1)
    1     | 14:00 | 2                                   (nothing to do)
    1     | 15:00 | 3                                   (nothing to do)
    -1    | 16:00 | 2                                   (nothing to do)
    -1    | 17:00 | 1                                   (nothing to do)
    -1    | 20:00 | 0           3  | 13:00 | 20:00      (record 3 updated, ctr 1->0)
    
  3. 最后一步非常简单:将单元中的timestampdiff()start 获取到end,您需要/喜欢它并进行任何进一步的过滤或分组。

    例如:在别处使用数据

    SELECT ID, start, end, timestampdiff(MINUTE, start, end) FROM tmp
    

    或者例如:总结每个用户的工作时间/登录时间

    SELECT user_id, SUM(timestampdiff(MINUTE, start, end)) FROM tmp GROUP BY user_id
    

我敢肯定,这将为任何级别的嵌套提供正确的持续时间,但是有人知道如何在 MySQL 中完成此操作吗?我也想用这个。

最好的问候

PS:脚本也可能“关闭”最后一个会话或抛出错误,如果它以计数器 > 1 结束并抛出错误,如果计数器随时变为

【讨论】:

    【解决方案3】:

    我为another question 写了一个类似的查询,所以我想我会针对这个问题调整它,以防有人感兴趣。

    SELECT SUM(a.end_time - a.start_time) total_duration
      FROM (
        SELECT MIN(g.start_time) start_time, MAX(g.end_time) end_time 
          FROM (
            SELECT @group_id := @group_id + (@end_time IS NULL OR o.start_time > @end_time) group_id,
                   start_time,
                   @end_time := CAST(CASE 
                     WHEN (@end_time IS NULL OR o.start_time > @end_time) THEN o.end_time
                     ELSE GREATEST(o.end_time, @end_time)
                   END AS DATETIME) end_time  
              FROM times o
              JOIN (SELECT @group_id := 0, @end_time := NULL) init
          ORDER BY o.start_time ASC  
                ) g
      GROUP BY  g.group_id  
            ) a
    

    最里面的查询将您的时间组合在重叠的组中,并在适当的情况下延长 end_time。 end_time 可以灵活处理完全被前一个包围的时间。

    下一个包装查询从每个组中提取完整的时间范围。

    外部查询总结了每个组的差异。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-06-29
      • 2011-09-24
      • 1970-01-01
      • 1970-01-01
      • 2012-07-18
      • 2020-05-15
      相关资源
      最近更新 更多