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

Postgresql 选择将具有范围的行拆分为具有较小范围的多行的选项?,postgresql,Postgresql,我有两个表,其中包含分类的tsrange值。每个表中的范围在每个类别中不重叠,但b中的范围可能与a中的范围重叠 我想做的是将这两个表组合成一个CTE,用于另一个查询。组合值需要是表a中的tsranges减去表b中具有相同类别的任何重叠tsranges 复杂的是,如果重叠的b.周期包含在a.周期内,则减法的结果是两行。Postgres Range-运算符不支持此操作,因此我创建了一个返回1或2行的函数: create function subtract_tsrange( a tsrange , b

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

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

复杂的是,如果重叠的b.周期包含在a.周期内,则减法的结果是两行。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中的一行可能会被拆分为许多周期较短的行

现在,我想创建一个select,它接受a中的每一行并返回:

如果没有相同类别的重叠b.期间,则为原始a.期间 或

1行或多行表示原始a.句点减去具有相同类别的所有重叠b.句点。 在阅读了很多其他帖子后,我想我应该以某种方式将selectlateral与我的函数结合起来使用,但我仍然不知道该如何使用??我们在谈论博士后9.6 btw

注意:您的问题很容易推广到每种范围类型,因此我将在回答中使用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_范围调用

此外,我将使用numrange类型进行测试,因为它比tsrange类型更容易创建和检查,但我的答案也应该适用于此

答复:


我重写了您的函数来处理任何类型的边界,包括、排除甚至无界范围。此外,当一个伟大的答案是广义答案时,它将返回一个空结果集!当我开始理解这个问题时的第一个问题是:range_div中最不合适的“{}”是什么?我不明白这意味着什么,我假设这不是一个打字错误,因为你似乎真的运行了这个code@JesperWe“{}”实际上是一个空数组,它是一个未知类型的文本,但是它的类型将被强制为适当范围的数组类型-但是这很难用它来表示,所以我使用了未知文本。是一个集合返回函数,它只是一个接一个地生成所有元素的结果,但是在结果集中,而不是在数组中。我喜欢这个解决方案,我觉得它非常简洁。它也有用!
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$;
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$;
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)