Опирается на правила: PG-T-010PG-T-016 из PostgreSQL Style Guide → раздел Числа и точность.

Важно знать

  • Id таблицы — bigint, без вариантов. integer кажется достаточным, миграция int→bigint на бою болезненна.
  • smallint — только для нумератора фиксированной шкалы (день недели, часовой пояс).
  • GENERATED ALWAYS AS IDENTITY вместо serial/bigserial (PG 10+).
  • Деньги — numeric(p,s), никогда real/double precision (0.1 + 0.2 ≠ 0.3).
  • Альтернатива деньгам — копейки в bigint, но ограничена для multi-currency.
  • Тип money PostgreSQL не используем — привязан к локали, нет кода валюты.
  • real/double — только когда неточность уместна (метрики, ML).
  • Boolean — это boolean, не smallint, не varchar('Y'/'N').

В типичном backend нужны три ответа: «чем считать id», «чем считать деньги», «чем считать метрики». Остальное — частные случаи.

bigint для id — без вариантов

PG-T-010:

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
);
ТипРазмерДиапазон
smallint2 байта−32 768 … 32 767
integer (int, int4)4 байта±2.1 миллиарда
bigint (int8)8 байт±9.2 квинтиллиона

integer кажется достаточным для большинства таблиц, но через несколько лет хотя бы один из таких аргументов перестаёт быть верным. Миграция int → bigint на живой большой таблице:

  • ALTER TYPE ... bigintACCESS EXCLUSIVE lock, переписывает всю таблицу.
  • Expand-contract — новая колонка + копирование + переключение.

Любой вариант — дни работы и риск инцидента. Стоимость превентивных 4 байт на строку — нулевая.

smallint — только для нумератора фиксированной шкалы

PG-T-011:

-- ✓
day_of_week     smallint NOT NULL CHECK (day_of_week BETWEEN 1 AND 7),
timezone_offset smallint NOT NULL,           -- минуты от UTC

-- ✗ — счётчики, лимиты
order_count     smallint NOT NULL            -- может вырасти за пределы

Для счётчиков, лимитов, остатков — integer или bigint. smallint экономит 2 байта, не оправдывает риск переполнения.

GENERATED ALWAYS AS IDENTITY

PG-T-012: с PG 10+ стандартный способ.

-- ✓
CREATE TABLE foo (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY
);

-- ✗ — устарело
CREATE TABLE foo (
    id bigserial PRIMARY KEY
);
serial / bigserialIDENTITY
СтандартPostgreSQL legacy macrosANSI SQL
Связь sequence ↔ columnimplicit (DEFAULT nextval)через каталог
DROP TABLEsequence остаётсяsequence уезжает
Запрет явного INSERT id=?нетALWAYS — да
pg_dump corner-casesестьнет

BY DEFAULT AS IDENTITY — мягкая версия, разрешает явный insert (для миграции данных).

Когда не годится: распределённые сервисы, которым нужен глобально уникальный id без БД-roundtrip. Тогда — UUID v7 (см. UUID).

Деньги — numeric(p,s)

PG-T-013: никогда float.

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. Performance: numeric медленнее bigint, но в OLTP не узкое место.

Альтернатива — копейки в bigint

amount_cents bigint NOT NULL CHECK (amount_cents >= 0)

Рабочий вариант, но ограничен:

  • Неудобно для систем с переменной точностью (BTC — 8 знаков, fiat — 2-3).
  • Требует дисциплины: один забытый делитель в коде = баг.

Для типичного marketplace/SaaS-биллинга — numeric(p,s).

Тип money не используем

PG-T-014:

money PostgreSQL:

  • Привязан к глобальной локали сервера.
  • Не хранит код валюты.

Для мультивалютной системы — бесполезен. Для одновалютной — numeric всё равно лучше.

real/double только для неточности

PG-T-015:

Допустимо:

  • Временные ряды метрик — CPU usage, latency p95.
  • Научные расчёты — вес, температура, расстояние (входы уже неточные).
  • ML-фичи, embeddings.

Недопустимо:

  • Деньги, учётные количества, баллы лояльности — всё, что должно сходиться при сравнении.

Boolean — это boolean

PG-T-016:

-- ✓
is_active   boolean NOT NULL DEFAULT true,
is_deleted  boolean NOT NULL DEFAULT false

-- ✗
is_active   smallint NOT NULL DEFAULT 1,
is_active   varchar(1) NOT NULL DEFAULT 'Y' CHECK (is_active IN ('Y','N')),
is_active   char(1) NOT NULL DEFAULT 'Y'

boolean — 1 байт, точно отражает семантику. Подробнее — Enum, boolean и перечисления.

Что запрещено

АнтипаттернПравилоЧто взамен
id integer (или int4)PG-T-010bigint GENERATED ALWAYS AS IDENTITY
id serial / bigserialPG-T-012bigint GENERATED ALWAYS AS IDENTITY
amount float / double precisionPG-T-013numeric(p, s)
amount moneyPG-T-014numeric(p, s)
Boolean как smallint, varchar, char(1)PG-T-016boolean
smallint для счётчиков, лимитовPG-T-011integer/bigint
float для score, который влияет на business decisionPG-T-015numeric

Куда дальше

  • PG → Числа и точность — нормативные формулировки.
  • Строковые типы — text vs varchar.
  • Время и таймзоны — timestamptz.
  • UUID и идентификаторы — UUID v7 для distributed.
  • Enum, boolean и перечисления — boolean детали.
  • Антипаттерны типов — сводка.