Опирается на правила:
PG-T-010…PG-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.- Тип
moneyPostgreSQL не используем — привязан к локали, нет кода валюты.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
);
| Тип | Размер | Диапазон |
|---|---|---|
smallint | 2 байта | −32 768 … 32 767 |
integer (int, int4) | 4 байта | ±2.1 миллиарда |
bigint (int8) | 8 байт | ±9.2 квинтиллиона |
integer кажется достаточным для большинства таблиц, но через несколько лет хотя бы один из таких аргументов перестаёт быть верным. Миграция int → bigint на живой большой таблице:
ALTER TYPE ... bigint—ACCESS EXCLUSIVElock, переписывает всю таблицу.- 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 / bigserial | IDENTITY | |
|---|---|---|
| Стандарт | PostgreSQL legacy macros | ANSI SQL |
| Связь sequence ↔ column | implicit (DEFAULT nextval) | через каталог |
DROP TABLE | sequence остаётся | 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)
Причины:
float/doubleхранит в двоичном — не все десятичные значения представимы точно.0.1 + 0.2 ≠ 0.3. Для денег это юридически неверный расчёт, проявится через год при сверке с банком.numericхранит как десятичные цифры — точно.- 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-010 | bigint GENERATED ALWAYS AS IDENTITY |
id serial / bigserial | PG-T-012 | bigint GENERATED ALWAYS AS IDENTITY |
amount float / double precision | PG-T-013 | numeric(p, s) |
amount money | PG-T-014 | numeric(p, s) |
Boolean как smallint, varchar, char(1) | PG-T-016 | boolean |
smallint для счётчиков, лимитов | PG-T-011 | integer/bigint |
float для score, который влияет на business decision | PG-T-015 | numeric |
Куда дальше
- PG → Числа и точность — нормативные формулировки.
- Строковые типы — text vs varchar.
- Время и таймзоны — timestamptz.
- UUID и идентификаторы — UUID v7 для distributed.
- Enum, boolean и перечисления — boolean детали.
- Антипаттерны типов — сводка.