Тип столбца это не формальность, а контракт о том, как значение хранится и как с ним считает СУБД. Ошибётесь с числовым типом - получите либо переполнение, либо тихую потерю копеек в деньгах. А ещё в любой таблице рано или поздно появляется 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) - деньги хранят точноКод: Выделить всё
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Код: Выделить всё
-- НЕВЕРНО: всегда 0 строк, сравнение с NULL даёт NULL
SELECT count(*) FROM flights WHERE actual_departure = NULL;
-- ВЕРНО: рейсы без фактического вылета
SELECT count(*) FROM flights WHERE actual_departure IS 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Код: Выделить всё
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 вместо ошибкиКод: Выделить всё
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 и как это связано с трёхзначной логикой?