如何在 Postgres 中获取时间间隔的平均值

Posted

技术标签:

【中文标题】如何在 Postgres 中获取时间间隔的平均值【英文标题】:How to get average values for time intervals in Postgres 【发布时间】:2018-10-20 01:25:57 【问题描述】:

我使用的是 PostgreSQL 9.6。我有一张这样的桌子:

mac   sn         loc   time     date      vin1    vin2    vin3
1a34 4as11111111 aaaa  7:06:18  1/1/2018  447.42  472.32  682.59
1a34 4as11111111 aaaa  7:06:43  1/1/2018  455.97  476.25  682.59
1a34 4as11111111 aaaa  7:07:35  1/1/2018  470.88  484.2   682.5

我需要在 300 秒(5 分钟)的时间间隔内计算 vin1vin2vin3 的平均值。例如,对于范围内的日期,从第一次 (7:06:18 - 7:11:18) 开始。我可以通过这个查询选择我需要的数据:

select * from table
where sn='4as11111111' and date between '2018-01-01' and '2018-01-02';

但我不知道如何按 300 秒的时间间隔对其进行分组,并计算这 5 分钟间隔的 vin1vin2vin3 列的平均值,以获得如下结果:

mac  sn          loc     time     date      vin1_av  vin2_av  vin3_av
1a34 4as11111111 aaaa   7:06:18  1/1/2018  450.0    480.32   600.59
1a34 4as11111111 aaaa   7:11:18  1/1/2018  460.0    490.25   782.59
1a34 4as11111111 aaaa   7:16:18  1/1/2018  470.88   500.2    600.5

任何帮助将不胜感激。

【问题讨论】:

没有任何现有行的时间间隔怎么办?包括结果0NULL或根本没有行?范围可以超过一天吗?此外,您的示例数据和结果不匹配。查询也没有(不同的日期)。 所有行都保证有数据,可能有0.0V但不能为NULL。可能有没有数据的时隙,你是对的,所以这些时隙应该被忽略。跨度可以是几周、几个月。抱歉查询日期(我已修复),它们与我想要的结果不匹配,我只是复制了一个实验查询作为示例 谢谢欧文。我的真实表有第一列“mac”(请参阅​​编辑后的问题,对不起,我没有将它包含在原始帖子中,认为这并不重要),并且它更改了查询,因此我无法使其工作使用您的解决方案。我觉得它是正确的,但仍然无法弄清楚变化。 看起来macloc 对于同一个sn 总是相同的。是这样吗?如果不是,那将如何反映预期的结果?我根据有根据的猜测更新了解决方案。 @Erwin:不,mac 和 loc 可能不同,但这并不重要,因为我最感兴趣的是 sn 选择。很奇怪,我在 dbfiddle (dbfiddle.uk/…) 上使用了您的查询,它与示例数据完美配合,但是当我用真实数据尝试它时(就像现在在 dbfiddle 中一样),“没有返回行”(参见 dbfiddle关联)。在我的本地数据库上,我得到这个“错误:在目标列表中找不到 ORDER/GROUP BY 表达式”。所以,我又错过了一些简单的东西。 【参考方案1】:

数据库设计

虽然您可以使用单独的 datetime 列,但与单个 timestamp 列相比确实没有优势。我会适应:

ALTER TABLE tbl ADD column ts timestamp;
UPDATE tbl SET ts = date + time;  -- assuming actual date and time types
ALTER TABLE tbl DROP column date, DROP column time;

如果日期和时间不是实际的 datetime 数据类型,请使用 to_timestamp()。相关:

Calculating Cumulative Sum in PostgreSQL How to convert "string" to "timestamp without time zone"

查询

那么查询就简单一点了:

SELECT *
FROM  (
   SELECT sn, generate_series(min(ts), max(ts), interval '5 min') AS ts
   FROM   tbl
   WHERE  sn = '4as11111111'
   AND    ts >= '2018-01-01'
   AND    ts <  '2018-01-02'
   GROUP  BY 1
   ) grid
CROSS  JOIN LATERAL (
   SELECT round(avg(vin1), 2) AS vin1_av
        , round(avg(vin2), 2) AS vin2_av
        , round(avg(vin3), 2) AS vin3_av
   FROM   tbl
   WHERE  sn =  grid.sn
   AND    ts >= grid.ts
   AND    ts <  grid.ts + interval '5 min'
   ) avg;

