NULL, трёхзначная логика и целостность

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

NULL — это маркер отсутствия значения: «значение неизвестно или неприменимо». NULL — это не ноль, не пустая строка и не «ничего»; это особое состояние «здесь значения нет».

Зачем нужен NULL

В реальных данных часть фактов просто неизвестна. У клиента ещё не указан телефон; у книги нет года переиздания, потому что его не было. Нам нужен способ сказать «значение отсутствует», не подменяя его выдуманным. Если вместо неизвестного телефона поставить 0 или пустую строку, мы соврём: 0 — это конкретное значение, а правда в том, что значения нет. Для честного «не знаю» Кодд и ввёл NULL.

NULL — это не ноль и не пустота

Самая частая ошибка новичка — считать NULL «нулём» или «пустой строкой». Разница принципиальна. Ноль (0) — это конкретное, известное значение: «на счёте ровно ноль рублей». Пустая строка ('') — тоже конкретное значение: «имя — пустая строка». А NULL — это отсутствие значения: «сколько на счёте — неизвестно», «имя не указано». Разница видна в арифметике и сравнениях: 0 + 5 = 5, но NULL + 5 = NULL (к неизвестному прибавили — получили неизвестное); '' = '' истинно, а NULL = NULL — нет. Смешение этих понятий ведёт к коварным ошибкам: например, скидка, посчитанная как cena - NULL, обнулит весь результат в NULL, а не вычтет ноль. Всегда спрашивайте себя: значение здесь известно и равно нулю или неизвестно? Это разные ситуации и хранить их надо по-разному.

Трёхзначная логика

NULL ломает привычную булеву логику. Сравните: phone = '123' — если телефон неизвестен (NULL), результат не TRUE и не FALSE, а UNKNOWN («неизвестно»). Поэтому реляционная модель использует не двузначную, а трёхзначную логику: TRUE, FALSE и UNKNOWN. Любое сравнение с NULL даёт UNKNOWN.

ВыражениеРезультат
NULL = 5UNKNOWN
NULL = NULLUNKNOWN (не TRUE!)
NULL <> 5UNKNOWN
TRUE AND UNKNOWNUNKNOWN
FALSE AND UNKNOWNFALSE
TRUE OR UNKNOWNTRUE

Главное практическое следствие: WHERE возвращает строку, только если условие равно строго TRUE. Если оно UNKNOWN — строка отбрасывается. Поэтому WHERE phone = phone не вернёт строки с NULL-телефоном, а проверять отсутствие нужно специальным оператором IS NULL, а не = NULL.

CREATE TABLE clients (id INTEGER, name TEXT, phone TEXT);
INSERT INTO clients VALUES (1, 'Анна', '+7-900'), (2, 'Борис', NULL);

-- = NULL не работает: сравнение с NULL даёт UNKNOWN
SELECT name FROM clients WHERE phone = NULL;     -- вернёт пусто
-- правильный способ найти отсутствующий телефон:
SELECT name FROM clients WHERE phone IS NULL;

Первый SELECT не вернёт ничего: условие phone = NULL для каждой строки равно UNKNOWN. Второй вернёт «Борис». Это классическая ловушка новичка.

Ещё ловушки NULL

  • Агрегаты игнорируют NULL. COUNT(phone) посчитает только непустые телефоны; COUNT(*) — все строки. AVG и SUM тоже пропускают NULL.
  • NOT IN с NULL опасен. Если в списке для NOT IN есть NULL, результат может стать UNKNOWN для всех строк, и запрос вернёт пусто.
  • UNIQUE и NULL. По стандарту два NULL не считаются равными, поэтому уникальный столбец обычно допускает несколько NULL.

Покажем разницу COUNT(*) и COUNT(столбец) вживую — это частый источник недоумения в отчётах.

CREATE TABLE clients (id INTEGER, name TEXT, phone TEXT);
INSERT INTO clients VALUES (1,'Анна','+7-900'),(2,'Борис',NULL),(3,'Вера',NULL);

SELECT
  COUNT(*)     AS vsego,        -- считает все строки
  COUNT(phone) AS s_telefonom   -- считает только не-NULL
FROM clients;

Вывод: «3, 1». Строк всего три, но телефон указан лишь у одной — COUNT(phone) пропустил два NULL. Не понимая этого, легко получить неверную статистику: «у нас 3 клиента, но дозвонимся только до одного».

Почему целостность встроена в модель

Стоит подчеркнуть, чем целостность сущностей и ссылочная целостность отличаются от прочих ограничений. Это не правила конкретной предметной области (вроде «цена ≥ 0»), а свойства самой реляционной модели: они вытекают из того, что такое ключ. Целостность сущностей — следствие того, что первичный ключ обязан идентифицировать кортеж (а NULL не идентифицирует). Ссылочная целостность — следствие того, что внешний ключ выражает связь (а ссылка в никуда — не связь). Поэтому СУБД обеспечивает их автоматически, как только вы объявили ключи, — в отличие от доменных ограничений, которые приходится задавать вручную через CHECK. Эти два правила — тот минимум целостности, ниже которого данные перестают быть реляционными.

