35

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             1
2001-04-01    2001-05-20   1             1

I want to disallow overlapping intervals a given (account_id, product_id)

5
  • 2
    You want to disallow overlapping intervals? Commented May 16, 2012 at 10:12
  • yep exactly it's the word i was looking for Commented May 16, 2012 at 10:20
  • You should go for a CREATE CONSTRAINT TRIGGER and write a specific function to perform your check. Commented May 16, 2012 at 11:03
  • The map-the-interval-into-2D-geometry hack has been posted here, too. (the rounding to float stinks) But honestly: creating a trigger (or a rule+canary table, like in my contribution) is the way to go. You can even catch the end_date IS NULL case with that. And the compares are exact. NOTE: don't use rules, unless you know what you are doing ;-] Commented May 17, 2012 at 21:01
  • 1
    I thought there was a problem with transactions and trigger, the triggers are not aware about other transactions running at the same time. Commented May 18, 2012 at 7:26

4 Answers 4

32

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.

Sign up to request clarification or add additional context in comments.

6 Comments

+1 Perfect use case for an exclusion constraint (new in Postgres 9.0). Instead of the CASE statement you could define the column to_ts TIMESTAMPTZ NOT NULL DEFAULT 'infinity'. And the same with '-infinity' for from_ts.
With the 9.2 release the range type has been added including daterange which simplifies your constaint and this whole problem domain. postgresql.org/docs/devel/static/rangetypes.html
You could use COALESCE(to_ts, 'infinity'::timestamptz) instead of CASE WHEN to make it less UPPERCASE WORD SALAD
I haven't tried it, but it looks like this would work in 9.2 without having to install the temporal extension by replacing the period() function with tstzrange().
Even better, 9.2's range types treat NULL as +/- Infinity. So instead of the CASE or even COALESCE, you get to use to_ts directly.
|
12

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

3 Comments

TSTZRANGE range type in postgresql is not immutable (because it depends on the timezone of the PostgreSQL server) and postgresql wouldn't let me create a constraint using a non immutable function. If my usecase it was ok to only compare dates without timezone so I could use either DATERANGE or TSRANGE
@PaulB. that was probably on some newer version? I remember back then 3 years ago I just did the above successfully...
Hi @mineralf, actually I checked again and my initial table had DATE times (so with no timezone information). In that case, trying to add a constraint using the TSTZRANGE function fails with this error: functions in index expression must be marked IMMUTABLE. TL;DR: - if your column type is DATE/TIMESTAMP use TSRANGE as function in the constraint - if your column type is TIMESTAMPTZ use TSTZRANGE as function in the constraint
0

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;

                                                                                                                      

1 Comment

I think it's possible to do something with excluding constraints. Not sure though, have a look at my edit.
-4

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

2 Comments

pg-8.1 is quite an old version.
hmm it's not really what i'm looking for. I'm looking for something to disallow overlaping intervals. but thanks

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.