Числовые и булевы типы, NULL и трёхзначная логика

Рейтинг: 61% · 6 голосов
Подробный курс по PostgreSQL: SQL по демобазе, типы данных и JSONB, индексы, транзакции и MVCC, VACUUM, EXPLAIN и планировщик, роли и привилегии, репликация, бэкап, настройка под нагрузку и под 1С. Актуально на 2026.
Ответить
Аватара пользователя
Egor_DBA
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Числовые и булевы типы, NULL и трёхзначная логика

Сообщение Egor_DBA »

Оглавление курса (47)
  1. Что такое PostgreSQL: история, философия, где применяется
  2. Урок 1. Что нового в PostgreSQL 15 и далее (15 -> 16 -> 17)
  3. Установка и первый запуск: Linux, Windows, Docker
  4. psql детально: метакоманды и работа в консоли
  5. Демобаза Авиаперевозки Postgres Pro: структура и развёртывание
  6. SELECT: проекция, фильтрация, сортировка
  7. Транзакции и ACID
  8. Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable
  9. Соединения таблиц (JOIN)
  10. Агрегация: GROUP BY, HAVING, GROUPING SETS
  11. Подзапросы и CTE (WITH), рекурсия
  12. Оконные функции
  13. Изменение данных: INSERT/UPDATE/DELETE, UPSERT, MERGE
  14. Числовые и булевы типы, NULL и трёхзначная логика (вы здесь)
  15. Строки, текст, дата и время, таймзоны
  16. Массивы, диапазоны, enum, составные типы
  17. JSON и JSONB: операторы и индексация
  18. DDL: CREATE TABLE, схемы, ALTER
  19. Ограничения целостности
  20. Представления и материализованные представления
  21. Последовательности, IDENTITY, генерируемые столбцы
  22. Индексы: B-tree и когда индекс не используется
  23. Типы индексов: Hash, GiST, SP-GiST, GIN, BRIN
  24. Продвинутые индексы: частичные, по выражению, покрывающие
  25. Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY
  26. Полнотекстовый поиск
  27. Роли и пользователи
  28. Привилегии: GRANT/REVOKE
  29. Row-Level Security и безопасность данных
  30. Подключение приложения: pg_hba.conf, SSL, пулы соединений
  31. Серверное программирование: функции и процедуры
  32. Триггеры и события
  33. Архитектура PostgreSQL: процессы и память
  34. MVCC: версии строк и видимость
  35. VACUUM, autovacuum, freeze и wraparound
  36. WAL, контрольные точки и долговечность
  37. Блокировки и взаимоблокировки
  38. Планировщик запросов
  39. EXPLAIN и EXPLAIN ANALYZE: чтение планов
  40. Конфигурация сервера: ключевые параметры
  41. Настройка под нагрузку и под 1С
  42. Параллельные запросы и партиционирование
  43. Резервное копирование и восстановление
  44. Репликация: потоковая и hot standby
  45. Логическая репликация и кластерные решения
  46. Внешние данные, расширения и сертификация
  47. Разбор планов запросов на explain.tensor.ru: глубокое чтение EXPLAIN ANALYZE
Урок 13. Числовые и булевы типы, NULL и трёхзначная логика

Тип столбца это не формальность, а контракт о том, как значение хранится и как с ним считает СУБД. Ошибётесь с числовым типом - получите либо переполнение, либо тихую потерю копеек в деньгах. А ещё в любой таблице рано или поздно появляется NULL, и сравнения с ним работают не так, как подсказывает интуиция. В этом уроке разберём числовые типы данных PostgreSQL, тип boolean и самое коварное - трёхзначную логику вокруг NULL, чтобы запросы выдавали то, что вы задумали.

Изображение

Как это работает

Целые делятся по размеру и диапазону. smallint занимает 2 байта (примерно от минус 32 тысяч до плюс 32 тысяч), integer - 4 байта (около плюс-минус 2.1 миллиарда), bigint - 8 байт (девятнадцать значащих цифр). Берите integer по умолчанию, bigint - под счётчики и идентификаторы, которые реально могут вырасти. smallint экономит место только в очень больших и узких таблицах, в обычной жизни выигрыш копеечный.

Дальше развилка, которую путают чаще всего: точные числа против приблизительных. numeric (он же decimal) хранит значение точно, с заданным числом знаков, и считает в десятичной системе без округлительных сюрпризов. Запись numeric(10,2) означает до 10 значащих цифр всего, из них 2 после запятой. Это единственный правильный выбор для денег и любых сумм, где копейка имеет значение.

