Числовых типов в PostgreSQL много, но в типичном бэкенде нужны три ответа: «чем считать id», «чем считать деньги», «чем считать метрики и проценты». Остальное — частные случаи.

1. Целые числа

PostgreSQL предлагает три целочисленных типа:

ТипРазмерДиапазон
smallint2 байта−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)

Причины:

  1. float / double хранит число в двоичном виде, и не все десятичные значения представимы точно. Классический пример: 0.1 + 0.2 ≠ 0.3. Для денег это не «погрешность округления», это юридически неверный расчёт, который проявится через год при сверке с банком.
  2. numeric хранит число как набор десятичных цифр — точно, без округления.
  3. Производительность: 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 для счётчиков и лимитов (только для коротких фиксированных шкал).
  • [ ] Нет money PostgreSQL.

Связанные

  • Строки — text против varchar(n).
  • UUID и идентификаторы — альтернатива IDENTITY для распределённых случаев.
  • Антипаттерны — полный список.