【问题标题】:Summing range of times without counting overlaps twice不计算重叠的时间范围相加两次
【发布时间】:2019-06-29 22:08:36
【问题描述】:

对于给定的用户 ID“1”和给定的日期 2018-01-02,我想计算记录的总小时数,其中可能存在重叠。

计算这个子集:

+-----+---------------------+---------------------+
| uid | time_start          | time_end            |
+-----+---------------------+---------------------+
|   1 | 2018-01-02 04:00:00 | 2018-01-02 04:30:00 |
|   1 | 2018-01-02 04:25:00 | 2018-01-02 04:35:00 |
|   1 | 2018-01-02 04:55:00 | 2018-01-02 05:15:00 |
+-----+---------------------+---------------------+

结果时间应该是:00:55

【问题讨论】:

  • 你用的是什么版本的 MySQL?
  • MariaDB 10.3 - 我会更新问题:)
  • 肯定结果应该是0:55 (0:30 + 0:05 + 0:20)?

标签: mysql sql algorithm datetime mariadb


【解决方案1】:

这是一种差距和孤岛问题。这在 MySQL 中确实很痛苦,但我认为您可以使用变量来做到这一点。

这个想法是遍历记录并注意何时新的开始与之前的“岛”重叠。它成为下一个岛屿的开始。然后您可以汇总并获取每个岛屿的持续时间:

select island_start,
       (to_seconds(max(time_start)) - to_seconds(min(time_end))) as num_seconds
from (select t.*,
             (@ts := if(time_start <= @te,
                        if(@te := greatest(@te, time_end), @ts, @ts),  -- no change on the start
                        if(@te := time_end, time_start, time_start)
                       )
             ) as island_start
      from (select t.*
            from t
            order by time_start
           ) t cross join
           (select @ts := -1, @te := -1) params
     ) t
group by island_start;

您可以将其用作子查询来添加差异。