db小提琴here

在第一个子查询grid 中生成一个开始时间网格,从给定时间范围内的第一行到最后一个限定行。

使用LATERAL 连接加入每个分区中的行,并立即聚合子查询avg 中的平均值。由于聚合,它总是返回一行,即使没有找到条目。在这种情况下,平均值默认为 NULL

结果包括给定时间范围内第一个和最后一个合格行之间的所有时间段。其他各种结果组合也很有意义。就像在给定的时间范围内包含 所有 个时隙,或者只包含具有实际值的时隙。一切可能,我必须选择一种解释。

索引

至少有这个多列索引:

CRATE INDEX foo_idx ON tbl (sn, ts);

或在(sn, ts, vin1, vin2, vin3) 上允许仅索引扫描 - 如果满足某些先决条件,特别是如果表行比演示中的行宽得多。

密切相关:

Slow LEFT JOIN on CTE with time intervals Best way to count records by arbitrary time intervals in Rails+Postgres

基于您的原始表格

As requested and clarified in the comment,后来在问题中再次更新以包含macloc 列。我假设您想要每个 (mac, loc) 的单独平均值。

datetime 仍然是单独的列,vin* 列是float 类型,并且排除没有行的时隙:

更新后的查询还将 set-returning 函数 generate_series() 移动到 FROM 列表中,这在 Postgres 10 之前更清晰:

SELECT t.mac, sn.sn, t.loc, ts.ts::time AS time, ts.ts::date AS date
     , t.vin1_av, t.vin2_av, t.vin3_av
FROM  (SELECT text '4as11111111') sn(sn)  -- provide sn here once
CROSS  JOIN LATERAL (
   SELECT min(date+time) AS min_ts, max(date+time) AS max_ts
   FROM   tbl
   WHERE  sn = sn.sn
   AND    date+time >= '2018-01-01 0:0'   -- provide time frame here
   AND    date+time <  '2018-01-02 0:0'
   ) grid
CROSS  JOIN LATERAL generate_series(min_ts, max_ts, interval '5 min') ts(ts)
CROSS  JOIN LATERAL (
   SELECT mac, loc
        , round(avg(vin1)::numeric, 2) AS vin1_av  -- cast to numeric for round()
        , round(avg(vin2)::numeric, 2) AS vin2_av  -- but rounding is optional
        , round(avg(vin3)::numeric, 2) AS vin3_av
   FROM   tbl
   WHERE  sn = sn.sn
   AND    date+time >= ts.ts
   AND    date+time <  ts.ts + interval '5 min'
   GROUP  BY mac, loc
   HAVING count(*) > 0  -- exclude empty slots
   ) t;

创建一个多列表达式索引来支持这一点:

CRATE INDEX bar_idx ON tbl (sn, (date+time));

db小提琴here

但我更愿意一直使用timestamp

【讨论】:

非常感谢,欧文。是否可以对现有数据库使用单独的日期和时间列执行我想要的操作? sn_idx 和 date_idx 有索引,因为这些查询经常发生,但 vin1 ets。都是浮动的,没有索引。你是对的,我不需要一个充满 NULL 的空时隙(就像在一天中的某些时候它可能没有实际值)。 @lugger1:我添加了另一个解决方案。 谢谢欧文。当我在 postgres 中尝试此操作时,由于某种原因,我收到“错误:在 targetlist 中找不到 ORDER/GROUP BY 表达式”。我错过了什么吗? @lugger1:我用你的 9.6 版进行了测试,它可以工作。查看添加的小提琴。 它工作正常,但现在我收到“错误:在目标列表中找不到 ORDER/GROUP BY 表达式”错误。我发现查询类似于“select sn, generate_series(min(date+time ), max(date+time), interval '5 min') AS ts from tbl where sn = '4as11111111' and date+time >= '2018-01-01 0:0' and date+time

以上是关于如何在 Postgres 中获取时间间隔的平均值的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 postgres 将时间间隔转换为小时数?

如何从postgres的间隔中提取年份

如何在postgres中对几个月的日期记录分组后填补时间间隔

SQL按时间戳间隔获取平均值

获取 Postgres 中每个值与整体最小值的差异

在 Postgres 中,如何平均每个用户的最新 5 个分数?