Суммирование диапазона раз без учета дублирования дважды

0

Для данного идентификатора пользователя "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.

  • 1
    Какую версию MySQL вы используете?
  • 0
    MariaDB 10.3 - обновлю вопрос :)
Показать ещё 1 комментарий
Теги:
datetime
algorithm
mariadb

3 ответа

2
Лучший ответ

MariaDB 10.3 имеет оконные функции и CTE, так что вы можете использовать их для генерации ваших результатов. CTE удаляет перекрытия из времени сеанса, сравнивая текущий time_start с максимальным предыдущим time_end для этого дня и принимая их максимальное (наибольшее) значение, а затем запрос просто SUM каждый раз в сеансе, группируя по идентификатору пользователя и дате. Обратите внимание, что если один сеанс полностью перекрывается другим, CTE устанавливает время start и end на время 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

Демо на dbfiddle

  • 0
    Это абсолютно великолепно! Работает очень быстро (мое предыдущее «почти работающее» решение занимало целую вечность после обновления до MariaDB 10.3), и результаты кажутся точными! :) Большое спасибо.
  • 1
    @ Нет, не беспокойся. Это была действительно интересная проблема, и я немного больше узнал об оконных функциях.
0

Это было отличное и приятное упражнение.

Итак, хитрость здесь заключается в следующем:

  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

Как мы видим, сеанс 2 пользователя 1 теперь правильно отражает его простые 300 секунд, которые он действительно проработал (5 минут). И для проблемы сеанса 3 пользователя 2 у нас отрицательное число, и причина в том, что он берет время из сеанса 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
0

Это форма проблемы пробелов и островков. Это большая боль в 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;

Вы можете использовать это как подзапрос, чтобы сложить различия.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню