SQL · Postgres

Тема 02 — NULL и трёхзначная логика

Здесь живёт «нет глубины». NULL ломает привычную логику тихо, без ошибки — и запрос молча возвращает не то. Разберём ловушки так, чтобы ты их видел заранее.

Что заберёшь с урока

Поймёшь, что NULL — это «неизвестно», а не 0 и не пустая строка. Научишься проверять его через IS NULL, «чинить» через COALESCE/NULLIF, и обходить две главные ловушки — <> и NOT IN, которые молча теряют строки.

Главный источник по теме PostgreSQL Docs — Comparison Functions (IS NULL, IS DISTINCT FROM) и Conditional Expressions (COALESCE, NULLIF). По-русски тот же материал — Функции сравнения (postgrespro).

§1 NULL — это «неизвестно», а не ноль

NULL значит «значения нет» / «неизвестно». Это не число 0 и не пустая строка ''. У Dmitry и Grace страна — NULL (мы просто не знаем её), а у Jack NULL — это email. Раз значение неизвестно, любой вопрос про него получает ответ «а бог его знает» — и вот с этого начинаются проблемы.

§2 Три значения истины: TRUE, FALSE, UNKNOWN

В обычных языках условие либо истина, либо ложь. В SQL их три: TRUE, FALSE и UNKNOWN. Любое сравнение с NULL даёт UNKNOWN — даже NULL = NULL. Запусти и посмотри (пустая ячейка в выводе — это и есть NULL/UNKNOWN):

Первый столбец — пусто (UNKNOWN), второй — t (true). Вывод: = NULL и <> NULL никогда не работают. Проверяют NULL только так:

И запомни главное про фильтр: WHERE оставляет строку, только если условие — ровно TRUE. FALSE и UNKNOWN отбрасываются одинаково. Поэтому строка с NULL тихо выпадает из выборки.

Сколько строк вернёт SELECT * FROM customers WHERE country = NULL;?

Верно: country = NULL для каждой строки даёт UNKNOWN, а WHERE оставляет только TRUE. Значит — 0 строк, без ошибки. Чтобы найти Dmitry и Grace, нужно WHERE country IS NULL.

§3 NULL «заразителен»: COALESCE и NULLIF

Почти любое выражение с NULL внутри само становится NULL. NULL + 5 → NULL. 'Hi ' || NULL → NULL (склейка строк тоже «заражается»). Два инструмента лечат это:

ФункцияЧто делаетПример
COALESCE(a, b, …)берёт первое не-NULL из спискаCOALESCE(country, 'Unknown') → страна или «Unknown»
NULLIF(a, b)даёт NULL, если a = b; иначе aNULLIF(stock, 0) → превращает 0 в NULL

COALESCE — «подставь запасное значение вместо дырки». NULLIF — обратное: «сделай это значение дыркой». Классика NULLIF — защита от деления на ноль: x / NULLIF(y, 0) вернёт NULL вместо ошибки.

§4 Две тихие ловушки: <> и NOT IN

Хочешь «всех, кроме USA» и пишешь WHERE country <> 'USA'. Для Dmitry и Grace страна NULL, значит NULL <> 'USA' → UNKNOWN → строка выпадает. Ты молча потерял два ряда, хотя они не из USA.

Ещё злее NOT IN со списком, где есть NULL: x NOT IN ('USA', NULL) становится UNKNOWN для всех строк — запрос вернёт ноль строк. Классический баг на проде.

Как правильно:

Что вернёт SELECT name FROM customers WHERE country NOT IN ('USA', NULL);?

Верно: NULL в списке делает NOT IN результатом UNKNOWN для каждой строки, и WHERE отбрасывает все. Ноль строк. Именно поэтому NOT IN с возможным NULL заменяют на NOT EXISTS или IS DISTINCT FROM.

§5 NULL в агрегатах и в JOIN

Два факта на стыке с прошлыми темами (пригодятся в упражнениях):

А из Темы 01 вспомни: порядок NULL в сортировке задаётся отдельно — ORDER BY col NULLS LAST (или NULLS FIRST).


§6 Практика · 12 упражнений

решено 0 / 12

Пиши запрос, жми «Проверить» (⌘/Ctrl+Enter). Сначала думай сам — усилие памяти и строит навык. Подсказка и решение рядом, бери их, только когда правда застрял. Держи схему под рукой. Задачи идут по нарастанию.

1. Разминка. Выведи name покупателей, у которых не указана страна (country).

2. Наоборот: выведи name покупателей, у которых есть email.

3. Одной строкой — два числа: сколько всего покупателей и у скольких заполнен email. Числа должны отличаться.

4. Выведи name и страну каждого покупателя, но вместо пустой страны показывай текст 'Unknown'. Столбцы: name, country.

5. Собери один столбец label вида Alice Johnson (alice@example.com). У кого email пустой — покажи Jack Wilson (no email). (Без COALESCE вся склейка стала бы NULL.)

6. Для каждого товара выведи name и price / stock как ratio. У двух товаров stock = 0 — прямое деление даст ошибку. Сделай так, чтобы там был NULL, а не падение.

7. Выведи name всех покупателей, чья страна не 'USA'. Важно: покупателей с неизвестной страной (NULL) тоже считаем «не из USA» и включаем. (Наивное country <> 'USA' их потеряет.)

8. Найди покупателей без единого заказа — выведи name. Здесь NOT IN безопасен: customer_id в заказах никогда не NULL.

9. Выведи order_id заказов, у которых нет оплаты (нет строки в payments). Подсказка идеи: LEFT JOIN оставит такие заказы, а поля оплаты у них будут NULL.

10. Для каждого заказа выведи order_id и оплаченную сумму amount; если оплаты нет — покажи 0. Столбцы: order_id, amount.

11. Повтор из Темы 01. Выведи name и country всех покупателей: сортировка по country в обратном алфавитном порядке (DESC), покупателей без страны (NULL) — в самое начало, при равной стране — по name по возрастанию. (важен порядок)

12. Капстоун. По каждому заказу, кроме отменённых (status <> 'cancelled'), выведи order_id, status и оплаченную сумму amount (нет оплаты → 0). Отсортируй по сумме по убыванию, при равной сумме — по order_id по возрастанию. (важен порядок)

Частые ловушки

1) = NULL / <> NULL не работают — только IS NULL / IS NOT NULL. 2) <> и NOT IN молча теряют строки с NULL — спасают IS DISTINCT FROM и NOT EXISTS. 3) Любая арифметика/склейка с NULL даёт NULL — закрывай дырки COALESCE.