Избегайте уникальных нарушений в атомарных транзакциях

15

Возможно ли создать атомарную транзакцию в PostgreSQL?

Считайте, что у меня есть категория таблицы с этими строками:

id|name
--|---------
1 |'tablets'
2 |'phones'

И имя столбца имеет уникальное ограничение.

Если я попробую:

BEGIN;
update "category" set name = 'phones' where id = 1;
update "category" set name = 'tablets' where id = 2;
COMMIT;

Я получаю:

ERROR:  duplicate key value violates unique constraint "category_name_key"
DETAIL:  Key (name)=(tablets) already exists.
Петр Пржикрил
источник

Ответы:

24

В дополнение к тому, что предоставил @Craig (и исправляет некоторые из них):

Эффективное Postgres 9.4 , UNIQUE, PRIMARY KEYи EXCLUDEограничения проверяются сразу после каждой строки , когда определено NOT DEFERRABLE. Это отличается от других видов NOT DEFERRABLEограничений (только в настоящее время REFERENCES(внешний ключ)), которые проверяются после каждого оператора . Мы работали над всем этим под вопросом, связанным с SO:

Это не достаточно для UNIQUE(или PRIMARY KEYили EXCLUDEограничение) , чтобы быть , DEFERRABLEчтобы сделать ваш код , представленный с несколькими отчетности работы.

И вы не можете использовать ALTER TABLE ... ALTER CONSTRAINTдля этой цели. По документации:

ALTER CONSTRAINT

Эта форма изменяет атрибуты ограничения, которое было создано ранее. В настоящее время могут быть изменены только ограничения внешнего ключа .

Жирный акцент мой. Используйте вместо этого:

ALTER TABLE t
   DROP CONSTRAINT category_name_key
 , ADD  CONSTRAINT category_name_key UNIQUE(name) DEFERRABLE;

Отбросьте и добавьте ограничение обратно в одном выражении, чтобы у всех не было временного промежутка, чтобы проникнуть в поврежденные строки. Для больших таблиц было бы заманчиво каким-то образом сохранить базовый уникальный индекс, поскольку его удаление и повторное создание обходится дорого. Увы, это не представляется возможным при использовании стандартных инструментов (если у вас есть решение для этого, пожалуйста, сообщите нам об этом!):

Для одного оператора достаточно сделать ограничение отложенным:

UPDATE category c
SET    name = c_old.name
FROM   category c_old
WHERE  c.id     IN (1,2)
AND    c_old.id IN (1,2)
AND    c.id <> c_old.id;

Запрос с CTE также является одним оператором:

WITH x AS (
    UPDATE category SET name = 'phones' WHERE id = 1
    )
UPDATE category SET name = 'tablets' WHERE id = 2;

Однако для вашего кода с несколькими операторами вам (дополнительно) необходимо фактически отложить ограничение - или определить его как INITIALLY DEFERREDEither, как правило, дороже, чем выше. Но может быть нелегко собрать все в одно утверждение.

BEGIN;
SET CONSTRAINTS category_name_key DEFERRED;
UPDATE category SET name = 'phones'  WHERE id = 1;
UPDATE category SET name = 'tablets' WHERE id = 2;
COMMIT;

Имейте в виду ограничение в связи с FOREIGN KEYограничениями, однако. По документации:

Столбцы, на которые ссылаются, должны быть столбцами неотложного ограничения уникального или первичного ключа в таблице, на которую ссылаются.

Таким образом, вы не можете иметь и то и другое одновременно.

Эрвин Брандштеттер
источник
13

Насколько я понимаю, ваша проблема здесь в том, что ограничение проверяется после каждого оператора, но вы хотите, чтобы оно проверялось в конце транзакции, поэтому оно сравнивает состояние «до» с состоянием «после», игнорируя промежуточные состояния.

Если это так, это возможно с отсроченным ограничением .

Смотрите SET CONSTRAINTSи DEFERRABLEограничения, как описано в CREATE TABLE.

Обратите внимание, что отложенные ограничения имеют свои затраты - система должна хранить их список для проверки во время принятия, поэтому они не подходят для транзакций, которые вносят огромные наборы изменений. Их также медленнее проверять.

Поэтому я думаю, что вы, вероятно, хотите:

ALTER TABLE mytable ALTER CONSTRAINT category_name_key DEFERRABLE;

Обратите внимание, что существует ограничение на ALTER TABLEустановку ограничений на DEFERRABLE; вам, возможно, придется вместо этого DROPи повторно ADDограничить.

Крейг Рингер
источник