滑动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
在 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