Распределение времени в интервалах - PullRequest
3 голосов
/ 22 марта 2019

У меня есть следующая таблица:

CREATE TABLE f_contact (
    agent character varying,
    datetimeconnect timestamp without time zone,
    datetimedisconnect timestamp without time zone,
    duration integer
);

duration - это время в секундах между временем отключения и временем соединения.
У меня могут быть такие данные:

agent   datetimeconnect         datetimedisconnect      duration
20024   2019-03-18 12:01:00.0   2019-03-18 13:01:30.0   3630
20011   2019-03-11 08:47:40.0   2019-03-11 09:30:10.0   2550

И я хочу взять эти данные и распределить время по 15-минутным интервалам, чтобы у меня был такой результат:

20024   12:00   840
20024   12:15   900
20024   12:30   900
20024   12:45   900 
20024   13:00   90
20011   08:45   740
20011   09:00   900
20011   09:15   900
20011   09:30   10

Как этого добиться?

Ответы [ 3 ]

2 голосов
/ 22 марта 2019

Это интересная проблема. Я немного упростил именование столбцов, чтобы сделать:

with t as (
      select 20024 as agent, '2019-03-18 12:01:00.0'::timestamp as conn, '2019-03-18 13:01:30.0'::timestamp as disconn, 3630 duration union all
      select 20011, '2019-03-11 08:47:40.0', '2019-03-11 09:30:10.0', 2550
     )
select gs.t, t.*,
       extract(epoch from least(gs.t + interval '15 minute', disconn) - greatest(gs.t, conn))
from t cross join lateral
     generate_series(date_trunc('hour', t.conn), date_trunc('hour', t.disconn) + interval '1 hour', interval '15 minute') gs(t)
where conn <= gs.t + interval '15 minute' and disconn >= gs.t ;

Здесь - это дБ <> скрипка.

Я понимаю, что столбец продолжительности не нужен. Вы пытаетесь захватить секунды перекрытия с 15-минутными интервалами.

Это создает интервалы на часовых границах - это просто проще. Это означает, что некоторые из перекрытий некорректны, что и отфильтровывает предложение where.

1 голос
/ 22 марта 2019

Быстрее всего сразу преобразовать в секунды и рассчитать с целыми числами:

SELECT agent
     , to_char(to_timestamp(q) AT TIME ZONE 'UTC', 'HH24:MI') AS quarter_hour
     , least(q + 900, b) - greatest(a, q) AS seconds
FROM   (
   SELECT agent
        , extract(epoch FROM datetimeconnect)::int    AS a
        , extract(epoch FROM datetimedisconnect)::int AS b
   FROM   f_contact
   )  f, generate_series(a / 900 * 900, b, 900) q
ORDER  BY agent DESC, q;

Дает желаемый результат.

db <> fiddle здесь (с угловыми кейсами, добавленными к контрольному кейсу)

Ключевые моменты

  • extract(epoch FROM datetimeconnect) выдержки (цитирование руководство )...

    количество секунд с 1970-01-01 00:00:00 UTC

  • generate_series() (целочисленный вариант) генерирует ровно столько строк, сколько нужно, без излишков.Точнее, нижняя граница каждого соответствующего четверть часа - значение, отображаемое в результате.

  • a / 900 * 900 использует целочисленное деление для округления в меньшую сторонудо полных четверти часа (кратно 900).Используя это, поскольку date_trunc() не имеет возможности усекать до четверти часа

  • least(q + 900, b) - greatest(a, q) гарантирует, что начало и конец подсчитаны правильно, даже если оба лежат в пределахтот же четверть часа (как показано в расширенном тестовом примере в скрипке).

  • to_timestamp(q) AT TIME ZONE 'UTC', поскольку to_timestamp() возвращает timestamptz, мы хотим получить timestamp в UTCиз него.

В стороне: duration является функционально зависимым значением.Это не нужно для задачи и не должно храниться в таблице с избыточностью.Это просто балласт, замедляющий все.Это можно вычислить на лету дешево.

0 голосов
/ 22 марта 2019

