Введение
Статья 393 представила ключи концептуально — в этой статье разберём их подробнее: разные подходы к выбору первичного ключа (естественный против суррогатного), и поведение внешних ключей при удалении/изменении связанных строк через ON DELETE/ON UPDATE.
Концепция
Первичный ключ может быть «естественным» (использующим реально значимый атрибут данных, например номер паспорта) или «суррогатным» (искусственно сгенерированным значением, не имеющим самостоятельного смысла, обычно автоинкрементное целое число или UUID) — суррогатные ключи на практике предпочтительны в большинстве случаев, поскольку защищены от изменения значения «естественного» атрибута в будущем (что потребовало бы каскадного изменения во всех связанных таблицах). Внешний ключ может определять поведение при удалении/изменении родительской строки — CASCADE (автоматически удалить/изменить и связанные дочерние строки), RESTRICT/NO ACTION (запретить удаление родителя, если есть связанные дочерние строки), SET NULL (обнулить ссылку в дочерних строках).
Пример кода: Первичные и внешние ключи
-- Суррогатный ключ (автоинкрементное целое число) — рекомендуемый подход для большинства случаев
CREATE TABLE products (
id SERIAL PRIMARY KEY, -- SERIAL — автоинкремент в PostgreSQL; в других СУБД синтаксис отличается
sku VARCHAR(50) UNIQUE NOT NULL, -- "естественный" атрибут, но НЕ используемый как сам первичный ключ
name VARCHAR(200) NOT NULL
);
-- Поведение внешнего ключа при удалении родительской записи
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER REFERENCES orders(id) ON DELETE CASCADE, -- удаление заказа удаляет его позиции
product_id INTEGER REFERENCES products(id) ON DELETE RESTRICT, -- товар нельзя удалить, если он есть в заказах
quantity INTEGER NOT NULL CHECK (quantity > 0)
);
-- SET NULL — связь сохраняется как "неизвестная" вместо удаления дочерней записи
CREATE TABLE employees (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
manager_id INTEGER REFERENCES employees(id) ON DELETE SET NULL -- ссылка сама на себя!
-- при удалении руководителя его подчинённые НЕ удаляются, просто manager_id становится NULL
);
Пояснения к коду
sku VARCHAR(50) UNIQUE NOT NULL рядом с суррогатным id SERIAL PRIMARY KEY показывает практичный компромисс — реально значимый, «естественный» идентификатор (артикул товара) сохраняется как отдельное уникальное ограничение (что всё ещё гарантирует отсутствие дублей и позволяет искать по нему), но именно искусственный, не имеющий самостоятельного бизнес-смысла id используется как первичный ключ и для связей с другими таблицами — это даёт гибкость изменить sku в будущем (например, при пересмотре системы артикулов) без необходимости менять связанные внешние ключи во всех таблицах, ссылающихся на товар. ON DELETE CASCADE против ON DELETE RESTRICT в одной и той же таблице order_items показывает осознанный, различный выбор поведения для двух разных связей — позиции заказа логически не имеют смысла без самого заказа (поэтому каскадное удаление оправдано), но товар не должен быть удалён, пока на него есть ссылки в истории заказов (поэтому ограничение, а не каскад).
Подводные камни
- Использование
ON DELETE CASCADEбез полного понимания цепочки всех связанных таблиц, которые могут быть затронуты — в сложной схеме с многоуровневыми связями (A ссылается на B, B ссылается на C, и так далее, всё сCASCADE) удаление одной записи на верхнем уровне может вызвать массовое, неожидаемо широкое каскадное удаление множества связанных записей по всей цепочке; перед применениемCASCADEстоит явно продумать (и в идеале протестировать) полную цепочку последствий, а не применять его «по умолчанию» для всех внешних ключей без разбора. - Выбор естественного ключа, который впоследствии оказывается не таким уникальным/стабильным, как изначально предполагалось (классический пример — номер социального страхования или номер паспорта, которые в реальности иногда дублируются из-за ошибок или переиздаются) — это типичная причина, по которой суррогатные ключи рекомендуются по умолчанию: они гарантированно уникальны и стабильны самой природой их генерации, не зависящей от внешних, потенциально несовершенных систем нумерации.
- Создание циклических ссылок между таблицами через внешние ключи без продуманной стратегии вставки/удаления — если таблица A ссылается на B, а B ссылается на A, порядок вставки/удаления записей становится нетривиальным (нельзя вставить запись A, ссылающуюся на ещё не существующую запись B, и наоборот) — для таких случаев нужна либо явная отложенная проверка ограничений (deferred constraints, поддерживаемая некоторыми СУБД), либо пересмотр самой структуры схемы, чтобы избежать прямого цикла.
- Отсутствие индекса на столбце внешнего ключа — хотя сам внешний ключ обеспечивает ссылочную целостность, многие СУБД не создают индекс на столбце внешнего ключа автоматически (в отличие от первичного ключа, для которого индекс создаётся автоматически почти всегда), и без явно созданного дополнительного индекса JOIN-запросы (статья 398) или проверка каскадных операций по этому столбцу могут быть значительно медленнее, чем ожидается, особенно на больших таблицах.