Postgres date overlapping constraint

前端 未结 4 1124
鱼传尺愫
鱼传尺愫 2020-12-14 08:22

I have a table like this:

date_start    date_end     account_id    product_id
2001-01-01    2001-01-31   1             1
2001-02-01    2001-02-20   1                 


        
相关标签:
4条回答
  • 2020-12-14 08:32

    This is a difficult problem because constraints can only reference the "current row", and may not contain subqueries. (otherwise the trivial solution would be to add some NOT EXISTS() subquery in the check)

    A check constraint specified as a column constraint should reference that column's value only, while an expression appearing in a table constraint can reference multiple columns.

    Currently, CHECK expressions cannot contain subqueries nor refer to variables other than columns of the current row.

    Popular work-arounds are: use a trigger function which does the dirty work (or use the rule system, which is deprecated by most people)

    Because most people favor triggers, I'll repost a rule-system hack here... (it does not have the extra "id" key element, but that's a minor detail)

    -- Implementation of A CONSTRAINT on non-overlapping datetime ranges
    -- , using the Postgres rulesystem.
    -- We need a shadow-table for the ranges only to avoid recursion in the rulesystem.
    -- This shadow table has a canary variable with a CONSTRAINT (value=0) on it
    -- , and on changes to the basetable (that overlap with an existing interval)
    -- an attempt is made to modify this variable. (which of course fails)
    
    -- CREATE SCHEMA tmp;
    DROP table tmp.dates_shadow CASCADE;
    CREATE table tmp.dates_shadow
        ( time_begin timestamp with time zone
        , time_end timestamp with time zone
        , overlap_canary INTEGER NOT NULL DEFAULT '0' CHECK (overlap_canary=0)
        )
        ;
    ALTER table tmp.dates_shadow
        ADD PRIMARY KEY (time_begin,time_end)
        ;
    
    DROP table tmp.dates CASCADE;
    CREATE table tmp.dates
        ( time_begin timestamp with time zone
        , time_end timestamp with time zone
        , payload varchar
        )
        ;
    
    ALTER table tmp.dates
        ADD PRIMARY KEY (time_begin,time_end)
        ;
    
    CREATE RULE dates_i AS
        ON INSERT TO tmp.dates
        DO ALSO (
        -- verify shadow
        UPDATE tmp.dates_shadow ds
            SET overlap_canary= 1
            WHERE (ds.time_begin, ds.time_end) OVERLAPS ( NEW.time_begin, NEW.time_end)
            ;
        -- insert shadow
        INSERT INTO tmp.dates_shadow (time_begin,time_end)
            VALUES (NEW.time_begin, NEW.time_end)
            ;
        );
    
    
    CREATE RULE dates_d AS
        ON DELETE TO tmp.dates
        DO ALSO (
        DELETE FROM tmp.dates_shadow ds
            WHERE ds.time_begin = OLD.time_begin
            AND ds.time_end = OLD.time_end
            ;
        );
    
    CREATE RULE dates_u AS
        ON UPDATE TO tmp.dates
        WHERE NEW.time_begin <> OLD.time_begin
        AND NEW.time_end <> OLD.time_end
        DO ALSO (
        -- delete shadow
        DELETE FROM tmp.dates_shadow ds
            WHERE ds.time_begin = OLD.time_begin
            AND ds.time_end = OLD.time_end
            ;
        -- verify shadow
        UPDATE tmp.dates_shadow ds
            SET overlap_canary= 1
            WHERE (ds.time_begin, ds.time_end) OVERLAPS ( NEW.time_begin, NEW.time_end)
            ;
        -- insert shadow
        INSERT INTO tmp.dates_shadow (time_begin,time_end)
            VALUES (NEW.time_begin, NEW.time_end)
            ;
        );
    
    
    INSERT INTO tmp.dates(time_begin,time_end) VALUES
      ('2011-09-01', '2011-09-10')
    , ('2011-09-10', '2011-09-20')
    , ('2011-09-20', '2011-09-30')
        ;
    SELECT * FROM tmp.dates;
    
    
    EXPLAIN ANALYZE
    INSERT INTO tmp.dates(time_begin,time_end) VALUES ('2011-09-30', '2011-10-04')
        ;
    
    INSERT INTO tmp.dates(time_begin,time_end) VALUES ('2011-09-02', '2011-09-04')
        ;
    
    SELECT * FROM tmp.dates;
    SELECT * FROM tmp.dates_shadow;
    
                                                                                                                          
    
    0 讨论(0)
  • 2020-12-14 08:37

    How to create unique constraint to a group of columns :

     CREATE TABLE table (
        date_start date,
        date_end  date,
        account_id integer,
        UNIQUE (account_id , date_start ,date_end) );
    

    in your case you will need to ALTER TABLE if the table already exists, check the documentation it would be helpful for you :
    - DDL Constraints
    - ALTER Table

    0 讨论(0)
  • 2020-12-14 08:47

    In up to date postgres versions (I tested it in 9.6 but I assume it's working in >=9.2) you can use the build in function tstzrange() as mentioned in some other comments. Null values will be treated as positive or negative infinity by default and the CHECK contraint is then not explicitly needed anymore (if you are fine that the check is only <= and a range can start and end with the same date). Only the extension btree_gist is still needed:

    CREATE EXTENSION btree_gist;
    
    CREATE TABLE test (
        from_ts TIMESTAMPTZ,
        to_ts TIMESTAMPTZ,
        account_id INTEGER DEFAULT 1,
        product_id INTEGER DEFAULT 1,
        CONSTRAINT overlapping_times EXCLUDE USING GIST (
            account_id WITH =,
            product_id WITH =,
            TSTZRANGE(from_ts, to_ts) WITH &&
        )
    );
    
    0 讨论(0)
  • 2020-12-14 08:59

    Ok i ended up doing this :

    CREATE TABLE test (
        from_ts TIMESTAMPTZ,
        to_ts TIMESTAMPTZ,
        account_id INTEGER DEFAULT 1,
        product_id INTEGER DEFAULT 1,
        CHECK ( from_ts < to_ts ),
        CONSTRAINT overlapping_times EXCLUDE USING GIST (
            account_id WITH =,
            product_id WITH =,
            period(from_ts, CASE WHEN to_ts IS NULL THEN 'infinity' ELSE to_ts END) WITH &&
        )
    );
    

    Works perfectly with infinity, transaction proof.

    I just had to install temporal extension which is going to be native in postgres 9.2 and btree_gist available as an extension in 9.1 CREATE EXTENSION btree_gist;

    nb : if you don't have null timestamp there is no need to use the temporal extension you could go with the box method as specified in my question.

    0 讨论(0)
提交回复
热议问题