如何在PostgreSQL中将状态日志数据聚合成具有相同状态的时间间隔?

Posted

技术标签:

【中文标题】如何在PostgreSQL中将状态日志数据聚合成具有相同状态的时间间隔?【英文标题】:How to aggregate state log data into time intervals with the same state in PostgreSQL? 【发布时间】:2018-08-28 18:20:39 【问题描述】:

我有一个看起来像这样的对象状态日志

timestamp, object_id, state, level
2018-01-01 123        f      100 
2018-01-02 123        t      100    
2018-01-02 123        f      100
2018-01-03 123        f      100
2018-01-03 123        f      100
2018-01-06 123        t      90
2018-01-07 123        t      90
2018-01-08 123        f      90

时间戳实际上是一个完整的日期/时间,为简洁起见,我没有包含时间部分。

我想要的是一个基于独特状态和级别的状态转换列表,看起来像这样

start      end        object_id, state, level
2018-01-01 2018-01-02 123        f      100 
2018-01-02 2018-01-02 123        t      100
2018-01-02 2018-01-06 123        f      100
2018-01-06 2018-01-08 123        t      90
2018-01-08 NOW()      123        f      90

我试图想出一种方法来使用窗口函数来做到这一点,比如

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

但我想我不明白 rank() 是如何工作的,它不能满足我的需要。出于某种原因,我认为 rank() 会在每次分区更改时重置行数,但事实并非如此。我怎样才能做到这一点?

【问题讨论】:

你的时间戳有时间分量吗?如果不是,则排序不稳定,这使您的问题无法解决(除非另一列指定了排序)。 是的,它是一个完整的时间戳。没有包含它,因为它使我的帖子过于庞大。 编辑帖子以包含此信息 我不确定要求,但可能有助于您开始:dbfiddle.uk/… 【参考方案1】:

这是一个孤岛问题。一个不错的解决方案使用row_number():

select object_id, level, state, min(timestamp), max(timestamp)
from (select t.*,
             row_number() over (partition by object_id, level order by timestamp) as seqnum,
             row_number() over (partition by object_id, level, state order by timestamp) as seqnum_2
      from t
     ) t
group by (seqnum - seqnum_2), object_id, level, state;

很难解释为什么会这样。但是如果你看一下子查询的结果,你会发现在状态不变的情况下,两个seqnums 的差值是不变的。这定义了您想要的分组——连同其他列——所以剩下的只是聚合。

这是一个rextester,表明它正在工作。

【讨论】:

让我试试我的数据 不幸的是,它不起作用。返回结果,但在第一行之后,min和max报告相同的时间戳 但是谢谢你的“差距和岛屿”这个名字,至少现在我知道用谷歌搜索什么了 :) @MadWombat 。 . .当我对您问题中的示例数据进行尝试时,它工作正常。最后一行有点不同——我不太确定第二个日期是如何变成“now()”的,但这似乎很小。 那是样本数据。真实数据具有多个对象 ID 和许多不同的状态+级别组合。当我在预期结果中写 now() 时,我的意思是如果对象没有下一个状态,则假设它仍处于上一个状态,并将上一个间隔的结束设置为当前时间。【参考方案2】:

这不是关于“差距和岛屿”。此技术由具有某些字段的相应常数值的组操作,但您需要使用此类组的边界进行操作。所以:

create table state_log(timestamp timestamp, object_id int, state boolean, level int);
insert into state_log values
  ('2018-01-01 00:00:01', 123, 'f', 100),
  ('2018-01-02 00:00:02', 123, 't', 100),
  ('2018-01-02 00:00:03', 123, 'f', 100),
  ('2018-01-03 00:00:04', 123, 'f', 100),
  ('2018-01-03 00:00:05', 123, 'f', 100),
  ('2018-01-06 00:00:06', 123, 't', 90),
  ('2018-01-07 00:00:07', 123, 't', 90),
  ('2018-01-08 00:00:08', 123, 'f', 90);

select
  timestamp::date as start,
  coalesce(lead(timestamp) over (order by timestamp), now()::timestamp)::date as end,
  object_id, state, level
from (
  select 
    *,
    coalesce(lag(state) over (order by timestamp) <> state, true) as is_new_group
  from state_log) as t
where
  object_id = 123 and timestamp >= date '2018-01-01' and
  is_new_group
order by timestamp;

结果(我删除了时间部分以使其更像您在问题中指定的结果):

┌────────────┬────────────┬──────────┬───────┬─── ────┐ │ start │ end │ object_id │ state │ level │ ├────────────┼────────────┼────────────┼────────┼─── ────┤ │ 2018-01-01 │ 2018-01-02 │ 123 │ f │ 100 │ │ 2018-01-02 │ 2018-01-02 │ 123 │ t │ 100 │ │ 2018-01-02 │ 2018-01-06 │ 123 │ f │ 100 │ │ 2018-01-06 │ 2018-01-08 │ 123 │ t │ 90 │ │ 2018-01-08 │ 2018-08-30 │ 123 │ f │ 90 │ └────────────┴────────────┴────────────┴────────┴─── ────┘

【讨论】:

以上是关于如何在PostgreSQL中将状态日志数据聚合成具有相同状态的时间间隔?的主要内容,如果未能解决你的问题,请参考以下文章

如何从 SQL 中的活动日志计算“活动用户”聚合?

PostgreSQL - 如何使用外键聚合数据?

如何在windows的“omnidb”中将csv文件数据导入postgresql

如何在python flask app中将数据从postgresql渲染到csv?

玩 framework 2.0 演进,如何在 PROD 中将不一致的状态标记为已解决

如何在 PostgreSQL / pgAdmin III 中将 bytea 数据打印为十六进制字符串?