В Postgres вы можете использовать generate_series() для генерации серии данных.Я бы начал с генерации серии чисел, а затем JOIN с исходными данными, чтобы создать 15-минутные интервалы.Внутренний запрос может использоваться для предварительного вычисления начальных и конечных границ.

Рассмотрим следующий запрос, демонстрирующий логику округления меток времени до 15 минут и JOIN таблицы с сериями:

    SELECT *
    FROM generate_series(0, 99, 1) t(x)
    INNER JOIN (
        SELECT 
            f.*,
            DATE_TRUNC('hour', datetimeconnect)    
                + DATE_PART('minute', datetimeconnect   )::int / 15 * interval '15 min' connect_15min,
            DATE_TRUNC('hour', datetimedisconnect) 
                + DATE_PART('minute', datetimedisconnect)::int / 15 * interval '15 min' disconnect_15min
        FROM f_contact f
    ) c 
        ON c.disconnect_15min >= c.connect_15min + ((t.x * 15) || ' minute')::interval 
    ORDER BY c.datetimeconnect, t.x;

Например, для agent = 2011 это вернет:

| x   | agent | datetimeconnect          | datetimedisconnect       | duration | connect_15min            | disconnect_15min         |
| --- | ----- | ------------------------ | ------------------------ | -------- | ------------------------ | ------------------------ |
| 0   | 20011 | 2019-03-11T08:47:40.000Z | 2019-03-11T09:30:10.000Z | 2550     | 2019-03-11T08:45:00.000Z | 2019-03-11T09:30:00.000Z |
| 1   | 20011 | 2019-03-11T08:47:40.000Z | 2019-03-11T09:30:10.000Z | 2550     | 2019-03-11T08:45:00.000Z | 2019-03-11T09:30:00.000Z |
| 2   | 20011 | 2019-03-11T08:47:40.000Z | 2019-03-11T09:30:10.000Z | 2550     | 2019-03-11T08:45:00.000Z | 2019-03-11T09:30:00.000Z |
| 3   | 20011 | 2019-03-11T08:47:40.000Z | 2019-03-11T09:30:10.000Z | 2550     | 2019-03-11T08:45:00.000Z | 2019-03-11T09:30:00.000Z |

Теперь мы можем вычислить продолжительность в предложении FROM.Хитрость заключается в том, чтобы правильно обрабатывать первый и последний интервалы, используя LEAST() и GREATEST() (обратите внимание, что duration не используется для вычисления):

SELECT 
    agent,
    c.connect_15min + ( t.x * 15 || ' minute' )::interval interval_start_15min,

    EXTRACT(EPOCH FROM (
        LEAST(datetimedisconnect, c.connect_15min + ( (t.x + 1) * 15 || ' minute' )::interval) 
        - GREATEST(datetimeconnect, c.connect_15min + ( t.x * 15 || ' minute' )::interval )
    )) duration
FROM generate_series(0, 99, 1) t(x)
INNER JOIN (
    SELECT 
        f.*,
        DATE_TRUNC('hour', datetimeconnect)    
            + DATE_PART('minute', datetimeconnect   )::int / 15 * interval '15 min' connect_15min,
        DATE_TRUNC('hour', datetimedisconnect) 
            + DATE_PART('minute', datetimedisconnect)::int / 15 * interval '15 min' disconnect_15min
    FROM f_contact f
) c 
    ON c.disconnect_15min >= c.connect_15min + ((t.x * 15) || ' minute')::interval 
ORDER BY agent, interval_start_15min;

Это демо на DB Fiddle возвращает:

| agent | interval_start_15min     | duration |
| ----- | ------------------------ | -------- |
| 20011 | 2019-03-11T08:45:00.000Z | 740      |
| 20011 | 2019-03-11T09:00:00.000Z | 900      |
| 20011 | 2019-03-11T09:15:00.000Z | 900      |
| 20011 | 2019-03-11T09:30:00.000Z | 10       |
| 20024 | 2019-03-18T12:00:00.000Z | 840      |
| 20024 | 2019-03-18T12:15:00.000Z | 900      |
| 20024 | 2019-03-18T12:30:00.000Z | 900      |
| 20024 | 2019-03-18T12:45:00.000Z | 900      |
| 20024 | 2019-03-18T13:00:00.000Z | 90       |
...