Rob*_*Rob 4 postgresql window-functions postgresql-9.3 range-types
基本上我的问题是:如何在 PostgreSQL 9.3(或 9.4)中进行涉及重叠范围的聚合操作?我手头的具体问题是,给定一个范围,我想找到适用重叠范围的最大 sum()。一个简单的例子:
create table event (
event_id int primary key,
event_type_id int not null,
period tstzrange not null,
quantity int not null
);
insert into event (event_id, event_type_id, period, quantity) values
(1, 1,'[2016-01-06 09:00:00+00,2016-01-08 17:00:00+00]',1),
(2, 1,'[2016-01-07 09:00:00+00,2016-01-07 11:00:00+00]',1),
(3, 1,'[2016-01-07 13:00:00+00,2016-01-07 17:00:00+00]',1),
(4, 2,'[2016-01-07 12:00:00+00,2016-01-07 17:00:00+00]',1);
Run Code Online (Sandbox Code Playgroud)
给定具有以下子句的查询:
select ...
where event_type_id = 1
and period && '[2016-01-07 00:00:00+00,2016-01-07 23:59:00+00]'::tstzrange
group by event_type_id
Run Code Online (Sandbox Code Playgroud)
期望的结果是:3,即在给定时间戳范围内sum(quantity)
相同范围event_type_id
重叠的最大值。
从表中选择period
与给定时间范围重叠的行。确定该集合内重叠时段的不同范围,并返回sum(quantity)
任何范围内的最大值。
需要 Postgres 9.2 +,因为旧版本中没有范围类型。
“重叠”是指层叠的方式,就像传统屋顶上的瓷砖:那些是“重叠”(防雨),尽管最高的瓷砖不直接与最低的瓷砖重叠。
中的所有值period
都有包含边界 ( []
)。否则,您必须调整排他性边界。(输入参数的范围仍然可以有任意边界。)
我们只过滤一个 event_type_id
。否则,您必须添加PARTITION BY event_type_id
到窗口定义中。
quantity
是一个integer
。否则,您必须根据计算类型进行调整。
重叠期间的数量会被完全计算在内,即使该期间的某些部分超出了您的给定时间范围。
甚至适用于(event_type_id, period)
.
这应该是炸药。
SELECT running_sum - lag(running_sum, 1, 0) OVER (ORDER BY p_start) AS sum_quantity
FROM (
SELECT lower(period) AS p_start
,(sum(quantity) OVER w)::int AS running_sum
, lead(lower(period), 1, 'infinity') OVER w
> max(upper(period)) OVER w AS range_end
FROM event
WHERE event_type_id = 1
AND period && '[2016-01-01 0:0+0,2016-01-10 0:0+0]'::tstzrange
WINDOW w AS (ORDER BY lower(period))
) sub
WHERE range_end
ORDER BY 1 DESC
LIMIT 1;
Run Code Online (Sandbox Code Playgroud)
子查询中的所有三个窗口函数都可以使用同一个窗口。这避免了额外的排序操作,应该是最快的。
相同的查询,只是更冗长和更慢,因为 CTE 实现了派生表并构成优化障碍。
WITH cte1 AS (
SELECT quantity
, lower(period) AS p_start
, upper(period) AS p_end
FROM event
WHERE event_type_id = 1
AND period && '[2016-01-01 0:0+0,2016-01-10 0:0+0]'::tstzrange
)
, cte2 AS (
SELECT (sum(quantity) OVER w)::int AS running_sum
, lead(p_start, 1, 'infinity') OVER w -- next start ..
> max(p_end) OVER w AS range_end -- .. after last end
, p_start, p_end
FROM cte1
WINDOW w AS (ORDER BY p_start)
)
SELECT running_sum - lag(running_sum, 1, 0) OVER (ORDER BY p_start) AS sum_quantity
-- subtract the previous sum to get the sum of this range
, p_end::text
FROM cte2
WHERE range_end -- only rows at the end of each range
ORDER BY 1 DESC -- biggest sum first
LIMIT 1; -- only return the winner
Run Code Online (Sandbox Code Playgroud)
sqlfiddle for Postgres 9.3
db<>fiddle here for Postgres 12
您需要一个索引才能在大表中快速运行。最好的选择将是一个GiST的索引上(event_type_id, period)
。细节:
过滤符合您条件的行,然后按时间范围 ( lower(period)
)的开始排序并计算:
running_sum
)。lead(lower(period), 1, 'infinity')
)。最后一行默认为“无穷大”以包含最后一个范围。max(upper(period))
。如果2.晚于3.它是(子)范围 ( range_end
)的结尾。
在外部SELECT
过滤器行中,使用range_end
和 减去先前的总数以获得范围的总和。ORDER BY
该结果并返回第一个 ( LIMIT 1
) 最大的sum_quantity
。瞧。
要选择 2016 年 1 月 7 日的所有内容,干净的表达式是:
'[2016-01-07 00:00:00+00,2016-01-08 00:00:00+00)'::tstzrange
Run Code Online (Sandbox Code Playgroud)
不是:
'[2016-01-07 00:00:00+00,2016-01-07 23:59:00+00]'::tstzrange
Run Code Online (Sandbox Code Playgroud)
细节:
由于时间戳值的默认精度为6张十进制数(微秒分辨率),你可以同时使用:
'[2016-01-07 00:00:00+00,2016-01-07 23:59:59.999999+00]'::tstzrange
Run Code Online (Sandbox Code Playgroud)
但这很麻烦,并且取决于可能会更改的实现细节(即使不太可能)。这是不是要进行舍入误差,因为时间戳都存储在现代Postgres的整数值:
归档时间: |
|
查看次数: |
3489 次 |
最近记录: |