重叠范围的最大 sum()

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重叠的最大值。

Erw*_*ter 7

我所理解的任务

从表中选择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 变体

相同的查询,只是更冗长和更慢,因为 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))的开始排序并计算:

  1. 数量的连续总和 ( running_sum)。
  2. 下一个时期的开始:(lead(lower(period), 1, 'infinity'))。最后一行默认为“无穷大”以包含最后一个范围。
  3. 到目前为止任何期间的最晚结束时间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的整数值: