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

В системе переводов исходный счёт у операции может отсутствовать, но если он указан, должны быть заданы обе части его составного ключа. Что именно гарантирует MATCH FULL в этой схеме?

CREATE TABLE account (
    country_code CHAR(2) NOT NULL,
    account_no VARCHAR(20) NOT NULL,
    owner_name VARCHAR(100) NOT NULL,
    PRIMARY KEY (country_code, account_no)
);

CREATE TABLE transfer (
    transfer_id INTEGER PRIMARY KEY,
    src_country CHAR(2),
    src_account VARCHAR(20),
    FOREIGN KEY (src_country, src_account)
        REFERENCES account (country_code, account_no)
        MATCH FULL
);
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

MATCH FULL требует, чтобы составная ссылка была либо полностью NULL, либо полностью заполненной и указывала на существующую строку родительской таблицы. Поэтому пара (NULL, '12345') будет отклонена, тогда как (NULL, NULL) допустима как отсутствие исходного счёта.

Без MATCH FULL обычно действует MATCH SIMPLE: наличие NULL хотя бы в одном столбце составного внешнего ключа может сделать проверку ссылочной целостности пройденной. Важно учитывать поддержку этого режима конкретной СУБД: MATCH FULL является стандартным SQL-механизмом, но реализован не во всех диалектах одинаково.

Исторический контекст

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

Режимы сопоставления MATCH SIMPLE и MATCH FULL задают разные правила обработки NULL в такой ссылке. Это позволяет отличить «связь отсутствует» от ошибочно переданной только части составного идентификатора.

Постановка проблемы

В примере счёт определяется парой (country_code, account_no). Значение только src_account не идентифицирует счёт однозначно, потому что номер может иметь смысл только вместе с кодом страны.

Если СУБД использует обычную семантику MATCH SIMPLE, строка с (NULL, '12345') может пройти проверку внешнего ключа. В результате в базе появится частичная ссылка: приложение передало номер счёта, но база не потребовала код страны.

Такие данные затрудняют JOIN, отчёты и последующую обработку. Часть запросов будет трактовать строку как перевод без исходного счёта, а часть — как некорректно заполненную ссылку.

Подробное решение

Для MATCH FULL действуют два допустимых состояния:

  • все столбцы внешнего ключа имеют NULL;
  • все столбцы заполнены, и соответствующая комбинация существует в родительской таблице.

Следовательно, допустимы (NULL, NULL) и, например, ('RU', '12345'), если такая строка есть в account. Значения (NULL, '12345') и ('RU', NULL) нарушают целостность и должны быть отклонены.

INSERT INTO transfer VALUES (1, NULL, NULL); -- допустимо INSERT INTO transfer VALUES (2, 'RU', '12345'); -- допустимо при наличии счёта INSERT INTO transfer VALUES (3, NULL, '12345'); -- ошибка MATCH FULL

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

Альтернативой может быть явное ограничение:

CHECK ((src_country IS NULL) = (src_account IS NULL))

Но такой CHECK контролирует только совместную заполненность столбцов, а внешний ключ отдельно должен проверять существование пары. MATCH FULL выражает обе части намерения в одном ограничении, если СУБД поддерживает этот режим.

У внешнего ключа также могут быть дополнительные ограничения NOT NULL, но они подходят только тогда, когда ссылка обязательна. Если сделать оба столбца NOT NULL, исчезнет возможность представить перевод без исходного счёта.

Ситуация из практики

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

Первый вариант — оставить MATCH SIMPLE. Его плюс — широкая поддержка и простая миграция. Минус — база принимает частично заполненные ссылки, поэтому ошибка интеграции обнаруживается только в приложении или отчётах.

Второй вариант — оставить обычный внешний ключ и добавить CHECK, проверяющий синхронность NULL. Это переносимее, но правило состоит из двух независимых ограничений, а разработчикам нужно явно поддерживать их вместе.

Третий вариант — использовать MATCH FULL. Команда выбрала его в СУБД, где режим поддерживался и был проверен тестами миграции. В результате отсутствие счёта стало представляться только парой NULL, а частичные идентификаторы отклонялись на уровне базы.

Что кандидаты часто упускают

  1. Что произойдёт, если оба столбца внешнего ключа имеют NULL при MATCH FULL?

    Такая строка обычно считается отсутствующей ссылкой и допускается. MATCH FULL не означает, что составной внешний ключ всегда обязан быть заполнен; он запрещает именно частичное заполнение. Если ссылка обязательна, нужно дополнительно задать NOT NULL для обоих столбцов.

  2. Проверяет ли MATCH FULL уникальность родительской комбинации?

    Нет. Он определяет правила сопоставления NULL и проверки составной ссылки, но не заменяет уникальность родительского набора столбцов. Для корректного внешнего ключа родительская комбинация должна быть первичным ключом или поддерживаться подходящим уникальным ограничением, как в примере с PRIMARY KEY (country_code, account_no).

  3. Можно ли заменить MATCH FULL двумя отдельными внешними ключами?

    Обычно это меняет смысл модели. Два отдельных внешних ключа проверяют country_code и account_no независимо, а не существование конкретной пары. Даже если каждый компонент существует в родительской таблице, их комбинация может не существовать. Для составной идентичности нужен один составной внешний ключ, дополненный MATCH FULL или эквивалентным CHECK для совместной заполненности.