【讨论】:

    【解决方案2】:

    MariaDB 10.3 具有窗口函数和 CTE,因此您可以使用它们来生成结果。 CTE 通过将当前time_start 与当天的最大先前time_end 进行比较并获取它们的最大值(最大值)然后在每个会话时间简单地查询SUMs,从而消除会话时间中的重叠,分组按用户 ID 和日期。请注意,如果一个会话与另一个会话完全重叠,CTE 会将 startend 时间设置为重叠会话的 end 时间,从而导致有效会话长度为 0。我已将我的演示扩展到包括这样的场景,以及多个重叠的会话:

    WITH sessions AS 
        (SELECT uid,
                GREATEST(time_start, COALESCE(MAX(time_end) OVER (PARTITION BY DATE(time_start) ORDER BY time_start ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), '2000-01-01')) AS start,
                MAX(time_end) OVER (PARTITION BY DATE(time_start) ORDER BY time_start ROWS UNBOUNDED PRECEDING)  AS end
                FROM sessions)
    SELECT uid, DATE(start) AS `date`, SEC_TO_TIME(SUM(TO_SECONDS(end) - TO_SECONDS(start))) AS totaltime
    FROM sessions
    GROUP BY uid, `date`
    

    输出:

    uid     date        totaltime
    1       2018-01-02  00:55:00
    1       2018-01-03  01:00:00
    1       2018-01-04  01:15:00
    

    Demo on dbfiddle

    【讨论】:

    • 这真是太棒了!运行速度非常快(我之前的“几乎可以工作”的解决方案在升级到 MariaDB 10.3 后一直持续),结果似乎是准确的! :) 非常感谢。
    • @Nuno 不用担心。这是一个非常有趣的问题,我在此过程中学到了更多关于窗口函数的知识。
    【解决方案3】:

    这是一次很棒且令人愉快的练习。

    所以,这里的窍门如下:

    1. 此人在从上次会话注销之前再次登录,并在第一次会话后结束会话;或
    2. 此人在退出上次会话之前再次登录,并在结束第一次会话之前结束会话

    因此,解决这个问题的诀窍就是为新会话分配一个开始时间,该时间等于他上一个会话的结束时间。在这种情况下,您将每个会话作为一行,您将能够计算时间差。好的,让我们模拟一下这个例子 :

      create table #temp (userId int, timeComienza datetime, timeTermina dateTime )
    
    -- exemplo de overlap
      insert into #temp values (1, '20180102 16:00', '20180102 16:30')
      insert into #temp values (1, '20180102 16:25', '20180102 16:35')
      insert into #temp values (1, '20180102 16:55', '20180102 17:15')
    -- ejemplo de no overlap
      insert into #temp values (2, '20180102 16:00', '20180102 16:30')
      insert into #temp values (2, '20180102 16:35', '20180102 16:50')
      insert into #temp values (2, '20180102 16:40', '20180102 16:45')
    
    
    userId  timeComienza    timeTermina
    1   2018-01-02 16:00:00.000 2018-01-02 16:30:00.000
    1   2018-01-02 16:25:00.000 2018-01-02 16:35:00.000
    1   2018-01-02 16:55:00.000 2018-01-02 17:15:00.000
    2   2018-01-02 16:00:00.000 2018-01-02 16:30:00.000
    2   2018-01-02 16:35:00.000 2018-01-02 16:50:00.000
    2   2018-01-02 16:40:00.000 2018-01-02 16:45:00.000
    

    您可以看到,用户 1 在结束他的第一个会话之前登录了他的第二个会话,而用户 2 在他的第三个会话中遇到了几乎相同的问题,除了他的第三个会话在结束他的第二个会话之前结束(完全重叠和他的第二次会议日食)。

    我们需要做的第一件事,就是使用order by来提供这些会话的顺序。

    select *, ROW_NUMBER() over(partition by userId order by timeComienza) as unOrden 
    into #temp2 
    from #temp 
    
    userId  timeComienza    timeTermina         unOrden
    1   2018-01-02 16:00:00.000 2018-01-02 16:30:00.000 1
    1   2018-01-02 16:25:00.000 2018-01-02 16:35:00.000 2
    1   2018-01-02 16:55:00.000 2018-01-02 17:15:00.000 3
    2   2018-01-02 16:00:00.000 2018-01-02 16:30:00.000 1
    2   2018-01-02 16:35:00.000 2018-01-02 16:50:00.000 2
    2   2018-01-02 16:40:00.000 2018-01-02 16:45:00.000 3
    

    现在,使用我们的迭代将容易 100 倍。 让我们创建一个与表 #2 具有相同结构的空表作为插入分析的表。

    select * 
    into #tablaInsertar
    from #temp2
    
    delete from #tablaInsertar
    

    最后,这是我们分析的核心:)

    -- variable to iterate users
    declare @x int = 1 , @usuarios int = 1, @usuariosMax int
    --num dif de usuarios:
    select @usuariosMax = count(distinct(userId)) from #temp2 
    
    
    while(@usuarios <= @usuariosMax)
    begin
    
    /*trabajando cada usuario*/
        /*Primero necesitamos saber la longitud de cada Usuario*/
        declare @trabajaUsuario int = 1, @longUsuario int
    
        --obtiene longitud usuario
        select @longUsuario = count(1) from #temp
        where userId = @usuarios
    
        while(@trabajaUsuario <= @longUsuario)
        begin 
    
            if(@trabajaUsuario = 1)
            begin 
    
                insert into #tablaInsertar
                select 
                    *
                from #temp2
                where userId = @usuarios and unOrden = @trabajaUsuario
    
            end 
    
            else -- dado que no sea la primera fila
            -- comparando horas
            begin 
                    declare @horaInicioEstePeriodo dateTime, @horaTerminaAnterior dateTime
                    select @horaInicioEstePeriodo = #temp2.timeComienza from #temp2 where userId = @usuarios and unOrden = @trabajaUsuario
                    select @horaTerminaAnterior = #temp2.timeTermina from #temp2 where userId = @usuarios and unOrden = @trabajaUsuario - 1
    
                    if(@horaInicioEstePeriodo < @horaTerminaAnterior) -- las modificaciones dado que el periodo inicio sea menro a la hora anterior
                    begin 
    
                        insert into #tablaInsertar
                        select 
                            t2.userId
                            , t1.timeTermina as tiempoComienzaActualizado
                            , t2.timeTermina
                            , t2.unOrden
                        from 
                            (
                                select 
                                    #temp2.userId
                                    ,#temp2.timeComienza
                                    , #temp2.timeTermina
                                    , #temp2.unOrden
                                from #temp2
                                where userId = @usuarios and unOrden = @trabajaUsuario - 1
                            )t1
                            join
                            (
                                select 
                                    #temp2.userId
                                    --, as tiempoComienzaActualizado --#temp2.timeComienza
                                    , #temp2.timeTermina
                                    , #temp2.unOrden
                                from #temp2
                                where userId = @usuarios and unOrden = @trabajaUsuario
                            ) t2 on t1.userId = t2.userId and t1.unOrden + 1 = t2.unOrden
                    end 
    
                    else -- dado que el periodo inicia sea mayor o igual a la hora anterior
                    begin 
    
                        insert into #tablaInsertar
                        select 
                            *
                        from #temp2
                        where userId = @usuarios and unOrden = @trabajaUsuario
                    end 
    
            end 
    
        select @trabajaUsuario += 1
        end
    
    select @usuarios += 1
    end
    

    让我们看看我们的新表:)

    select *, DATEDIFF(s,timeComienza,timeTermina) timeInSeconds
    from #tablaInsertar
    
    userId  timeComienza    timeTermina unOrden timeInSeconds
    1   2018-01-02 16:00:00.000 2018-01-02 16:30:00.000 1   1800
    1   2018-01-02 16:30:00.000 2018-01-02 16:35:00.000 2   300
    1   2018-01-02 16:55:00.000 2018-01-02 17:15:00.000 3   1200
    2   2018-01-02 16:00:00.000 2018-01-02 16:30:00.000 1   1800
    2   2018-01-02 16:35:00.000 2018-01-02 16:50:00.000 2   900
    2   2018-01-02 16:50:00.000 2018-01-02 16:45:00.000 3   -300
    

    我们可以看到,用户 1 会话 2 现在正确地反映了他真正工作的仅仅 300 秒(5 分钟)。而对于用户 2 会话 3 的问题,我们有一个负数,原因是因为他正在从他的会话 2 中花费时间。所以,我们需要做的就是将正值相加,才能知道每个用户记录的实时时间,像这样:

    select 
        t1.userId,
        sum(case when timeInSeconds > 0 then timeInSeconds else 0 end) totalTimeLogged
    from 
    (
        select *, DATEDIFF(s,timeComienza,timeTermina) timeInSeconds
        from #tablaInsertar
    ) t1
    group by t1.userId
    

    最终结果:

    userId  totalTimeLogged
    1           3300
    2           2700
    

    【讨论】:

      猜你喜欢
      • 2023-01-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-06-12
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多