Числовых типов в PostgreSQL много, но в типичном бэкенде нужны три ответа: «чем считать id», «чем считать деньги», «чем считать метрики и проценты». Остальное — частные случаи.
1. Целые числа
PostgreSQL предлагает три целочисленных типа:
| Тип | Размер | Диапазон |
|---|---|---|
smallint | 2 байта | −32 768 … 32 767 |
integer (int, int4) | 4 байта | ±2.1 миллиарда |
bigint (int8) | 8 байт | ±9.2 квинтиллиона |
PG-T-010 Для id таблицы — bigint. Без вариантов.
integer кажется достаточным («у нас никогда не будет 2 миллиардов записей»), но через несколько лет один из таких аргументов всё равно перестаёт быть верным — а замена int → bigint на живой большой таблице болезненная: либо ALTER TYPE, переписывающий всю таблицу под ACCESS EXCLUSIVE lock, либо expand-contract через новую колонку. Стоимость превентивных 4 байт на строку — нулевая, стоимость миграции на бою — дни и инцидент.
PG-T-011 smallint оправдан только когда тип — нумератор фиксированной короткой шкалы (например, год рождения автомобиля, день недели, часовой пояс). Для счётчиков, лимитов, остатков — integer или bigint, не smallint.
CREATE TABLE order_item (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_id bigint NOT NULL,
quantity integer NOT NULL CHECK (quantity > 0),
weight_g integer NOT NULL -- граммы, не килограммы дробные
);
2. Генерация id: IDENTITY вместо serial
Исторически id-колонки оформляли через serial / bigserial:
CREATE TABLE foo (
id bigserial PRIMARY KEY -- не надо
);
PG-T-012 С PostgreSQL 10+ используем GENERATED ALWAYS AS IDENTITY, а не serial / bigserial.
CREATE TABLE foo (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY
);
Отличия:
serial— это макрос: создаётсяsequenceи колонкаint/bigintсDEFAULT nextval(...). Связь между sequence и колонкой не явная — это породило поколение проблем приpg_dumpи при перепривязке последовательностей.IDENTITY— стандарт SQL. Sequence привязана к колонке через каталог, приDROP TABLEуезжает вместе с таблицей.ALWAYSзапрещает явныеINSERT id = ?— это полезно: в обычной работе никто не должен подсовывать свои id.BY DEFAULT(мягкая версияIDENTITY) — если иногда нужна явная вставка id (например, при миграции данных), но писать вручную каждый день не приходится.
Когда не годится: распределённые сервисы, которым нужен глобально уникальный id без обращения к одной БД. Тогда — UUID v7 (см. UUID).
3. Деньги: numeric, не float
PG-T-013 Деньги, проценты, налоги, тарифы — numeric(p, s). Никогда не real / double precision.
amount_total numeric(15, 2) NOT NULL, -- до 13 цифр до запятой, 2 после
exchange_rate numeric(20, 8) NOT NULL, -- курсы: 8 знаков после
discount_percent numeric(5, 2) NOT NULL CHECK (discount_percent BETWEEN 0 AND 100)
Причины:
float/doubleхранит число в двоичном виде, и не все десятичные значения представимы точно. Классический пример:0.1 + 0.2 ≠ 0.3. Для денег это не «погрешность округления», это юридически неверный расчёт, который проявится через год при сверке с банком.numericхранит число как набор десятичных цифр — точно, без округления.- Производительность:
numericмедленнееbigintна простых операциях, но для финансовых расчётов в OLTP это не узкое место.
Альтернатива: хранить деньги в копейках (целое в bigint). Вариант рабочий, но ограничен:
- неудобно для систем с переменной точностью (BTC требует 8 знаков, валюты — 2–3);
- требует дисциплины во всём приложении: один забытый делитель / умножитель портит данные.
Для типичного маркетплейса/SaaS-биллинга — numeric(p, s) со стандартной точностью валют.
PG-T-014 Тип money PostgreSQL не используем. Он привязан к глобальной локали сервера и не хранит код валюты. Для мультивалютной системы бесполезен, для одновалютной — numeric всё равно лучше.
4. Метрики, проценты, физические величины
PG-T-015 real (float4) и double precision (float8) применяем только когда уместна неточность.
Допустимо:
- временные ряды метрик (CPU usage, latency p95);
- научные расчёты, где входы уже неточные (вес, температура, расстояние);
- ML-фичи, embeddings.
Недопустимо:
- деньги, учётные количества, баллы лояльности — всё, что должно сходиться при сравнении.
5. Boolean
PG-T-016 Boolean — это boolean. Не smallint 0/1, не varchar('Y'/'N'), не char(1) с CHECK.
is_active boolean NOT NULL DEFAULT true,
is_deleted boolean NOT NULL DEFAULT false
Подробнее — в статье Enum и boolean.
Чек-лист на ревью
- [ ] Все id-колонки —
bigint GENERATED ALWAYS AS IDENTITY(или UUID v7). - [ ] Все денежные колонки —
numeric(p, s)с явной точностью. - [ ] Нет
serial/bigserialв новой схеме. - [ ] Нет
float/real/double precisionдля финансовых полей. - [ ] Нет
smallintдля счётчиков и лимитов (только для коротких фиксированных шкал). - [ ] Нет
moneyPostgreSQL.
Связанные
- Строки —
textпротивvarchar(n). - UUID и идентификаторы — альтернатива
IDENTITYдля распределённых случаев. - Антипаттерны — полный список.