SQL: рассчитать среднемесячные значения по произвольным интервалам - PullRequest
0 голосов
/ 24 августа 2018

У меня есть таблица журнала, в которой хранятся события в виде

timestamp,        object_id, state
2018-08-12 13:45  123        10
2018-08-13 15:56  183        25
2018-08-13 15:58  123        10
2018-08-15 16:02  256        15

Существует первичный ключ (не включенный для краткости), отметка времени - это поле даты и времени, object_id - это отношение ключа foregn к разностной таблице, а состояние - целое число в диапазоне 0-100. События записываются по мере их поступления, и состояние не обязательно меняется между событиями, поэтому один и тот же object_id может иметь несколько последовательных записей с одним и тем же состоянием.

База данных PostgreSQL 9.5

То, что я пытаюсь сделать, это вычислить среднее состояние для месячных, дневных и недельных интервалов для отдельных объектов или объектов, выбранных по некоторым критериям. Результаты, которые я ожидаю получить для среднесуточных значений, должны выглядеть примерно так:

date,        object_id, average state
2018-08-12   123        18.6
2018-08-13   123        37.1
2018-08-14   123        126.7
2018-08-15   123        5.5

где среднее состояние вычисляется взвешенным по количеству времени, которое объект провел в каждом данном состоянии в течение интервала (в случае выше, в течение одного дня) с интервалами в одну минуту, так что если объект проводит 23 часа в состоянии 10, но 15 минут в состоянии 50, среднее значение должно составлять

15/1440 * 50 + 1425/1440 * 10 = 10.42

До сих пор мне удавалось использовать оконные функции для преобразования отдельных событий в интервалы между изменениями состояния. SQL выглядит примерно так

SELECT
    state.object_id,
    state.timestamp as start, 
    lead(timestamp) OVER (ORDER BY timestamp) as end,
    state.state, 
FROM 
(
    SELECT 
        *, 
        rank() OVER (PARTITION BY (state) ORDER BY timestamp)
    FROM event_log AS l
    WHERE object_id=123 AND timestamp >= DATE '2018-01-01'
) AS state
WHERE state.rank=1
ORDER BY timestamp

и получите вывод, который дает мне начало и конец интервалов, когда состояние действительно меняется. Я не уверен, куда идти отсюда. События не всегда происходят часто, поэтому у меня может быть интервал, который длится три дня, и мне как-то нужно сообщать об этом изо дня в день, поэтому мне нужно разделить этот интервал на дни. Как мне поступить правильно?

1 Ответ

0 голосов
/ 25 августа 2018

Что ж, одним из способов расчета этого среднего значения будет фактическое развертывание всех минут с помощью generate_series(), присвоение им состояния с помощью подзапроса, а затем GROUP BY ID и день.

SELECT date_trunc('day',
                  "gs"."timestamp") "date",
       "x1"."object_id",
       avg((SELECT "el1"."state"
                   FROM "event_log" "el1"
                   WHERE "el1"."object_id" = "x1"."object_id"
                         AND "el1"."timestamp" <= "gs"."timestamp"
                   ORDER BY "el1"."timestamp" DESC
                   LIMIT 1)) "state"
       FROM (SELECT "el1"."object_id",
                    min(date_trunc('minute',
                                   "el1"."timestamp")) "timestamp_begin",
                    max(date_trunc('minute',
                                   "el1"."timestamp")) "timestamp_end"
                    FROM "event_log" "el1"
                    GROUP BY "el1"."object_id") "x1"
             CROSS JOIN LATERAL generate_series("x1"."timestamp_begin",
                                                "x1"."timestamp_end",
                                                '1 minute'::interval) "gs"("timestamp")
       GROUP BY date_trunc('day',
                           "gs"."timestamp"),
                "x1"."object_id"
       ORDER BY date_trunc('day',
                           "gs"."timestamp"),
                "x1"."object_id";

дб <> скрипка

Результат:

date                | object_id |               state
:------------------ | --------: | ------------------:
2018-08-12 00:00:00 |       123 | 10.0000000000000000
2018-08-13 00:00:00 |       123 | 10.0000000000000000
2018-08-13 00:00:00 |       183 | 25.0000000000000000
2018-08-15 00:00:00 |       256 | 15.0000000000000000

Идея состоит в том, чтобы генерировать все минуты между первой и последней отметкой времени объекта. И присвойте самое последнее известное состояние минуте, которая была записана до или в эту минуту.

Если у нас есть каждая минута и состояние, это более или менее простой запрос агрегации для получения средних значений за день и объект.

Сначала мы получаем первую и последнюю отметку времени с точностью до минуты для каждого объекта с псевдонимом подзапроса "x1". Чтобы урезать временные метки до минутной точности, мы используем date_trunc().

Мы боковым крестом соединяем "x1" с generate_series() и кормим его в первую и последнюю минуту. Это сгенерирует мелкие отметки времени от первого до последнего.

Теперь в подзапросе в вызове avg() мы выбираем все строки, где объект совпадает с текущей строкой во внешнем запросе, а метка времени меньше или равна одной из текущей строки. Но мы хотим только самые последние из них. Поэтому мы сортируем их по метке времени в порядке убывания, выбирая только первую из отсортированных.

Мы снова используем date_trunc(), чтобы обрезать минуты до дней и группировать их по объекту.

...