Как внешний ключ должен вести себя при удалении родительской строки, если зависимые записи нельзя терять?
Удаление родительской строки должно быть отклонено, пока существуют ссылающиеся на неё дочерние строки. Для этого выбирают поведение внешнего ключа RESTRICT или NO ACTION, а не каскадное удаление.
Так сохраняется ссылочная целостность: каждая дочерняя запись продолжает ссылаться на существующую родительскую запись, а данные не исчезают незаметно.
В реляционной модели данные разделяются на связанные отношения, поэтому удаление строки в одной таблице может нарушить ссылки из другой таблицы. Ограничения внешнего ключа появились как декларативный способ поручить СУБД контроль таких зависимостей, а не оставлять его только прикладному коду.
Исходная проблема особенно заметна в системах, где несколько приложений или фоновых задач изменяют одну базу. Если проверку выполнять только в одном сервисе, другой клиент может удалить родительскую запись и оставить недействительные ссылки.
Предположим, заказ содержит ссылки на клиента. Удаление клиента, для которого уже существуют заказы, может сделать историю продаж неполной или нарушить требования аудита.
Если выбрать CASCADE, удаление клиента автоматически удалит и заказы. Если выбрать SET NULL, ссылки исчезнут, а заказам потребуется разрешать отсутствие клиента. Если внешнего ключа нет, база вообще не гарантирует, что ссылка останется действительной.
Для неизменяемых зависимых данных следует запретить удаление родителя при наличии дочерних строк. СУБД проверяет внешний ключ во время операции удаления и отклоняет операцию, если найдено хотя бы одно ссылающееся значение.
RESTRICT обычно означает немедленный запрет удаления при существующей ссылке. NO ACTION также не разрешает нарушить ограничение, но точные моменты проверки могут зависеть от поддержки отложенных ограничений конкретной СУБД. Поэтому поведение и доступные варианты нужно проверять по документации используемой системы, а критически важное действие задавать явно.
Родительскую запись можно удалить после удаления или переноса всех зависимых данных в рамках согласованной операции. В системах с историческими сущностями чаще применяют мягкое удаление: строку не удаляют физически, а помечают как неактивную. Это сохраняет ссылки и позволяет отличать «клиент больше не обслуживается» от «клиент никогда не существовал».
Ограничение не заменяет индексы и бизнес-правила. Индекс на внешнем ключе обычно ускоряет поиск зависимых строк при удалении родителя, но необходимость и параметры индекса зависят от СУБД и нагрузки. Кроме того, запрет удаления не мешает изменить другие атрибуты родителя, если для них нет отдельных ограничений.
В платёжной системе у клиента есть счета, платежи и документы аудита. Команда рассматривала три варианта: CASCADE упрощал очистку тестовых данных, но был опасен для продуктивных платежей; SET NULL сохранял платежи, однако разрушал обязательную связь с владельцем; физический запрет удаления сохранял историю, но требовал отдельного процесса деактивации.
Выбрали запрет удаления через внешний ключ и мягкое удаление клиента. Для тестовой среды очистку выполняют специальной контролируемой процедурой, а в продуктивной среде клиент переводится в неактивное состояние. В результате история платежей сохраняется, а случайное удаление владельца блокируется самой базой.
Оба варианта не позволяют завершить удаление родителя при нарушении внешнего ключа. Существенная разница проявляется при отложенной проверке ограничений: RESTRICT требует проверки сразу, тогда как NO ACTION в системах, поддерживающих deferred constraints, может позволить временное нарушение до конца транзакции. Если отложенные ограничения не поддерживаются или не используются, практическая разница может отсутствовать.
После удаления родителя СУБД должна записать NULL в дочерний внешний ключ. Поэтому столбец должен допускать NULL, а модель должна разрешать отсутствие родителя. Если связь обязательна по смыслу или столбец объявлен NOT NULL, такой вариант несовместим с моделью либо операция удаления завершится ошибкой.
Ограничение блокирует только удаление строки, на которую существуют ссылки. Оно не запрещает изменить описательные атрибуты родителя, удалить дочерние строки отдельной операцией или изменить бизнес-процесс так, что исторический смысл станет неясным. Для аудита дополнительно нужны правила доступа, журналирование, запреты на удаление дочерних сущностей или отдельная модель неизменяемых исторических записей.