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

How to get average values for time intervals in Postgres

我正在使用 PostgreSQL 9.6。我有一个 table 这样的:

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 通过此查询获得我需要的数据:

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

如有任何帮助,我们将不胜感激。

数据库设计

虽然您 可以 使用单独的 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<>fiddle here

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

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

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

索引

至少要有这个多列索引:

CRATE INDEX foo_idx ON tbl (sn, ts);

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

密切相关:

  • Best way to count records by arbitrary time intervals in Rails+Postgres

基于您原来的 table

,后来在问题中再次更新以包含列 macloc。我假设您想要每个 (mac, loc).

的单独平均值

datetime还是分开的列,vin*列是float类型,排除没有行的时隙:

更新后的查询还将集合返回函数 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<>fiddle here

但我更愿意一直使用 timestamp