SQL:从任意间隔计算月平均值

时间:2018-08-24 19:31:49

标签: sql postgresql query-optimization

我有一个日志表,以

的形式存储事件
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

有一个主键(为简便起见不包括在内),时间戳是datetime字段,object_id是与不同表的前键关系,状态是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

其中,平均状态是根据对象在间隔中(在上述情况下,一天中)以一分钟为间隔在每个给定状态中花费的时间量加权得出的,因此,如果对象在状态10中花费了23小时,在状态50下15分钟,平均值应为

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 :(得分:0)

一种计算该平均值的方法是实际使用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";

db<>fiddle

结果:

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()将分钟数缩短为几天,并按它们和对象分组。