real (4 байта) и double precision (8 байт) - это числа с плавающей точкой по стандарту IEEE 754. Они быстрые и компактные, но хранят значение приблизительно: классическое 0.1 + 0.2 тут не равно ровно 0.3. Годятся для научных расчётов, координат, метрик, где небольшая погрешность не страшна, и категорически не годятся для бухгалтерии.

serial - это не настоящий тип, а сокращение: PostgreSQL создаёт последовательность и вешает её как DEFAULT на столбец integer. Работает, но считается устаревшим стилем. Современная практика 2026 года - объявлять автоинкремент через стандартный SQL: GENERATED ALWAYS AS IDENTITY. Это переносимо, аккуратнее с правами на последовательность и не даёт случайно вставить своё значение в ключ. Подробно про IDENTITY будет в отдельном уроке.

boolean прост на вид: true, false или NULL. На вход он принимает много литералов - true, 'yes', 'on', '1', а на выход всегда отдаёт t или f. Важно помнить, что у boolean три состояния, потому что третье - это тот самый NULL.

Теперь сердце урока. NULL это не ноль и не пустая строка, а маркер отсутствия значения, читается как неизвестно. Из-за этого обычная логика становится трёхзначной: выражение может быть истинным, ложным или неизвестным. Любое сравнение с NULL через обычные операторы (=, <, <>) даёт не TRUE и не FALSE, а NULL. Поэтому WHERE x = NULL не вернёт ни одной строки никогда - корректно писать x IS NULL. В условиях WHERE строка попадает в результат только когда предикат строго TRUE; NULL отсеивается так же, как FALSE.

Для осмысленной работы с пропусками есть набор инструментов. IS NULL и IS NOT NULL проверяют наличие значения. IS DISTINCT FROM сравнивает два значения, считая два NULL равными между собой, а NULL и не-NULL - различными, и всегда возвращает TRUE или FALSE, без третьего состояния. COALESCE берёт первый не-NULL аргумент из списка - удобно подставлять значение по умолчанию. NULLIF(a, b) возвращает NULL, если a равно b, иначе a - частый приём против деления на ноль.

SQL и примеры

Посмотрим на типы прямо в демобазе Авиаперевозки (схема bookings). Сумма билета хранится как точное число, и это видно:

Код: Выделить всё

\d bookings.ticket_flights
-- столбец amount имеет тип numeric(10,2) - деньги хранят точно
Сравним точный и приблизительный счёт. numeric даёт ровный результат, double precision - погрешность представления:

Код: Выделить всё

SELECT 0.1::numeric + 0.2::numeric        AS exact_sum,
       0.1::float8 + 0.2::float8          AS float_sum,
       (0.1::float8 + 0.2::float8) = 0.3  AS float_eq_03;
-- exact_sum = 0.3, float_sum = 0.30000000000000004, float_eq_03 = false
Главная ловушка NULL на живом примере. В таблице flights у не вылетевших рейсов actual_departure пустой (NULL). Наивное сравнение с NULL не находит ничего:

Код: Выделить всё

-- НЕВЕРНО: всегда 0 строк, сравнение с NULL даёт NULL
SELECT count(*) FROM flights WHERE actual_departure = NULL;

-- ВЕРНО: рейсы без фактического вылета
SELECT count(*) FROM flights WHERE actual_departure IS NULL;
Трёхзначная логика наглядно: посчитаем рейсы по тому, известна фактическая отправка или нет. Обычное сравнение прячет неизвестные в NULL, поэтому суммы не сойдутся, если не учесть третий случай:

Код: Выделить всё

SELECT
  count(*) FILTER (WHERE actual_departure > scheduled_departure) AS departed_late,
  count(*) FILTER (WHERE actual_departure IS NULL)               AS not_departed,
  count(*)                                                       AS total
FROM flights;
-- строки с NULL не попадают в departed_late - они не TRUE и не FALSE
COALESCE и NULLIF в деле. Подставим заглушку для не вылетевших рейсов и защитимся от деления на ноль:

Код: Выделить всё

SELECT flight_no,
       coalesce(actual_departure::text, 'ещё не вылетел') AS dep_status,
       round(amount / nullif(occupied, 0), 2)            AS price_per_seat
FROM (
  SELECT f.flight_no, f.actual_departure,
         sum(tf.amount) AS amount,
         count(tf.*)    AS occupied
  FROM flights f
  LEFT JOIN ticket_flights tf ON tf.flight_id = f.flight_id
  GROUP BY f.flight_no, f.actual_departure
) s
LIMIT 10;
-- nullif(occupied,0) превращает 0 в NULL, и деление даёт NULL вместо ошибки
И boolean как выражение: вычислим признак прямо в SELECT. Сравнения возвращают boolean, его можно класть в столбец:

