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 = 5 | UNKNOWN |
| NULL = NULL | UNKNOWN (не TRUE!) |
| NULL <> 5 | UNKNOWN |
| TRUE AND UNKNOWN | UNKNOWN |
| FALSE AND UNKNOWN | FALSE |
| TRUE OR UNKNOWN | TRUE |
Главное практическое следствие: 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.