Какую версию PostgreSQL вы используете? Начиная с 9.0 вы можете реализовать все это, используя ограничение исключения и расширения cube / btree_gist.
Чтобы реализовать это с помощью триггера, я обычно использовал бы триггер после вставки / обновления, который просматривал другие специальные предложения с тем же идентификатором продукта, но с другим первичным ключом для вставляемой / обновляемой строки. То есть:
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
, imho это оправдывает добавление одного.
Решение для ограничения исключений
(потому что мне постоянно нужно напоминать себе, как это сделать, поэтому я хочу где-то записать)
(Обратите внимание: если время начала / окончания дискретно (например, даты или вы можете зафиксировать конечные точки на достаточно большой степени детализации), вы можете вместо этого использовать ограничение уникальности для вспомогательной таблицы, заполненной триггерами: 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)).
Как видите, четкость деталей сообщения об ошибке оставляет желать лучшего! (Тип «period» из статьи http://thoughts.j -davis.com / 2010/09/25 / exclusion-constraints-are-generalized-sql-unique / , которую @a_horse_with_no_name, по-видимому, приводит к получению лучших) Тем не менее, функциональность не повреждена.
Использование исключения ограничений решает некоторые сложные проблемы с блокировками, которые я не рассмотрел. Строго говоря, перед вашим запросом «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 &&
);