滑动1小时周期聚合查询

Sliding 1-hour periods aggregation query

在 Postgres 9.2 中,我有一个 table 包含在特定时间点采取的措施:

CREATE TABLE measures (dt timestamptz, measure integer);

INSERT INTO measures VALUES
('2015-01-13 12:05', 10), 
('2015-01-13 12:30', 8), 
('2015-01-13 13:02', 16), 
('2015-01-13 13:30', 12), 
('2015-01-13 14:15', 7);

我想计算 1 小时内的平均行数和行数,我可以这样做:

SELECT date_trunc('hour', dt) as d, max(measure), count(*)
FROM measures group by d order by d;

但不是从 12:00、13:00 等开始的 1 小时时段。我想要一个事件后的 1 小时时段。在这种情况下,这是从 12:05 到 13:05 的一个时间段,下一个从 13:30 到 14:30 的时间段。

这在 PostgreSQL 中可行吗?

如果你能找到一个函数是 postgresql,它将一个小时添加到日期时间,那么你应该能够根据内部查询中的日期和日期 + 1 小时将结果集自身加入,然后将值聚合起来在外部查询中获取您需要的结果。

SELECT
    LowDate,
    HighDate=DATEADD(HOUR,1,LowDate),
    SumMeasure=SUM(measure),
    ItemCount=COUNT(*)
FROM
(
    SELECT
        LowDate=M1.dt,  
        measure=M2.measure
    FROM
        measures M1 
        INNER JOIN measures M2 ON M2.dt BETWEEN M1.dt AND DATEADD(HOUR,1,M1.dt)
)AS DETAIL  
GROUP BY
    LowDate 
ORDER BY
    LowDate

递归 CTE

普通 SQL 与 recursive CTE 作品:

WITH RECURSIVE cte AS (
   SELECT t.dt, m.measure
   FROM  (SELECT dt FROM measures ORDER BY 1 LIMIT 1) t -- no lower bound
   JOIN   measures m ON m.dt < t.dt + interval '1h'  -- excl. upper bound

   UNION ALL
   SELECT t.dt, m.measure
   FROM  (
      SELECT m.dt
      FROM  (SELECT dt FROM cte LIMIT 1) c
      JOIN   measures m ON m.dt >= c.dt + interval '1h'
      ORDER  BY 1
      LIMIT  1
      ) t
   JOIN   measures m ON m.dt >= t.dt                 -- incl. lower bound
                    AND m.dt <  t.dt + interval '1h' -- excl. upper bound
   )
SELECT dt AS hour_start
     , round(avg(measure), 2) AS avg_measure, count(*) AS ct
FROM   cte
GROUP  BY 1
ORDER  BY 1;

Returns:

hour_start          | avg_measure | ct
--------------------+-------------+----
2015-01-13 13:05:00 | 11.33       | 3
2015-01-13 14:30:00 | 9.50        | 2

db<>fiddle here(添加了对大 table 的测试,其中包含索引和选定的时间范围)
sqlfiddle

它在 dt 上的索引表现不错 - 或者更好的是 multicolumn index to allow index-only scans 在 Postgres 9.2+:

CREATE INDEX measures_foo_idx ON measures (dt, measure);

这也是标准 SQL including the recursive CTE except for LIMIT. Postgres supports the standard keywords FETCH FIRST,如果您需要它,所有标准 SQL。

Window函数?

无法使用单个 window 函数

虽然 window 函数的结果是 window 框架的聚合,但框架定义本身不能引用其他行。在您的情况下,粒度是通过从头到尾考虑 all 行动态确定的。单个 window 函数无法做到这一点。

但是!

我们仍然可以使用 window frame with the RANGE clause bounded by an interval 获得 每行 的滚动小时平均值 - 需要 Postgres 11 或更高版本。

SELECT *, avg(measure) OVER (ORDER BY dt
                             RANGE BETWEEN CURRENT ROW AND '1 hour' FOLLOWING)
FROM   measures;

以低廉的成本为每一行生成聚合。然后我们需要动态过滤新周期的每个开始。我们可以使用行数并在每个小时内向前跳过行数 - PL/pgSQL cursor 自然适合任务:

CREATE OR REPLACE FUNCTION f_dynamic_hourly_avg()
  RETURNS TABLE(hour_start timestamp, avg_measure numeric, ct int)
  LANGUAGE plpgsql AS
$func$
DECLARE
    _cursor CURSOR FOR
      SELECT dt, round(avg(measure) OVER w, 2), count(*) OVER w 
      FROM   measures
      WINDOW w AS (ORDER BY dt RANGE BETWEEN CURRENT ROW AND '1 hour' FOLLOWING);
BEGIN
    OPEN _cursor;
    FETCH _cursor INTO hour_start, avg_measure, ct;
    WHILE FOUND
    LOOP
      RETURN NEXT;
      FETCH RELATIVE ct FROM _cursor INTO hour_start, avg_measure, ct;
    END LOOP;
END
$func$;

致电:

SELECT * FROM f_dynamic_hourly_avg();

事实证明非常有效只有几个 每个时期的行数。它会随着每个周期的 many 行而下降。很难确定一个数字。结果是 1000 倍快 在快速基准测试中每个周期 < 10 行。

db<>fiddle here

我们甚至可以使用 dynamic cursor 并传递 table 和列名以使其适用于任何 table ...

优化性能

您基本上需要遍历所有行,使用过程解决方案可以更快:plpgsql 函数 中的 FOR 循环。哪个会更快?

  • 几个小时的递归查询,每个很多行
  • 许多小时的函数,每个几行
  • 更新: 添加的函数将光标放在带有 window 函数的查询上,远远超过其余函数(虽然每个周期没有太多行?)

相关PL/pgSQL解决方案:

  • Group by repeating attribute
  • GROUP BY and aggregate sequential numeric values
  • SQL Query where I get most recent rows from timestamp from another table