Высчитать общее время временных диапозонов

Имеется БД MSql, в ней хранятся промежутки временных диапазонов (начало, конец)

На примере офиса:

1) Вася пришёл в офис 24.01.2020 09:30 и ушёл 24.01.2020 11:00 (1 (09:30-11:00)
2) Петя пришёл в офис 24.01.2020 12:00 и ушёл 24.01.2020 13:00 (2 
3) Саша пришёл в офис 24.01.2020 12:30 и ушёл 24.01.2020 17:00 (2
4) Гена пришёл в офис 24.01.2020 13:30 и ушёл 24.01.2020 14:00 (2 (12:00-17:00)
5) Вася пришёл в офис 25.01.2020 10:00 и ушёл 25.01.2020 15:00 (3 (10:00-15:00)

Общее время (сумма 1-2-3 диапазонов) =  1.5 + 5 + 5 = 21.5 часов.

На примере всего 5 записей и 3 интервала времени, по факту их может быть 1000+
Как я понимаю, надо все диапазоны положить в одну плоскость/линию и посчитать суммарное время
SQL запрос или LINQ или ещё что? в какую сторону смотреть, чтобы реализовать данный подсчёт?


Ответы (2 шт):

Автор решения: Stanislav Volodarskiy

Каждая запись порождает два события - приход и уход:

24.01.2020 09:30 Вася пришёл
24.01.2020 11:00 Вася ушёл
24.01.2020 12:00 Петя пришёл
24.01.2020 13:00 Петя ушёл
24.01.2020 12:30 Саша пришёл
24.01.2020 17:00 Саша ушёл
24.01.2020 13:30 Гена пришёл
24.01.2020 14:00 Гена ушёл
25.01.2020 10:00 Вася пришёл
25.01.2020 15:00 Вася ушёл

Отсортируем их по времени и добавим в таблицу число работников на работе:

                                 0 на работе
24.01.2020 09:30 Вася пришёл     1 на работе
24.01.2020 11:00 Вася ушёл       0 на работе
24.01.2020 12:00 Петя пришёл     1 на работе
24.01.2020 12:30 Саша пришёл     2 на работе
24.01.2020 13:00 Петя ушёл       1 на работе
24.01.2020 13:30 Гена пришёл     2 на работе
24.01.2020 14:00 Гена ушёл       1 на работе
24.01.2020 17:00 Саша ушёл       0 на работе
25.01.2020 10:00 Вася пришёл     1 на работе
25.01.2020 15:00 Вася ушёл       0 на работе

Вычеркнем события когда на работе людей стало или было больше одного (должны остаться события вида "первый пришёл" и "последний ушёл"):

24.01.2020 09:30 Вася пришёл     1 на работе
24.01.2020 11:00 Вася ушёл       0 на работе
24.01.2020 12:00 Петя пришёл     1 на работе
24.01.2020 17:00 Саша ушёл       0 на работе
25.01.2020 10:00 Вася пришёл     1 на работе
25.01.2020 15:00 Вася ушёл       0 на работе

События соберём в пары. Для каждой пары вычислим длину промежутка и сложим их:

24.01.2020 09:30 Вася пришёл     1 на работе
24.01.2020 11:00 Вася ушёл       0 на работе   1.5 часа

24.01.2020 12:00 Петя пришёл     1 на работе
24.01.2020 17:00 Саша ушёл       0 на работе   5 часов

25.01.2020 10:00 Вася пришёл     1 на работе
25.01.2020 15:00 Вася ушёл       0 на работе   5 часов

                                 сумма         11.5 часов

Коротко

  1. из промежутков наделать событий,

  2. отсортировать,

  3. подсчитать число работников на работе,

  4. убрать события, когда работников много,

  5. оставшиеся события попарно вычесть,

  6. разности сложить.

Технология называется заметание.

Пример на PSQL

Исходная таблица:

@sv=# select * from ranges;
  who  |        enter        |        leave        
-------+---------------------+---------------------
 Vasya | 2020-01-24 09:30:00 | 2020-01-24 11:00:00
 Petya | 2020-01-24 12:00:00 | 2020-01-24 13:00:00
 Sasha | 2020-01-24 12:30:00 | 2020-01-24 17:00:00
 Gena  | 2020-01-24 13:30:00 | 2020-01-24 14:00:00
 Vasya | 2020-01-25 10:00:00 | 2020-01-25 15:00:00
(5 rows)

Таблица с событиями. Приходы помечаются как -1, уходы как 1. Почему так станет ясно позже:

@sv=# create view events
    as    select -1 as tag, enter as time from ranges
    union select  1 as tag, leave as time from ranges
    order by time;
CREATE VIEW
@sv=# select * from events;
 tag |        time         
-----+---------------------
  -1 | 2020-01-24 09:30:00
   1 | 2020-01-24 11:00:00
  -1 | 2020-01-24 12:00:00
  -1 | 2020-01-24 12:30:00
   1 | 2020-01-24 13:00:00
  -1 | 2020-01-24 13:30:00
   1 | 2020-01-24 14:00:00
   1 | 2020-01-24 17:00:00
  -1 | 2020-01-25 10:00:00
   1 | 2020-01-25 15:00:00
(10 rows)

Таблица со счётчиками присутствия:

@sv=# create view counts
    as select time, tag, sum(tag) over (rows unbounded preceding) as count from events;
CREATE VIEW
@sv=# select * from counts;
        time         | tag | count 
---------------------+-----+-------
 2020-01-24 09:30:00 |  -1 |    -1
 2020-01-24 11:00:00 |   1 |     0
 2020-01-24 12:00:00 |  -1 |    -1
 2020-01-24 12:30:00 |  -1 |    -2
 2020-01-24 13:00:00 |   1 |    -1
 2020-01-24 13:30:00 |  -1 |    -2
 2020-01-24 14:00:00 |   1 |    -1
 2020-01-24 17:00:00 |   1 |     0
 2020-01-25 10:00:00 |  -1 |    -1
 2020-01-25 15:00:00 |   1 |     0
(10 rows)

Фильтр "первый пришёл"/"последний ушёл". Обратите внимание как расположены ярлыки. Знаки чередуются: -1, 1, -1, 1, -1, 1. Это не случайно:

@sv=# select * from counts
    where (tag = -1 and count = -1) or (tag = 1 and count = 0);
        time         | tag | count 
---------------------+-----+-------
 2020-01-24 09:30:00 |  -1 |    -1
 2020-01-24 11:00:00 |   1 |     0
 2020-01-24 12:00:00 |  -1 |    -1
 2020-01-24 17:00:00 |   1 |     0
 2020-01-25 10:00:00 |  -1 |    -1
 2020-01-25 15:00:00 |   1 |     0
(6 rows)

Время присутствия на работе. Времена умножаются на ярлычки прихода/ухода. Теперь ясно зачем приход обозначался как -1, а уход как 1. Результат получается в секундах, делим его 3600:

@sv=# select sum((extract(epoch from time)) * tag) / 3600 from counts
    where (tag = -1 and count = -1) or (tag = 1 and count = 0);
 ?column?  
----------
     11.5
(1 row)
→ Ссылка
Автор решения: Daniil Loban

Мне стало интересно, можно ли решить этот вопрос не сильно усложняя sql запрос. В итоге пришла идея ? не без минусов конечно, но работающая.

Минусы этой идеи в том что:

  • понадобится дополнительная таблица в базе
  • от количества записей в этой таблице зависит точность и диапазон
Диапазон Точность в минутах Кол-во записей
c 9:00 до 17:30 30 минут 17
с 0:00 до 23:55 5 минут 288
с 0:00 до 23:59 1 минута 1440

иллюстрация запроса в инфографике

На этой картинке я попытался изобразить как учитываются полу часы. на примере отвлеченных данных (не из таблицы ниже)

Алгоритм:

  • Сначала выбираются диапазоны которые либо содержат интервалы присутсвия на работе либо частично в них включены. таким образом на каждые пол часа есть статистика сколько человек присутствует.
  • После чего мы выбираем только полу часы присутствия exist т.е. те что > 0
  • Остается подсчитать их количество COUNT(in_time.exist) и умножить на 0.5 что является обозначением полу часа.

Ниже идет пример дополнительной таблицы интервалов (9:00 - 17:30 / 30 мин) :

CREATE TABLE intervals (
    id int,
    start time,
    end TIME
);

INSERT INTO intervals VALUES (0, '09:00:00', '09:30:00');
INSERT INTO intervals VALUES (0, '09:30:00', '10:00:00'); 
INSERT INTO intervals VALUES (1, '10:00:00', '10:30:00'); 
INSERT INTO intervals VALUES (2, '10:30:00', '11:00:00'); 
INSERT INTO intervals VALUES (3, '11:00:00', '11:30:00'); 
INSERT INTO intervals VALUES (4, '11:30:00', '12:00:00'); 
INSERT INTO intervals VALUES (5, '12:00:00', '12:30:00'); 
INSERT INTO intervals VALUES (6, '12:30:00', '13:00:00'); 
INSERT INTO intervals VALUES (7, '13:00:00', '13:30:00'); 
INSERT INTO intervals VALUES (8, '13:30:00', '14:00:00'); 
INSERT INTO intervals VALUES (9, '14:00:00', '14:30:00'); 
INSERT INTO intervals VALUES (10, '14:30:00', '15:00:00'); 
INSERT INTO intervals VALUES (11, '15:00:00', '15:30:00'); 
INSERT INTO intervals VALUES (12, '15:30:00', '16:00:00'); 
INSERT INTO intervals VALUES (13, '16:00:00', '16:30:00'); 
INSERT INTO intervals VALUES (14, '16:30:00', '17:00:00');
INSERT INTO intervals VALUES (15, '17:00:00', '17:30:00');

Далее таблица нашего журнала (2 колонки времени - имена не важны):

CREATE TABLE journal (
    id int,
    start time,
    end TIME
);

INSERT INTO journal VALUES(0, '9:30:00', '11:30:00');
INSERT INTO journal VALUES(1, '12:00:00', '13:00:00');
INSERT INTO journal VALUES(2, '12:30:00', '17:00:00');
INSERT INTO journal VALUES(3, '13:30:00', '14:00:00');
INSERT INTO journal VALUES(4, '10:00:00', '15:00:00');

Ну и под конец самое интересное - запрос, в данном случае мы получаем 7.5 часов.

SELECT COUNT(in_time.exist) * 0.5 as result_hours 
FROM (
    SELECT start as s, end as e, 
      (
        SELECT COUNT(start) as counter 
        FROM journal 
        WHERE 
          ( journal.start = s OR journal.end = e ) 
          OR 
          ( journal.start < s AND journal.end > e )
      ) exist 
    FROM intervals HAVING exist > 0
) as in_time
result_hours
7.5
→ Ссылка