Есть тема, о которую спотыкаются все новички без исключения, — NULL. Запрос выглядит правильно, а возвращает не то; счётчики расходятся; строки исчезают из результата. В девяти случаях из десяти под капотом NULL. Разберёмся с ним и заодно с типами данных — второй половиной тех же граблей.
NULL — это «неизвестно», а не ноль
NULL — отметка «значения нет». Это не ноль, не пустая строка '', а именно отсутствие: у заказа не заполнена дата доставки, у клиента не указан телефон. И у NULL особая логика: любое сравнение с ним даёт не истину и не ложь, а «неизвестно».
SELECT * FROM customers WHERE phone = NULL; -- всегда пусто!
Этот запрос не вернёт ни строки — даже если незаполненных телефонов тысячи. «Равно неизвестно чему?» — «неизвестно», и строка в результат не попадает. Для NULL есть специальные операторы:
SELECT * FROM customers WHERE phone IS NULL; -- телефона нет
SELECT * FROM customers WHERE phone IS NOT NULL; -- телефон есть
Коварнее другое: NULL прячется и в отрицаниях. WHERE status <> 'paid' вернёт заказы со статусом, отличным от paid, — но не вернёт заказы, где статус NULL: сравнение с неизвестным не истинно. Хотите «все, кроме оплаченных, включая пустые» — пишите WHERE status <> 'paid' OR status IS NULL.
Ещё три места, где NULL меняет результат:
- Агрегаты игнорируют NULL: отсюда разница
COUNT(*)иCOUNT(phone)из статьи про агрегаты;AVGсчитает среднее только по заполненным. - LEFT JOIN порождает NULL в столбцах таблицы, где не нашлось пары, — мы пользовались этим для поиска «сирот».
- Арифметика с NULL даёт NULL:
amount + NULL— это NULL, и сумма «испаряется».
Подставить замену вместо NULL умеет функция COALESCE: COALESCE(phone, 'не указан') вернёт телефон, а при его отсутствии — заглушку.
Типы: почему '10' — не 10
У каждого столбца есть тип, и сравнивать нужно в своём типе:
- Числа (
integer,numeric) — без кавычек:amount > 1000. Строка'10'и число10— разные вещи: строки сравниваются посимвольно, поэтому в строковом столбце'9' > '10'(девятка «больше» единицы). Если числа в базе внезапно сортируются как1, 10, 2, 20— столбец строковый, и это само по себе интересная находка. - Строки (
varchar,text) — в одинарных кавычках, с точностью до регистра и пробелов. - Булев тип (
boolean) —true/false:WHERE is_active = true. В некоторых базах вместо него числа 0/1 — смотрите на реальные данные. - Даты и время (
date,timestamp) — в кавычках в формате'2026-07-08'. Диапазоны дат удобны через BETWEEN или сравнения:WHERE created_at >= '2026-07-01' AND created_at < '2026-08-01'— все записи июля. Обратите внимание на приём «начало следующего месяца, строго меньше»: он честно захватывает записи за последний день с временем 23:59:59. - Часовые пояса — тема, на которую натыкаются внезапно:
timestampможет храниться в UTC, а интерфейс показывает местное время. Если запись «созданная в 01:30» находится в базе «вчерашним» временем — это, скорее всего, не баг, а UTC.
Где это применяется
NULL — это карта минных полей продукта: незаполненные поля — ровно те места, где интерфейс, отчёты и интеграции ведут себя неожиданно. Запрос SELECT COUNT(*) FROM t WHERE важное_поле IS NULL — экспресс-аудит качества данных в любой таблице. А понимание типов избавляет от ложных тревог: «сортировка сломалась» (нет, столбец строковый), «время на час уехало» (нет, это часовой пояс).
Где спотыкаются начинающие:
- Пишут
= NULLвместоIS NULL— запрос молча возвращает пустоту, и кажется, что данных нет. - Забывают, что
<>не находит NULL. «Все кроме оплаченных» безOR status IS NULLтеряет строки — и никто не замечает. - Сравнивают дату-время с «просто датой» и удивляются:
WHERE created_at = '2026-07-08'не находит записи за день — потому что сравнивает с полуночью. Диапазон>= сегодня AND < завтранадёжнее.
Что учить дальше. Читать вы уже умеете во всех видах — пора научиться аккуратно менять данные: INSERT, UPDATE, DELETE и транзакции.