Статья
NULL и три значения истины
Что значит пустое значение NULL, почему сравнение с ним не даёт ни да, ни нет и в каких местах запроса это тихо портит результат
NULL — это «неизвестно», а не ноль и не пустая строка
NULL в ячейке значит: значения нет или оно неизвестно. У заказа, который ещё не отгрузили, пустой shipped_at; у директора пустой manager_id; у отзыва без текста пустой text. Это не 0 и не '': ноль — известное число, пустая строка — известная строка, а NULL — вопросительный знак.
Представь анкету, где в графе «возраст» стоит прочерк. Старше ли этот человек тридцати? Равен ли его возраст возрасту другого человека с прочерком? Честный ответ на оба вопроса — «неизвестно». SQL рассуждает так же, поэтому у условий в нём три возможных значения: TRUE, FALSE и NULL («неизвестно»).
Сравнение с NULL даёт NULL
SELECT id, name, manager_id, manager_id = 1 AS eq_one, manager_id <> 1 AS ne_one, manager_id = NULL AS eq_null, manager_id IS NULL AS is_nullFROM employeesWHERE id <= 4ORDER BY id id | name | manager_id | eq_one | ne_one | eq_null | is_null
----+-----------------+------------+--------+--------+---------+---------
1 | Виктор Орлов | | | | | t
2 | Светлана Гусева | 1 | t | f | | f
3 | Роман Зайцев | 1 | t | f | | f
4 | Ксения Белова | 1 | t | f | | f
(4 rows)Пустые ячейки в колонках-условиях — это NULL. У директора manager_id неизвестен, поэтому и = 1, и <> 1 дают «неизвестно». Колонка eq_null пустая во всех строках: = NULL не бывает истинным даже для директора. Проверить пустоту умеет только IS NULL (и IS NOT NULL) — он всегда отвечает t или f.
Как ведут себя AND, OR и NOT с неизвестным:
TRUE AND NULL—NULL, ноFALSE AND NULL—FALSE: если одна часть ложна, остальное уже не важно.TRUE OR NULL—TRUE, ноFALSE OR NULL—NULL.NOT NULL— тожеNULL: отрицание неизвестного неизвестно.
Главное правило: WHERE и HAVING оставляют строку, только если условие равно TRUE, а ON считает пару совпавшей, только если условие равно TRUE. FALSE и NULL отбрасываются одинаково. Поэтому WHERE shipped_at = NULL не находит ни одной строки (карточка where), а WHERE manager_id <> 1 молча теряет директора. Об IS NULL — карточка is-null.
Ловушка NOT IN
NOT IN (список) раскрывается в цепочку <> … AND <> … AND …. Если в списке есть NULL, одно из сравнений всегда «неизвестно», и всё условие не может стать истинным:
SELECT id, nameFROM employeesWHERE department = 'Продажи' AND id NOT IN (SELECT manager_id FROM employees) id | name
----+------
(0 rows)Хотели продавцов, у которых нет подчинённых, а получили пустоту — хотя такие продавцы есть. Подзапрос вернул все manager_id, включая NULL директора, и id <> NULL испортил каждую строку. Ошибки нет, поэтому такой баг легко пропустить. Лечится двумя способами: отфильтровать пустые значения в подзапросе (WHERE manager_id IS NOT NULL) или написать NOT EXISTS, которому NULL не мешает (карточки in и exists). У IN без NOT этой проблемы нет.
Агрегаты пропускают NULL
COUNT(*) считает строки, а COUNT(колонка) — только непустые значения в колонке. SUM, AVG, MIN и MAX тоже просто не замечают NULL:
SELECT COUNT(*) AS orders, COUNT(shipped_at) AS shipped, DATE_TRUNC('minute', AVG(shipped_at - created_at)) AS avg_to_shipFROM orders orders | shipped | avg_to_ship
--------+---------+-----------------
200 | 111 | 3 days 06:30:00
(1 row)Заказов 200, а отгрузка записана у 111: у остальных 89 shipped_at пустой. Среднее время до отгрузки посчитано только по 111 отгруженным: для неотгруженных shipped_at - created_at — NULL, и AVG их пропускает, а не считает нулём. Обычно это ровно то, что нужно, но помни, по скольким строкам на самом деле посчитано среднее (карточки count и sum).
Обратная сторона: если непустых значений нет совсем, SUM, AVG, MIN и MAX возвращают NULL, а не 0. Только COUNT в этом случае даёт 0. Когда нужен ноль, оборачивай агрегат в COALESCE(SUM(…), 0).
NULL заразен в выражениях
Почти любое выражение с NULL внутри тоже даёт NULL: price + NULL, NULL * 2, склейка строк через ||:
SELECT id, 'Товар ' || product_id || ': ' || text AS line, 'Товар ' || product_id || ': ' || COALESCE(text, 'без текста') AS fixedFROM reviewsWHERE rating = 3ORDER BY idLIMIT 4 id | line | fixed
----+--------------------------+--------------------------
1 | Товар 17: Не понравилось | Товар 17: Не понравилось
2 | | Товар 19: без текста
4 | | Товар 18: без текста
6 | | Товар 6: без текста
(4 rows)У отзывов 2, 4 и 6 нет текста, и значение line целиком стало NULL, хотя номер товара известен. COALESCE(text, 'без текста') подставляет замену вместо NULL, и склейка снова работает (карточка coalesce). Функция CONCAT пустые значения просто пропускает — ещё один способ (карточка string-functions).
Где NULL при сортировке
При ORDER BY … ASC PostgreSQL ставит NULL в конец, а при DESC — в начало, как будто NULL больше любого значения:
SELECT id, status, shipped_atFROM ordersWHERE customer_id = 1ORDER BY shipped_at DESC, id id | status | shipped_at
-----+-----------+---------------------
78 | cancelled |
87 | paid |
182 | cancelled |
164 | shipped | 2025-08-22 22:00:00
60 | delivered | 2025-05-14 09:00:00
(5 rows)Хотели «последние отгрузки сверху», а сверху оказались заказы, которые не отгружали вовсе. ORDER BY shipped_at DESC NULLS LAST отправит пустые значения в конец, NULLS FIRST — в начало (карточка order-by). Для QA это частый баг в интерфейсе: «сортировка по дате» на экране, а сверху — строки без даты.
LEFT JOIN и условие в WHERE
LEFT JOIN сохраняет строки левой таблицы, для которых пары не нашлось, и заполняет колонки правой таблицы NULL. Если потом написать в WHERE условие на колонку правой таблицы, например p.price < 1000, для этих строк оно даст «неизвестно» — и WHERE их выбросит. LEFT JOIN незаметно превратится в обычный JOIN. Условие на правую таблицу, которое не должно терять строки, пишут в ON. Пример на живых данных — в статье «JOIN на пальцах».
Проверка «пар нет» наоборот держится на NULL: LEFT JOIN … WHERE правая.id IS NULL оставляет как раз строки без пары (anti-join).
Короткая памятка
- Пустоту проверяй только через
IS NULL/IS NOT NULL, никогда через= NULL. <>,NOT IN,NOT LIKEмолча теряют строки сNULL— спроси себя, должны ли они попасть в результат.COUNT(колонка)меньшеCOUNT(*)ровно на число пустых значений.- Выражение с
NULL— этоNULL; подставить значение по умолчанию помогаетCOALESCE. - При сортировке по колонке с пустыми значениями решай явно:
NULLS FIRSTилиNULLS LAST.