Код: Выделить всё

SELECT flight_no, status,
       (status = 'Arrived')          AS is_arrived,
       (actual_departure IS NOT NULL) AS has_departed
FROM flights
LIMIT 5;
Частые грабли
  • Деньги в real или double precision. Копейки уплывают из-за округления IEEE 754. Только numeric(p,s).
  • Сравнение через = NULL вместо IS NULL. Условие всегда NULL, строк ноль, а ошибки нет - тихо неправильный результат.
  • NOT IN с подзапросом, где есть NULL. Один NULL в списке превращает весь NOT IN в неопределённость, и запрос не вернёт ничего. Используйте NOT EXISTS или IS DISTINCT FROM.
  • Уверенность, что NULL = NULL даёт TRUE. Нет, даёт NULL. Если нужно равенство с учётом пропусков - IS NOT DISTINCT FROM.
  • Агрегаты молча игнорируют NULL. avg(col) считает среднее только по непустым; count(col) не равен count(*), если есть пропуски.
  • integer под идентификатор, который перерастёт 2.1 млрд. Получите ошибку переполнения в самый неподходящий момент - берите bigint.
  • CHECK или WHERE на boolean-столбце: строки со значением NULL не проходят условие col = true, их легко потерять. Думайте про три состояния.
  • UNIQUE и NULL: по умолчанию два NULL считаются различными, поэтому уникальный индекс пропускает несколько пустых. В PostgreSQL 15 появилась опция UNIQUE NULLS NOT DISTINCT, чтобы запретить такое.
Мини-лаба
  • Создайте таблицу: CREATE TABLE money_test (id bigint GENERATED ALWAYS AS IDENTITY, price_num numeric(10,2), price_flt double precision);
  • Вставьте строку со значением 0.1, потом ещё 0.2 в оба столбца и сложите суммы: SELECT sum(price_num), sum(price_flt) FROM money_test; - сравните точность.
  • Добавьте строку, где price_num оставлен пустым (NULL), и выполните SELECT count(*), count(price_num) FROM money_test; - убедитесь, что числа разные.
  • Сравните два запроса: WHERE price_num = NULL и WHERE price_num IS NULL - какой находит пропуск.
  • Проверьте трёхзначную логику явно: SELECT NULL = NULL, NULL IS NOT DISTINCT FROM NULL, COALESCE(NULL, 0, 7), NULLIF(5, 5);
  • На демобазе посчитайте через FILTER число рейсов с IS NULL и без NULL в actual_departure и убедитесь, что в сумме выходит count(*).
  • Создайте уникальный индекс с NULLS NOT DISTINCT на тестовом столбце и попробуйте вставить два NULL - поймайте отказ (нужен PostgreSQL 15+).
Контрольные вопросы
  • Чем numeric отличается от double precision и почему деньги хранят именно в numeric?
  • Что вернёт выражение 5 = NULL и почему WHERE с таким условием не находит строк?
  • В чём разница между = и IS DISTINCT FROM при сравнении значений, одно из которых NULL?
  • Как работают COALESCE и NULLIF и какую типовую ошибку (деление на ноль) закрывает NULLIF?
  • Почему serial считается устаревшим и что предлагается вместо него в современном PostgreSQL?
  • Сколько состояний у boolean в PostgreSQL и как это связано с трёхзначной логикой?
👍5 ❤️3 🔥3 😄 🤔
Аватара пользователя
basedredteam
Сообщения: 1
Зарегистрирован: 11 май 2026, 09:56

Re: Числовые и булевы типы, NULL и трёхзначная логика

Сообщение basedredteam »

А я думал serial и identity это одно и то же по факту. То есть на новых проектах лучше сразу GENERATED ALWAYS AS IDENTITY ставить и про serial забыть?
👍1 ❤️1 🔥 😄 🤔
Аватара пользователя
gall
Сообщения: 2
Зарегистрирован: 12 май 2026, 22:34

Re: Числовые и булевы типы, NULL и трёхзначная логика

Сообщение gall »

Споткнулся ровно на NOT IN с подзапросом где был один NULL, запрос полдня молча возвращал пусто. Перешёл на NOT EXISTS и всё ожило, спасибо что предупредили.
👍 ❤️1 🔥 😄 🤔
Ответить
← Предыдущая глава
Изменение данных: INSERT/UPDATE/DELETE, UPSERT, MERGE
Следующая глава →
Строки, текст, дата и время, таймзоны

Все главы курса «PostgreSQL: от первого запроса до продакшена»

Поделиться темой: ✈ Telegram VK
  • Похожие темы

Вернуться в «PostgreSQL: от первого запроса до продакшена»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя