选择将具有范围的行拆分为多个范围较小的行?

时间:2016-12-21 14:07:30

标签: postgresql

我有两个包含分类的tsrange值的表。每个表格中的范围不是每个类别重叠,但b中的范围可能与a中的范围重叠。

create table a ( id serial primary key, category int, period tsrange );
create table b ( id serial primary key, category int, period tsrange );

我想要做的是将这两个表组合成另一个查询的CTE。合并后的值必须是表a 中的tsranges减去b中具有相同类别的任何重叠tsrange。

复杂的是,在b.period内包含重叠a.period的情况下,减法的结果是两行。 Postgres Range -运算符不支持这个,所以我创建了一个返回1或2行的函数:

create function subtract_tsrange( a tsrange , b tsrange )
returns table (period tsrange)
language 'plpgsql' as $$
begin
    if a @> b and not isempty(b) and lower(a) <> lower(b) and upper(b) <> upper(a)
    then
        period := tsrange(lower(a), lower(b), '[)');
        return next;
        period := tsrange(upper(b), upper(a), '[)');
        return next;
    else
        period :=  a - b;
        return next;
    end if;
    return;
end
$$;

b.period也可能有多个a.period重叠,因此a中的一行可能会被分成很多行,周期较短。

现在我想创建一个选择,它将a中的每一行都带回来并返回:

  • 原始a.period如果没有重叠的b.period具有相同的类别

  • 表示原始a.period的1行或多行,减去具有相同类别的所有重叠b.period

在阅读了很多其他帖子之后,我想我应该将SELECT LATERAL与我的功能结合使用,但我仍然在摸索如何? (我们正在谈论Postgres 9.6顺便说一句!)

1 个答案:

答案 0 :(得分:2)

备注:您的问题很容易推广到每个范围类型,因此我会在答案中使用anyrange伪类型,但您不必这样做。事实上因为这个我必须为范围类型创建一个通用的构造函数,因为PostgreSQL还没有定义它(还):

create or replace function to_range(t anyrange, l anyelement, u anyelement, s text default '[)', out to_range anyrange)
language plpgsql as $func$
begin
  execute format('select %I($1, $2, $3)', pg_typeof(t)) into to_range using l, u, s;
end
$func$;

当然,您可以使用适当的范围构造函数而不是to_range()调用。

此外,我会使用numrange类型进行测试,因为它可以比tsrange类型更轻松地创建和检查,但我的答案也应该适用。

<强>答案

我重写了你的函数来处理任何类型的边界(包含,独占甚至无界范围)。此外,它将在a <@ b时返回空结果集。

create or replace function range_div(a anyrange, b anyrange)
returns setof anyrange
language sql as $func$
  select * from unnest(case
    when b is null or a <@ b then '{}'
    when a @> b then array[
      to_range(a, case when lower_inf(a) then null else lower(a) end,
                  case when lower_inf(b) then null else lower(b) end,
                  case when lower_inc(a) then '[' else '(' end ||
                  case when lower_inc(b) then ')' else ']' end),
      to_range(a, case when upper_inf(b) then null else upper(b) end,
                  case when upper_inf(a) then null else upper(a) end,
                  case when upper_inc(b) then '(' else '[' end ||
                  case when upper_inc(a) then ']' else ')' end)
    ]
    else array[a - b]
  end)
$func$;

考虑到这一点,你需要的是一些聚合反转。 F.ex.使用sum(),可以从空值(0)开始,并不断向其添加一些值。但是你有初始值,你需要不断删除部分部分。

一个解决方案是使用recursive CTEs

with recursive r as (
    select    *
    from      a
  union
    select     r.id, r.category, d
    from       r
    left join  b using (category)
    cross join range_div(r.period, b.period) d -- this is in fact an implicit lateral join
    where      r.period && b.period
)
select    r.*
from      r
left join b on r.category = b.category and r.period && b.period
where     not isempty(r.period) and b.period is null

我的示例数据:

create table a (id serial primary key, category int, period numrange);
create table b (id serial primary key, category int, period numrange);
insert into a (category, period) values (1, '[1,4]'), (1, '[2,5]'), (1, '[3,6]'), (2, '(1,6)');
insert into b (category, period) values (1, '[2,3)'), (1, '[1,2]'), (2, '[3,3]');

上面的查询产生:

id | category | period
 3 |  1       |  [3,6]
 1 |  1       |  [3,4]
 2 |  1       |  [3,5]
 4 |  2       |  (1,3)
 4 |  2       |  (3,6)