我有两个包含分类的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顺便说一句!)
答案 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)