我正在尝试创建一个PL/PGSQL触发器函数来检查新行的日期范围,以确保表中没有其他行的日期范围重叠(对于相同的product_id).我已经成功地创建了函数并将其设置为BEFORE INSERT触发器,但我正试图弄清楚如何也将其设置为BEFORE UPDATE触发器,因为触发器内部的SELECT语句肯定会抛出异常,因为它符合重叠更新版本的日期的标准。
下面是我的函数:
CREATE OR REPLACE FUNCTION check_specials_dates()
RETURNS trigger AS
$$
DECLARE
BEGIN
IF EXISTS (SELECT * FROM rar.product_specials
WHERE product_id = NEW.product_id
AND (
(NEW.end_time between start_time and end_time) OR
(NEW.start_time between start_time and end_time) OR
(start_time between NEW.start_time and NEW.end_time))
THEN
RAISE EXCEPTION
'Cannot insert overlapping specials date for Product ID#%', NEW.product_id;
END IF;
RETURN NEW;
END
$$ LANGUAGE plpgsql;
我的想法是,IF EXISTS SELECT语句将返回一个匹配,因为它将匹配它试图更新的行。
正确吗?如果是这样,我该如何避开呢?
您使用的是哪个版本的PostgreSQL ?从9.0开始,您可以使用排除约束和cube/btree_gist扩展来实现所有这些。
要使用触发器实现这一点,我通常会使用after insert/update触发器,该触发器查看具有相同产品ID但具有插入/更新行不同主键的其他特殊值。即:
IF EXISTS (SELECT 1 FROM rar.product_specials
WHERE product_specials.product_id = NEW.product_id
AND product_specials.product_special_id <> NEW.product_special_id
AND overlaps(NEW.start_time, NEW.end_time,
product_specials.start_time, product_specials.end_time))
如果您还没有为product_specials
生成主键,我想这将证明添加一个主键是合理的。
排除约束解
(因为我一直需要提醒自己怎么做,所以我想把它写下来)
(需要注意的是,如果你的开始/结束时间是离散的(例如日期或者你可以将你的端点固定到足够大的粒度),那么你可以在一个由触发器填充的辅助表上使用唯一性约束,而不是PostgreSQL,触发器和并发来强制一个临时键)
PostgreSQL可以使用其广泛的操作符/索引方法基础设施来强制通用的排除约束——如果任何其他行满足一组操作,则拒绝接受该行。传统的唯一性约束本质上是这种情况的一种特殊情况——如果该行中的某些值/值集与其他行的值/值集相等,则会导致该行被拒绝。在您的示例中,如果与表中的其他行相比,product_id相等并且范围(start_time,end_time)重叠,则希望拒绝一行。
索引方法"gist"可以用来建立索引来满足这类请求(特别是重叠范围)。扩展"cube"提供了一种通用的数据类型,它是gist可索引的,而"btree_gist"为整数提供了一个gist索引方法,允许这两种类型在单个索引中组合。
所以在PostgreSQL 9.1中:
CREATE EXTENSION cube;
CREATE EXTENSION btree_gist;
(在9.0中,从contrib运行脚本)
下面是我测试的例子:
create table product_specials(product_special_id serial primary key,
product_id int not null,
start_time timestamp not null, end_time timestamp not null);
insert into product_specials(product_id, start_time, end_time)
values(1, '2011-10-31 15:00:00', '2011-11-01 09:00:00'),
(2, '2011-10-31 12:00:00', '2011-11-01 12:00:00'),
(1, '2011-11-01 15:00:00', '2011-11-02 09:00:00');
现在,这些范围不重叠所以我们可以添加约束:
alter table product_specials add constraint overlapping_times exclude using gist (
product_id with = ,
cube(extract(epoch from start_time), extract(epoch from end_time)) with &&
);
cube(n1, n2)
创建了一个从n1到n2的一维"立方体"。extract(epoch from t)
将时间戳t转换为数字。如果有两个立方体,如果它们重叠,"&&"操作符返回true。因此,这将为每一行索引product_id和start_time/end_time"多维数据集",并且每次插入/更新一行时,通过查找与新行值匹配的现有行来测试约束:使用"="操作符测试product_id,使用"&&"操作符测试start_time/end_time"多维数据集"。
如果您现在尝试插入冲突行,您将得到一个错误:
insert into product_specials(product_id, start_time, end_time)
values(2, '2011-10-31 00:00:00', '2011-10-31 13:00:00');
ERROR: conflicting key value violates exclusion constraint "overlapping_times"
DETAIL: Key (product_id, cube(date_part('epoch'::text, start_time), date_part('epoch'::text, end_time)))=(2, (1320019200),(1320066000)) conflicts with existing key (product_id, cube(date_part('epoch'::text, start_time), date_part('epoch'::text, end_time)))=(2, (1320062400),(1320148800)).
如您所见,错误消息细节的易读性还有待改进!(@a_horse_with_no_name提到的文章http://thoughts.j-davis.com/2010/09/25/exclusion-constraints-are-generalized-sql-unique/中的"period"类型可能会产生更好的类型)但是,功能是完整的。
使用约束排除解决了一些我还没有解决的与锁有关的小问题。严格地说,在触发器中的"IF EXISTS…"查询之前,您应该执行SELECT 1 FROM rar.product_specials WHERE product_specials.product_id = NEW.product_id FOR SHARE
,以确保在检查的约束与其事务提交之间,您正在测试的其他行都不会发生变化。然而,当同时插入两个新的特殊值时,仍然存在潜在的竞争条件,因为没有什么可锁定的——这就是使用辅助表来排除离散值的动机,但是随着排除空间变得更细粒度,这会产生缩放问题。
对于PostgreSQL 9.2,将有一个"范围"数据类型,这将消除使用多维数据集扩展或类似的需要。range类型还允许适当地指定边界在两端是打开还是关闭,而使用立方体边界总是在两端关闭(因此您需要做一些调整以避免关于日期范围重叠的错误)。与往常一样,Depesz在这个特性上有一个很好的帖子:http://www.depesz.com/index.php/2011/11/07/waiting-for-9-2-range-data-types/
例如:create table product_specials(product_special_id serial primary key,
product_id int not null,
applicable_dates tsrange not null);
insert into product_specials(product_id, applicable_dates)
values(1, tsrange('2011-10-31 15:00:00', '2011-11-01 09:00:00')),
(2, tsrange('2011-10-31 12:00:00', '2011-11-01 12:00:00')),
(1, tsrange('2011-11-01 15:00:00', '2011-11-02 09:00:00'));
alter table product_specials add exclude using gist (
product_id with =,
applicable_dates with &&
);
现在,如果您尝试插入一个冲突的行,您也会得到一个更可读的错误消息:
insert into product_specials(product_id, applicable_dates)
values(2, tsrange('2011-10-31 00:00:00', '2011-10-31 13:00:00'));
ERROR: conflicting key value violates exclusion constraint "product_specials_product_id_applicable_dates_excl"
DETAIL: Key (product_id, applicable_dates)=(2, ["2011-10-31 00:00:00","2011-10-31 13:00:00")) conflicts with existing key (product_id, applicable_dates)=(2, ["2011-10-31 12:00:00","2011-11-01 12:00:00")).
注意,您不必更改表的模式来使用这个新类型,因为您可以为函数调用的结果建立索引。因此,使用范围类型来强制约束的细节不必放在应用程序或触发器中。即:
alter table product_specials add exclude using gist (
product_id with =,
tsrange(start_time, end_time) with &&
);