Отсроченный уникальный индекс в postgres

14

Глядя на документацию postgres для alter table , кажется, что регулярные ограничения могут быть помечены как DEFERRABLE(более конкретно, INITIALLY DEFERREDэто то, что меня интересует).

Индексы также могут быть связаны с ограничением, если:

Индекс не может иметь ни столбцов выражения, ни частичного индекса

Что заставляет меня верить, что в настоящее время нет способа получить уникальный индекс с условиями, такими как:

CREATE UNIQUE INDEX unique_booking
  ON public.booking
  USING btree
  (check_in, check_out)
  WHERE booking_status = 1;

Это INITIALLY DEFERREDозначает, что «ограничение» уникальности будет проверено только в конце транзакции (если SET CONSTRAINTS ALL DEFERRED;используется).

Правильно ли мое предположение, и если да, есть ли способ достичь предполагаемого поведения?

Благодарность

jcristovao
источник

Ответы:

15

Индекс не может быть отложен - не имеет значения, является ли он UNIQUEчастичным или нет, только UNIQUEограничением. Другие типы ограничений ( FOREIGN KEY, PRIMARY KEY, EXCLUDE) также откладываемые - но не CHECKограничение.

Таким образом, уникальный частичный индекс (и неявное ограничение, которое он реализует) будет проверяться при каждом утверждении (и фактически после каждой вставки / обновления строки в текущей реализации), а не в конце транзакции.


Если вы хотите реализовать это ограничение как отложенное, вы можете добавить еще одну таблицу в проект. Что-то вроде этого:

CREATE TABLE public.booking_status
  ( booking_id int NOT NULL,               -- same types
    check_in timestamp NOT NULL,           -- as in  
    check_out timestamp NOT NULL,          -- booking
    CONSTRAINT unique_booking
        UNIQUE (check_in, check_out)
        DEFERRABLE INITIALLY DEFERRED,
    CONSTRAINT unique_booking_fk
        FOREIGN KEY (booking_id, check_in, check_out)
        REFERENCES public.booking (booking_id, check_in, check_out)
        DEFERRABLE INITIALLY DEFERRED
  ) ;

С этим дизайном и предполагая, что у booking_statusнего есть только 2 возможных варианта (0 и 1), вы можете полностью удалить его из booking(если есть строка в booking_status, это 1, если не 0).


Другим способом было бы (ab) использовать EXCLUDEограничение:

ALTER TABLE booking
    ADD CONSTRAINT unique_booking
        EXCLUDE 
          ( check_in  WITH =, 
            check_out WITH =, 
            (CASE WHEN booking_status = 1 THEN TRUE END) WITH =
          ) 
        DEFERRABLE INITIALLY DEFERRED ;

Проверено на dbfiddle .

Что выше делает:

  • CASEВыражение становится , NULLкогда booking_statusравна нулю или отличается от 1. Мы могли бы написать(CASE WHEN booking_status = 1 THEN TRUE END) , как (booking_status = 1 OR NULL)если это делает любой более ясным.

  • Уникальные и исключающие ограничения принимают строки, в которых одно или несколько выражений равно NULL. Таким образом, он действует как отфильтрованный индекс сWHERE booking_status = 1 .

  • Все WITHоператоры= так действуют как UNIQUEограничение.

  • Эти два вместе делают ограничение действовать как отфильтрованный уникальный индекс.

  • Но это ограничение, и EXCLUDEограничения могут быть отложены.

ypercubeᵀᴹ
источник
2
+1 для ИСКЛЮЧИТЕЛЬНОЙ версии, было то, что мне было нужно. Вот еще один пример, демонстрирующий возможности EXCLUDE: cybertec-postgresql.com/en/postgresql-exclude-beyond-unique
Бенджамин Питер
(CASE WHEN booking_status = 1 THEN TRUE END) WITH =)должно быть заменено на, ) WHERE (booking_status = 1)потому что «Ограничения исключения реализованы с использованием индекса», и этот частичный индекс WHEREбудет меньше и быстрее - postgresql.org/docs/current/sql-createtable.html и postgresql.org/docs/current/sql- createindex.html
Денис Рыжков
1

Хотя годы этого вопроса прошли, я хотел бы уточнить для испаноязычных, тесты были проведены в Postgres:

Следующее ограничение было добавлено в таблицу из 1337 записей, где комплект является первичным ключом:

**Bloque 1**
ALTER TABLE ele_kitscompletos
ADD CONSTRAINT unique_div_nkit
PRIMARY KEY (div_nkit) 

Это создает первичный ключ по умолчанию NOT DEFERRED для таблицы, поэтому при попытке следующего UPDATE мы получаем ошибку:

update ele_kitscompletos
set div_nkit = div_nkit + 1; 

ОШИБКА: дубликат ключа нарушает ограничение уникальности «unique_div_nkit»

В Postgres выполнение UPDATE для каждого ROW проверяет, удовлетворено ли ОГРАНИЧЕНИЕ или ОГРАНИЧЕНИЕ.


CONSTRAINT IMMEDIATE теперь создан, и каждый оператор выполняется отдельно:

ALTER TABLE ele_kitscompletos
ADD CONSTRAINT unique_div_nkit
PRIMARY KEY (div_nkit)
DEFERRABLE INITIALLY IMMEDIATE

**Bloque 2**
BEGIN;   
UPDATE ele_kitscompletos set div_nkit = div_nkit + 1;
INSERT INTO public.ele_kitscompletos(div_nkit, otro_campo)
VALUES 
  (1338, '888150502');
COMMIT;

Запрос в порядке, затронуто 0 строк (время выполнения: 0 мс; общее время: 0 мс) Запрос в порядке, затронуто 1328 строк (время выполнения: 858 мс; общее время: 858 мс) : Ya existe la llave (div_nkit) = (1338).

Здесь SI позволяет изменить первичный ключ, поскольку он выполняет все первое полное предложение (1328 строк); но хотя он находится в транзакции (BEGIN), CONSTRAINT проверяется сразу после завершения каждого предложения без выполнения COMMIT, поэтому выдает ошибку при выполнении INSERT. Наконец, мы создали CONSTRAINT DEFERRED и сделаем следующее:

**Bloque 3**
ALTER TABLE public.ele_edivipol
DROP CONSTRAINT unique_div_nkit RESTRICT;   

ALTER TABLE ele_edivipol
ADD CONSTRAINT unique_div_nkit
PRIMARY KEY (div_nkit)
DEFERRABLE INITIALLY DEFERRED

Если мы выполним каждый оператор ** Block 2 **, каждое предложение отдельно, в INSERT не будет сгенерировано никакой ошибки, поскольку он не проверяется, но выполняется последний COMMIT, где он обнаруживает несоответствие.


Для полной информации на английском я предлагаю вам проверить ссылки:

Отложенные ограничения SQL в глубине

НЕ ОТДЫХАЕТСЯ, а НЕ ОТДЫХАЕТСЯ ПЕРВОНАЧАЛЬНО НЕМЕДЛЕННО

Дэвид Кампос
источник