Есть тема, о которую спотыкаются все новички без исключения, — 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 и транзакции.