Как стать автором
Обновить

Комментарии 17

Спасибо за отличную статью! Хотел прояснить одну вещь.


В этот момент может возникнуть вопрос – зачем вообще создавать ограничение, если индекс выполняет ровно то, что требуется – гарантирует уникальность значений?
Единственное отличие, которое удалось найти, состоит в том, что ограничения могут быть отложенными (deferrable), а индексы – нет.

Ответ очень простой: на ограничения уникальности можно ссылаться из внешних ключей. Просто индекса в этом случае недостаточно. Читаем документацию:


A foreign key must reference columns that either are a primary key or form a unique constraint. This means that the referenced columns always have an index (the one underlying the primary key or unique constraint).

Попутно у некоторых людей возникает вопрос: а в чём тогда отличие первичных ключей от уникальных? Ответ: первичный ключ не допускает значений NULL, и он лишь один на таблицу.

Спасибо за комментарий!
Ответ очень простой: на ограничения уникальности можно ссылаться из внешних ключей. Просто индекса в этом случае недостаточно. Читаем документацию:


Эксперименты показывают, что уникального индекса все же достаточно. В ссылке на SO выше есть пример для воспроизведения:
create table A(
  first_a varchar,
  second_a varchar,
  third_a varchar,
  primary key(first_a, second_a)
  );
  
  create unique index on A (second_a, third_a);
  
  create table B(
    first_b varchar,
    second_b varchar,
    foreign key (first_b, second_b) references A(second_a, third_a), --references the unique index
    foreign key (first_b, second_b) references A(first_a, second_a) -- references the primary key
    );

Причем, если не создавать индекс, то получим ошибку:
ERROR: there is no unique constraint matching given keys for referenced table «a»

В общем, похоже на неточность в документации.

Да, похоже вы правы, действительно, работает! Возможно, это послабление было сделано специально, по каким-то неизвестным нам причинам, и открывать тикет навряд ли стоит, потому что обратная совместимость важнее и всё такое. Я точно знаю по опыту работы с другими СУБД что этот constraint необходим, и иногда даже задаю этот вопрос на собеседованиях.


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


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


В общем случае индекс не обязан присутствовать для поддержки уникального ограничения, но крайне желателен для больших таблиц во избежание full/seq-scan. Для маленьких табличек он точно не нужен, и в других СУБД индекс не создаётся автоматически при создании уникального ключа.


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

Согласитесь, что ключи и индексы — это совершенно разные вещи. Ключи обеспечивают целостность и непротиворечивость данных с точки зрения бизнес правил, в то время как индексы — это чисто техническая вещь для ускорения доступа к данным.

Полностью согласен с вами и сам лично также предпочитаю выполнять этот последний «опциональный» пункт по созданию ограничения на основе индекса.
Более того, как я отметил в статье, раньше даже сама документация советовала использовать ограничение, а вот почему с 9.5 эту сноску удалили – интересный вопрос.
>и в других СУБД индекс не создаётся автоматически при создании уникального ключа.

Можете подсказать в каких именно СУБД? Знаю точно, что Oracle и MySQL ведёт себя точно также как PostgreSQL в этом вопросе.
Важно заметить, что команда REINDEX, которая как раз предназначена для пересоздания индекса, до 12й версии работает только в блокирующем режиме, что не дает возможности ее использовать. В 12й версии PostgreSQL появилась поддержка CONCURRENTLY, и теперь и ей можно пользоваться.

Тут хотел бы отметить, что reindex'ов несколько и они немного различаются:
reindex index concurrently — будет. Но в случае очередной ошибки (например уникальности) каждый такой reindex index будет оставлять по дополнительному дублирующему invalid индексу.
reindex более крупных объектов (reindex table concurrently и т.д.) не будет перестраивать invalid индексы. Сделано специально, чтобы не плодить много invalid индексов.
К слову, reindex concurrently на уровне кода базы — это create index concurrently + swap + drop index concurrently

Создание ограничения NOT NULL

На версии postgresql 12 и новее после VALIDATE CONSTRAINT уже можно сделать
ALTER TABLE my_table ALTER COLUMN name SET NOT NULL

И затем можно удалить добавленный check constraint. Мой патч принят в pg12 и set not null теперь имеет возможность не читать данные, если по имеющимся check можно увидеть, что в поле null нет.
Что упростит и махинации с primary key.

PS: отдельно спасибо за выделение опасности взятия эксклюзивной блокировки. Довольно часто пишут, что «такой alter table — это быстрая операция», но без уточнения «быстрая только после взятия блокировки». Что на практике довольно регулярно приводит к сюрпризам для разработчиков.
Спасибо за подробный комментарий! Дополню статью.
Небольшой вопрос – в какую из минорных версий pg12 попал ваш патч (беглый поиск по release notes не помог мне)?
Во всех 12 начиная с самого feature freeze. В release notes вот эта запись:
Allow ALTER TABLE… SET NOT NULL to avoid unnecessary table scans

до PG12 если есть права на записть в pg_attribute можно сделать немного чёрной магии:


UPDATE pg_catalog.pg_attribute SET attnotnull = TRUE
WHERE attrelid = 'table_name'::regclass::oid AND attname = 'col_name'
Да, знаю. Это очень грязный скальпель и потому DBA не очень-то о нём распространяются. Официально любые приключения после правок системного каталога вручную — не баг, исправляться не будут и могут быть фатальны для всей базы.
Я намеренно на стал включать подобные трюки, но сейчас подумал, что может быть и стоит упомянуть, с дисклеймером, конечно же.

ещё можно добавить:


  • FK делает блокировку на две таблицы (особенно может вылазить при удалении столбца или таблицы)
  • есть ещё statement_timeout в добавок к lock_timeout — на случай, когда, что-то упустили
  • EXCLUDE констрэйнт, который вроде не обойти
  • есть другие операции как SET SCHEMA, SET TABLESPACE, SET DATA TYPE

также интересно:


  • как писать логику и делать миграции когда стэйтов приложения и базы много
  • сколько стэйтов не больно поддерживать одновременно
  • как ведут себя блокировки в репликах и шардах
  • что делать если операция нужна, но серьёзно блокирующая
Да, возможно, что statement_timeout даже более безопасно использовать, т.к. включает и время захвата блокировки и ее удержание во время выполения команды.

С EXCLUDE мне не особо приходилось работать, поэтому ничего не написал :-) Если есть, чем поделиться, то так же буду рад дополнить статью.

Насчет остального – спасибо за идеи, подумаю.

Отличная статья, спасибо!
А как в Postgres работает ограничение уникальности по столбцу партиционированной таблицы, если индексы создаются для каждой партиции отдельно?
Или есть вариант глобального индекса?

Или ключ партицирования входит в сам уникальный индекс или уникальность только в пределах партиции. Это прямым текстом описано в limitations:
This limitation exists because PostgreSQL can only enforce uniqueness in each partition individually.
В добавление к ответу выше – если ограничение/индекс не включает ключ партиционирования, но при этом «глобальная» уникальность требуется, есть костыльный вариант создать триггер на модифицации для валидации. Но это будет сильно влиять на производительность обновлений.
Очень полезно! Распечатать и повесить в офисе в качестве шпаргалки:)
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.