【问题标题】:Sliding 1-hour periods aggregation query滑动 1 小时周期聚合查询
【发布时间】:2022-05-12 09:44:24
【问题描述】:

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

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 中可行吗?

【问题讨论】:

  • 有趣的问题。

标签: sql postgresql aggregate postgresql-9.2 recursive-query


【解决方案1】:

递归 CTE

使用recursive CTE 的纯 SQL 可以工作:

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;

返回:

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

dbfiddle here(在带有索引和选定时间范围的大表上添加了测试)
sqlfiddle

它在 dt 上的索引表现不错 - 或者更好的是 multicolumn index 以允许在 Postgres 9.2+ 中使用 index-only scans

CREATE INDEX measures_foo_idx ON measures (dt, measure);

这是标准 SQL including the recursive CTELIMIT 除外。 Postgres 也支持标准关键字FETCH FIRST,如果你需要它所有的标准 SQL。

窗口函数?

单窗口函数无法实现

虽然窗口函数的结果是对窗口框架的聚合,但框架定义本身不能引用其他行。在您的情况下,粒度是通过从第一到最后考虑 all 行来动态确定的。这对于单个窗口函数是不可能的。

但是!

我们仍然可以使用 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();

事实证明这是非常有效,每个周期只有 很少 行。每个周期的行数过多 行。很难确定一个数字。在每个周期 快 1000 倍。

db小提琴here

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

优化性能

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

  • 几小时的递归查询,每行有很多行
  • 很多小时的函数,每行只有几行
  • 更新:将光标悬停在带有窗口函数的查询上的添加函数远胜于其余函数(虽然每个周期没有太多行?)

相关的PL/pgSQL解决方案:

【讨论】:

  • 谢谢你的详细回答,我对递归CTE不熟悉。
  • @zen:考虑更新。稍微改进了查询并添加了一个小提琴。
  • 哇,查询确实得到了改进:在我的测试表中,它需要 160 秒,现在需要 180 毫秒。非常感谢您的跟进。
  • @zen:因子 1000。这就是我喜欢听到的。 :) 我很确定,plpgsql 函数可能会更快。
  • @zen:这个问题很老,但我在一个相关案例中找到了一个更好的解决方案。您可能对更新感兴趣。
【解决方案2】:

如果你能找到一个函数是 postgresql,它在 datetime 上增加了一个小时,那么你应该能够在内部查询中根据日期和日期 + 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

【讨论】:

  • 这很有帮助,但会输出五行:每个输入行一个。我的目标是只输出两行,一行用于 12:05,一行用于 13:30,因为其他事件包含在您的函数的这些 Postgres 代码中以供参考:SELECT LowDate, LowDate + '1 hour' as HighDate, SUM(measure) as SumMeasure, COUNT(*) as ItemCount FROM ( SELECT M1.dt as LowDate, M2.measure as measure FROM measures M1 INNER JOIN measures M2 ON M2.dt BETWEEN M1.dt AND M1.dt + '1 hour' )AS DETAIL GROUP BY LowDate ORDER BY LowDate
  • 我明白了。我明白。嗯。您从中提取数据的结构并不适合您想要的输出的基于集合的良好操作。使用标准 sql 可能是不可能的。我会研究可能的 postgresql 窗口函数。除此之外,您可能必须使用临时存储编写脚本。
  • @zen:请编辑您的问题以阐明您的要求。公众几乎看不到评论。
  • @lrb:(非平凡的)递归 CTE 可以解决问题。我添加了一个答案。
猜你喜欢
  • 2019-01-23
  • 2023-03-23
  • 1970-01-01
  • 1970-01-01
  • 2014-11-26
  • 2021-01-15
  • 2013-12-06
  • 2011-04-05
相关资源
最近更新 更多