简体   繁体   English

Postgres 排除在不同表中使用 gist

[英]Postgres exclude using gist across different tables

I have 2 tables like this我有两张这样的桌子

drop table if exists public.table_1;
drop table if exists public.table_2;

CREATE TABLE public.table_1 (
    id serial NOT NULL,
    user_id bigint not null,
    status varchar(255) not null,
    date_start date NOT NULL,
    date_end date NULL
);

CREATE TABLE public.table_2 (
    id serial NOT NULL,
    user_id bigint not null,
    status varchar(255) not null,
    date_start date NOT NULL,
    date_end date NULL
);


alter table public.table_1
add constraint my_constraint_1 
EXCLUDE USING gist (user_id with =, daterange(date_start, date_end, '[]') WITH &&)
where (status != 'deleted');

alter table public.table_2 
add constraint my_constraint_2 
EXCLUDE USING gist (user_id with =, daterange(date_start, date_end, '[]') WITH &&)
where (status != 'deleted');

Every table contains rows which are related to a user, and all the rows of the same user cannot overlap in range.每个表都包含与用户相关的行,并且同一用户的所有行不能在范围内重叠。 In addition, some rows may be logically deleted, so I added a where condition.另外,有些行可能会被逻辑删除,所以我加了一个where条件。 So far it's working w/o problems, but the 2 constraints work separately for each table.到目前为止,它的工作没有问题,但是每个表的 2 个约束分别工作。

I need to create a constraint which cover the 2 set of tables, so that a single daterange (of the same user and not deleted), may appaer only once across the 2 different tables.我需要创建一个涵盖 2 组表的约束,以便单个日期范围(同一用户且未删除)可能仅在 2 个不同表中出现一次。

Does the EXCLUDE notation be extended to work with different tables or do I need to check it with a trigger? EXCLUDE表示法是否被扩展以适用于不同的表,还是我需要用触发器检查它? If the trigger is the answer, which is the simplier way to do this?如果触发器是答案,那么执行此操作的更简单方法是什么? Create a temporary table with the union of the 2, add the constraint on it and check if fails?使用 2 的并集创建一个临时表,在其上添加约束并检查是否失败?

You can probably use a trigger, but triggers are always vulnerable to race conditions (unless you are using SERIALIZABLE isolation).您可能可以使用触发器,但触发器总是容易受到竞争条件的影响(除非您使用SERIALIZABLE隔离)。

If your tables really have the same columns, why don't you use a single table (and perhaps add a type column to disambiguate)?如果您的表确实具有相同的列,为什么不使用单个表(并且可能添加一个type列来消除歧义)?

Starting from @Laurenz Albe suggestion, this is what I made从@Laurenz Albe 的建议开始,这就是我所做的

-- #################### SETUP SAMPLE TABLES ####################

drop table if exists public.table_1;
drop table if exists public.table_2;

CREATE TABLE public.table_1 (
    id serial NOT NULL,
    user_id bigint not null,
    status varchar(255) not null,
    date_start date NOT NULL,
    date_end date NULL
);

CREATE TABLE public.table_2 (
    id serial NOT NULL,
    user_id bigint not null,
    status varchar(255) not null,
    date_start date NOT NULL,
    date_end date NULL
);


alter table public.table_1
add constraint my_constraint_1 
EXCLUDE USING gist (user_id with =, daterange(date_start, date_end, '[]') WITH &&)
where (status != 'deleted');

alter table public.table_2 
add constraint my_constraint_2 
EXCLUDE USING gist (user_id with =, daterange(date_start, date_end, '[]') WITH &&)
where (status != 'deleted');

-- #################### SETUP TRIGGER ####################

create or REPLACE FUNCTION check_date_overlap_trigger_hook()
RETURNS trigger as
$body$
DECLARE
    l_table text;
    l_sql text;
    l_row record;
begin
    l_table := TG_ARGV[0];

    l_sql := format('
        select *
        from public.%s as t
        where
            t.user_id = %s -- Include only records of the same user
            and t.status != ''deleted'' -- Include only records that are active
    ', l_table, new.user_id);

    for l_row in execute l_sql       
    loop
        
        IF daterange(l_row.date_start, COALESCE(l_row.date_end, 'infinity'::date)) && daterange(new.date_start, COALESCE(new.date_end, 'infinity'::date))
        THEN
            RAISE EXCEPTION 'Date interval is overlapping with another one in table %', l_table
                USING HINT = 'You can''t have the same interval across table1 AND table2';
        END IF;
    
    end loop;
   
    RETURN NEW;
end
$body$
LANGUAGE plpgsql;

-- #################### INSTALL TRIGGER ####################

create trigger check_date_overlap
BEFORE insert or update
ON public.table_1 
FOR EACH row
EXECUTE PROCEDURE check_date_overlap_trigger_hook('table_2');

create trigger check_date_overlap
BEFORE insert or update
ON public.table_2
FOR EACH row
EXECUTE PROCEDURE check_date_overlap_trigger_hook('table_1');


-- #################### INSERT DEMO ROWS ####################

insert into public.table_1 (user_id, status, date_start, date_end) values (1, 'active', '2020-12-10', '2020-12-20');
insert into public.table_1 (user_id, status, date_start, date_end) values (1, 'deleted', '2020-12-15', '2020-12-25');
insert into public.table_1 (user_id, status, date_start, date_end) values (2, 'active', '2020-12-10', '2020-12-20');
insert into public.table_1 (user_id, status, date_start, date_end) values (2, 'deleted', '2020-12-15', '2020-12-25');


-- This will fail for overlap on the same table
-- insert into public.table_1 (user_id, status, date_start, date_end) values (1, 'active', '2020-12-15', '2020-12-25');

-- This will fail as the user 1 already has an overlapping period on table 1
-- insert into public.table_2 (user_id, status, date_start, date_end) values (1, 'active', '2020-12-15', '2020-12-25');

-- This will fail as the user 1 already has an overlapping period on table 1
insert into public.table_2 (user_id, status, date_start, date_end) values (1, 'deleted', '2020-12-15', '2020-12-25');
update public.table_2 set status = 'active' where id = 1;


select 'table_1' as src_table, * from public.table_1
union
select 'table_2', * from public.table_2

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM