【问题标题】:Postgres exclude using gist across different tablesPostgres 排除在不同表中使用 gist
【发布时间】:2020-12-09 09:30:20
【问题描述】:

我有两张这样的桌子

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');

每个表都包含与用户相关的行,同一用户的所有行不能在范围内重叠。另外,有些行可能会被逻辑删除,所以我加了一个where条件。 到目前为止,它的工作没有问题,但 2 个约束分别适用于每个表。

我需要创建一个涵盖 2 组表的约束,以便单个日期范围(同一用户且未删除)只能在 2 个不同的表中出现一次。

EXCLUDE 符号是否可以扩展以适用于不同的表,或者我是否需要使用触发器对其进行检查?如果触发器是答案,那么执行此操作的更简单方法是什么?用2的并集创建一个临时表,在上面加上约束,检查是否失败?

【问题讨论】:

    标签: postgresql exclude-constraint


    【解决方案1】:

    从@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
    

    【讨论】:

      【解决方案2】:

      您可能可以使用触发器,但触发器是 always vulnerable to race conditions(除非您使用的是 SERIALIZABLE 隔离)。

      如果您的表确实有相同的列,为什么不使用单个表(也许添加一个type 列来消除歧义)?

      【讨论】:

      • 这是我实际情况的简化示例,实际表大约有 10-20 个,每个表不同,这就是为什么我有 2 个不同的表。唯一共同的部分是不能重叠的日期开始/结束。在触发器方面,除了竞态条件问题,您对如何实现排除约束有什么建议吗?
      • 好吧,每当INSERTUPDATE 出现在任一表上时,请选择另一个表中的重叠条目,如果有则抛出错误。
      • 好的,我明白了。是否有一个已经内置的函数可以用来检查是否有重叠 我需要手动逐行检查我正在处理的记录的日期吗?
      • 使用您在排除约束中使用的&& 运算符。
      • 是的,但是我会写两个函数,表名是硬编码的,所以我会得到更好的性能。
      猜你喜欢
      • 1970-01-01
      • 2020-11-17
      • 2015-04-17
      • 1970-01-01
      • 2013-12-23
      • 2021-04-24
      • 2021-07-21
      • 1970-01-01
      • 2019-09-07
      相关资源
      最近更新 更多