Допустимо ли объявить внешний ключ со ссылкой на столбец, уникальность которого фактически соблюдается, но не задана ограничением?
Обычно нет. Внешний ключ должен ссылаться на первичный ключ или на столбцы с явно заданным ограничением UNIQUE, потому что СУБД должна сама гарантировать однозначное соответствие каждой ссылки родительской строке.
Фактическая уникальность, которую поддерживает только приложение или текущие данные, недостаточна: она может исчезнуть после следующей вставки или изменения.
Внешние ключи появились как механизм декларативной ссылочной целостности в реляционных базах данных. Их задача — передать контроль согласованности от прикладного кода самой СУБД.
Чтобы ссылка была однозначной, родительские столбцы должны образовывать кандидатный ключ: набор атрибутов, однозначно идентифицирующий строку. Первичный ключ и подходящее ограничение UNIQUE являются такими ключами.
Предположим, приложение считает значения столбца code уникальными, но СУБД этого не проверяет. Пока в таблице одна строка с конкретным кодом, внешний ключ может выглядеть логично, однако параллельная транзакция или ошибка приложения способна создать дубликат.
Тогда одна дочерняя строка будет соответствовать нескольким родительским строкам. СУБД не сможет однозначно определить, какую строку считать владельцем ссылки, а операции обновления и удаления потеряют надежную семантику.
При создании внешнего ключа СУБД проверяет, что целевые столбцы образуют допустимый уникальный ключ. В типичном варианте это первичный ключ или явно объявленное ограничение UNIQUE:
Здесь ссылка на external_code допустима, поскольку уникальность контролируется СУБД. Если же external_code просто содержит неповторяющиеся значения без UNIQUE, создание внешнего ключа обычно завершится ошибкой; конкретные расширения СУБД могут дополнительно разрешать ссылку на уникальный индекс.
Для составного внешнего ключа требуется составной уникальный ключ с теми же столбцами и в том же логическом порядке. Два независимых ограничения UNIQUE на отдельных столбцах не гарантируют уникальность их комбинации.
Типы и количество столбцов внешнего ключа также должны быть совместимы с целевыми столбцами. Значение NULL обычно не нарушает внешний ключ: оно означает отсутствие установленной ссылки, если столбец не объявлен обязательным.
Ограничение UNIQUE проверяется при изменениях данных, поэтому оно устраняет расхождение между текущим состоянием и заявленной моделью. Компромисс — дополнительные проверки и обычно индекс, а также необходимость сначала устранить существующие дубликаты при добавлении ограничения к заполненной таблице.
В системе заказов идентификатор клиента во внешнем сервисе хранится в external_code. Разработчики не объявили UNIQUE, потому что при первоначальной загрузке выгрузка не содержала дубликатов. Позже повторная синхронизация создала два клиента с одним кодом, и определить владельца новых заказов стало невозможно.
Вариант с проверкой уникальности только в коде дешевле на уровне схемы, но не защищает от ошибок миграций и конкурентных записей. Вариант со ссылкой на неуникальный столбец не дает СУБД корректно выразить ссылочную целостность и обычно отклоняется.
Выбранное решение — очистить дубликаты, добавить UNIQUE на external_code, затем создать внешний ключ. В результате база данных сама гарантирует как уникальность родительского идентификатора, так и существование каждой допустимой ссылки.
1. Достаточно ли двух отдельных UNIQUE для составной ссылки?
Нет. Если внешний ключ ссылается на пару столбцов, уникальной должна быть именно пара. Например, отдельная уникальность country_code и local_number не гарантирует уникальность комбинации; одна и та же пара не должна встречаться повторно только при наличии составного ограничения UNIQUE.
2. Что произойдет при добавлении UNIQUE в уже заполненную таблицу с дубликатами?
Операция обычно завершится ошибкой, потому что существующие строки уже нарушают будущий инвариант. Сначала необходимо найти и обработать дубликаты: объединить записи, переназначить дочерние ссылки или удалить ошибочные строки. Только после этого ограничение можно добавить; некоторые СУБД предоставляют специальные отложенные или поэтапные механизмы, но их поведение непереносимо между СУБД.
3. Почему приложение не может заменить ограничение UNIQUE?
Потому что проверка в приложении не является атомарной с записью без корректной синхронизации. Два параллельных запроса могут одновременно увидеть, что значения еще нет, и оба вставить его. Ограничение UNIQUE проверяется внутри СУБД как часть операции изменения данных и поэтому защищает от такой гонки.