Рассмотрите DDL и две вставки ниже. Почему ограничение UNIQUE допускает обе строки с NULL в email, тогда как PRIMARY KEY не позволяет такое значение?
CREATE TABLE accounts (
account_id INTEGER PRIMARY KEY,
email VARCHAR(255) UNIQUE
);
INSERT INTO accounts (account_id, email) VALUES (1, NULL);
INSERT INTO accounts (account_id, email) VALUES (2, NULL);
Обе вставки выполняются успешно: UNIQUE не считает два значения NULL дубликатами, поскольку NULL означает отсутствие известного значения, а не конкретное значение для сравнения. PRIMARY KEY отличается тем, что объединяет уникальность с обязательностью: его столбцы не могут содержать NULL.
Реляционная модель использует NULL для представления неизвестного, отсутствующего или неприменимого значения. Поэтому сравнение NULL с другим NULL не дает TRUE: результатом является UNKNOWN в трехзначной логике SQL.
Ограничение UNIQUE предназначено для предотвращения повторения известных значений. Оно не превращает отсутствие значения в обычное значение и потому обычно допускает несколько строк с NULL.
В примере account_id является идентификатором строки, поэтому каждая запись обязана иметь уникальное и известное значение. Для email бизнес-правило другое: адреса должны быть уникальными, если они указаны, но отсутствие адреса может быть допустимым для нескольких аккаунтов.
Неверно считать, что UNIQUE автоматически запрещает NULL. Если столбец должен быть заполнен, нужно явно добавить NOT NULL; иначе база данных позволит несколько строк без значения email.
PRIMARY KEY логически включает два требования: UNIQUE и NOT NULL. Поэтому такая вставка будет отклонена:
Для email ограничение UNIQUE проверяет конфликт между известными значениями. Две строки с одним и тем же адресом нарушат ограничение, а несколько строк с NULL обычно не нарушают его, потому что NULL не равен NULL в обычном SQL-сравнении.
Если отсутствие адреса недопустимо, определение должно выглядеть так:
Здесь NOT NULL отвечает за наличие значения, а UNIQUE — за отсутствие повторов среди существующих значений. Порядок ограничений в объявлении не меняет их смысл.
Это правило имеет важную реализационную оговорку: конкретная СУБД может иметь особенности уникальных индексов или дополнительные настройки. В переносимом SQL следует проверять документацию целевой СУБД, особенно если требуется нестандартное поведение — например, запрет более одного NULL или уникальность без учета регистра.
Если нужно разрешить отсутствие адреса, но считать адреса без учета регистра одинаковыми, одного обычного UNIQUE недостаточно: сравнение зависит от типа данных, сортировки и правил сравнения строк. Такое правило обычно выражают функциональным индексом, вычисляемым столбцом или специальной нормализацией значения, если это поддерживает выбранная СУБД.
В системе регистрации часть пользователей не сообщает email, но указанные адреса должны быть уникальны. Вариант email VARCHAR(255) UNIQUE позволяет нескольким пользователям оставить поле пустым и одновременно предотвращает повторение одного известного адреса.
Вариант с NOT NULL был бы слишком строгим: пришлось бы записывать фиктивные адреса, что ухудшило бы качество данных. Вариант без UNIQUE позволил бы случайно связать один адрес с несколькими аккаунтами и усложнил бы восстановление доступа.
Выбранное решение — email VARCHAR(255) UNIQUE, а если адрес впоследствии становится обязательным по бизнес-правилу, выполняется отдельная миграция: сначала исправляются существующие данные, затем добавляется NOT NULL. Результат — различие между «адрес неизвестен» и «адрес известен и должен быть уникальным» сохраняется на уровне схемы.
Что произойдет при вставке двух одинаковых известных адресов?
Вторая вставка будет отклонена нарушением ограничения UNIQUE. В отличие от NULL, строковые значения сравниваются как конкретные значения, поэтому одинаковые адреса считаются дубликатами.
Однако результат сравнения строк может зависеть от правил сортировки: в некоторых конфигурациях регистр или диакритические знаки считаются незначимыми. Поэтому User@example.com и user@example.com не всегда гарантированно воспринимаются одинаково.
Почему UNIQUE(email) не заменяет первичный ключ?
UNIQUE допускает NULL, если столбец не объявлен как NOT NULL, и таблица может иметь несколько уникальных ограничений. Первичный ключ выбирает главный способ идентификации строк, не допускает NULL и обычно используется внешними ключами как целевой ключ.
Кроме того, в таблице может быть только один первичный ключ, хотя уникальных ограничений может быть несколько. Например, account_id может быть первичным ключом, а email — альтернативным уникальным ключом.
Как выразить правило «не более одного NULL»?
Простое стандартное UNIQUE обычно не выражает такое правило, поскольку допускает несколько NULL. Возможные решения зависят от СУБД: отдельное ограничение или индекс на признак наличия значения, частичный уникальный индекс, вычисляемый ключ либо триггер.
На практике чаще проверяют, действительно ли это правило нужно. Если NULL означает отсутствие email, несколько таких строк обычно корректны; если же требуется ровно одна специальная «пустая» запись, лучше хранить явный уникальный признак или отдельную сущность, а не полагаться на нестандартную трактовку NULL.