Почему NULL — спорное решение

NULL удобен, но за него идёт давний спор среди теоретиков, и полезно понимать обе стороны. Сторонники говорят: отсутствие значения — реальный факт, его нужно как-то выражать, иначе придётся врать выдуманными значениями. Критики (в их числе сам Кодд в поздних работах сомневался в реализации) указывают: NULL ломает привычную двузначную логику, делает запросы неинтуитивными и порождает тонкие ошибки, которые мы только что видели. Некоторые проектировщики намеренно избегают NULL, заменяя его явными значениями-маркерами или выделяя отдельные таблицы для необязательных данных. Единственно верного ответа нет: NULL — это инструмент с острыми краями. Зная его поведение (трёхзначная логика, IS NULL, игнорирование в агрегатах), вы пользуетесь им осознанно, а не натыкаетесь на сюрпризы.

Два вида целостности

Реляционная модель защищает данные двумя фундаментальными правилами целостности — они не зависят от конкретной предметной области и встроены в саму модель.

Целостность сущностей

Целостность сущностей: ни один атрибут первичного ключа не может быть NULL. Логика железная: первичный ключ должен уникально идентифицировать кортеж, а NULL означает «неизвестно» — значит, по нему нельзя ни идентифицировать строку, ни отличить две строки с NULL в ключе. Поэтому СУБД запрещает NULL в первичном ключе.

Ссылочная целостность

Ссылочная целостность: значение внешнего ключа должно либо совпадать с каким-то значением первичного ключа в целевой таблице, либо быть полностью NULL (если связь необязательна). Нельзя сослаться на несуществующего клиента. Это защищает от «висячих ссылок» — заказов, привязанных к клиенту, которого нет.

Обратите внимание на оговорку «полностью NULL». Если внешний ключ составной (ссылается на составной первичный ключ), частично заполненное состояние — часть атрибутов задана, часть NULL — обычно недопустимо: либо все NULL (связи нет), либо все заданы и указывают на существующую строку. Это правило не даёт появиться «полуссылке», которая никуда конкретно не ведёт.

CREATE TABLE clients (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  client_id INTEGER REFERENCES clients(id)
);
INSERT INTO clients VALUES (1, 'Анна');

-- корректно: клиент 1 существует
INSERT INTO orders VALUES (10, 1);
SELECT * FROM orders;

А вот попытка вставить заказ со client_id = 999 (несуществующий клиент) при включённой проверке внешних ключей будет отклонена — это ссылочная целостность в действии. Что делать при удалении клиента, у которого есть заказы, описывают политики ON DELETE: запретить (RESTRICT), обнулить ссылку (SET NULL) или удалить каскадно зависимые строки (CASCADE).

Выбор политики — содержательное проектное решение. RESTRICT (по умолчанию во многих СУБД) защищает от случайной потери данных: нельзя удалить клиента, пока у него есть заказы. CASCADE уместен, когда зависимые строки бессмысленны без родителя (удалили заказ — удалились его строки). SET NULL подходит, когда связь необязательна и потеря родителя допустима (удалили категорию — товары остались, но без категории). Неверно выбранная политика либо мешает работе (всё запрещено), либо тихо стирает нужные данные (всё каскадно). Поэтому, объявляя внешний ключ, всегда осознанно решайте, что должно происходить с зависимыми строками при удалении родителя.

Типичные ошибки

  • Сравнение = NULL вместо IS NULL. Первое всегда даёт UNKNOWN и не находит NULL-значения.
  • Ожидание, что NULL = NULL истинно. Это UNKNOWN: два «неизвестно» не равны.
  • NULL в первичном ключе. Нарушает целостность сущностей — СУБД его не допустит.
  • Внешний ключ без проверки. Ведёт к висячим ссылкам на удалённые сущности.

Итог

  • NULL — маркер отсутствия значения, а не ноль и не пустая строка.
  • Из-за NULL логика становится трёхзначной (TRUE/FALSE/UNKNOWN); проверять отсутствие нужно через IS NULL.
  • Целостность сущностей запрещает NULL в первичном ключе.
  • Ссылочная целостность требует, чтобы внешний ключ указывал на существующую строку либо был NULL.
Проверьте себя
1. Чему равно сравнение NULL = NULL в трёхзначной логике?
ATRUE
BFALSE
CUNKNOWN
DОшибка
2. Что гарантирует целостность сущностей?
AВнешний ключ ссылается на существующую строку
BПервичный ключ не может содержать NULL
CВсе значения уникальны
DНет дубликатов строк
3. Почему запрос WHERE phone = NULL не находит строки с пустым телефоном?
ANULL нельзя хранить в столбце
BСравнение с NULL даёт UNKNOWN, а WHERE пропускает только строго TRUE
CНужно писать phone == NULL
DЭто синтаксическая ошибка
Поддержать проект