我有一张表,描述了在不同时间在机器上安装了哪些软件版本:
machine_id::integer, version::text, datefrom::timestamp, dateto::timestamp
我想做一个约束以确保没有日期范围重叠,即不可能同时在一台机器上安装多个软件版本。
这如何在 SQL 中实现?我正在使用 PostgreSQL v8.4。
我有一张表,描述了在不同时间在机器上安装了哪些软件版本:
machine_id::integer, version::text, datefrom::timestamp, dateto::timestamp
我想做一个约束以确保没有日期范围重叠,即不可能同时在一台机器上安装多个软件版本。
这如何在 SQL 中实现?我正在使用 PostgreSQL v8.4。
In PostgreSQL 8.4 this can only be solved with triggers. The trigger will have to check on insert/update that no conflicting rows exist. Because transaction serializability doesn't implement predicate locking you'll have to do the necessary locking by yourself. To do that SELECT FOR UPDATE
the row in the machines table so that no other transaction could be concurrently inserting data that might conflict.
In PostgreSQL 9.0 there will be a better solution to this, called exclusion constraints (somewhat documented under CREATE TABLE). That will let you specify a constraint that date ranges must not overlap. Jeff Davis, the author of that feature has a two part write-up on this: part 1, part 2. Depesz also has some code examples describing the feature.
同时(如果我正确阅读了手册,从 9.2 版开始)postgreSQL 增加了对rangetypes的支持。
使用这些范围类型,问题突然变得非常简单(从手册复制的示例):
CREATE TABLE reservation (
during tsrange,
EXCLUDE USING gist (during WITH &&)
);
就是这样。测试(也从手册中复制):
INSERT INTO reservation VALUES
('[2010-01-01 11:30, 2010-01-01 15:00)');
插入 0 1
INSERT INTO reservation VALUES
('[2010-01-01 14:45, 2010-01-01 15:45)');
错误:冲突键值违反排除约束“reservation_during_excl”详细信息:键(期间)=([“2010-01-01 14:45:00”,“2010-01-01 15:45:00”))与现有冲突键(期间)=([“2010-01-01 11:30:00”,“2010-01-01 15:00:00”))。
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE machines(
machine_id integer,
version text,
during tsrange,
EXCLUDE USING gist ( machine_id with =, during with &&)
);
表中具有相同 id 的机器不会重叠。
-- Implementation of a CONSTRAINT on non-overlapping datetime ranges
-- , using the Postgres rulesystem.
-- This mechanism should work for 8.4, without needing triggers.(tested on 9.0)
-- We need a shadow-table for the rangesonly 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;
你真的想要一个 CHECK 约束,就像标题中提到的那样吗?这是不可能的,因为 CHECK 约束一次只能工作一行。不过,可能有一种方法可以使用触发器来做到这一点......
如果由于某种原因您无法更改表架构并且需要保留两个时间行,则可以在约束内构建范围,例如:
CREATE TABLE reservations (
datefrom timestamp,
dateto timestamp,
EXCLUDE USING gist (tsrange(datefrom, dateto) WITH &&)
);
在这种情况下,我使用 tsrange 来处理时间戳类型,但您还可以使用其他类型 - 查看https://www.postgresql.org/docs/current/rangetypes.html上的文档。