Перечислимые значения — статус заказа, тип уведомления, валюта, роль пользователя — в схеме можно описать тремя способами. Каждый имеет цену.
1. Boolean — это boolean
PG-T-050 Для двух значений «да/нет» — тип boolean. Не smallint 0/1, не varchar('Y'/'N'), не char(1) с CHECK.
-- правильно
is_active boolean NOT NULL DEFAULT true,
is_verified boolean NOT NULL DEFAULT false
-- неправильно — у каждого пять собственных «нет», и только тип boolean гарантирует одно
is_active smallint NOT NULL DEFAULT 1 CHECK (is_active IN (0, 1)),
is_active char(1) NOT NULL DEFAULT 'Y' CHECK (is_active IN ('Y', 'N'))
Boolean занимает 1 байт, читается и пишется атомарно, не путает в SQL (WHERE is_active короче, чем WHERE is_active = 1), правильно мапится на Java boolean.
2. Три способа сделать перечисление
Допустим, у заказа есть статус: NEW, PAID, SHIPPED, DELIVERED, CANCELLED.
Вариант 1 — PG ENUM
CREATE TYPE order_status AS ENUM ('NEW', 'PAID', 'SHIPPED', 'DELIVERED', 'CANCELLED');
CREATE TABLE order_doc (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
status order_status NOT NULL DEFAULT 'NEW'
);
Плюсы:
- Компактно (4 байта на значение).
- Type-safety на уровне БД.
- Хорошо смотрится в
EXPLAIN, вWHERE status = 'PAID'.
Минусы:
- Добавление нового значения дешёвое только в PG12+ (
ALTER TYPE ... ADD VALUE). До 12 — пересоздание типа. - Удаление значения — нативно невозможно. Только пересоздание типа со всеми зависимостями (таблицы, default'ы, проверки) — большая миграция.
- Изменение порядка значений — нативно невозможно, а порядок влияет на
ORDER BY status. - Переименование значения возможно (
ALTER TYPE ... RENAME VALUE), но в PG10+. - Между сервисами/репликами enum-ы должны совпадать — лишний источник миграционной координации.
Вариант 2 — reference table
CREATE TABLE order_status_dict (
code varchar(20) PRIMARY KEY,
description text NOT NULL,
sort_order smallint NOT NULL,
is_terminal boolean NOT NULL
);
INSERT INTO order_status_dict VALUES
('NEW', 'Создан', 10, false),
('PAID', 'Оплачен', 20, false),
('SHIPPED', 'Отправлен', 30, false),
('DELIVERED', 'Доставлен', 40, true),
('CANCELLED', 'Отменён', 50, true);
CREATE TABLE order_doc (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
status varchar(20) NOT NULL REFERENCES order_status_dict(code) DEFAULT 'NEW'
);
Плюсы:
- Добавление / переименование / удаление значений — обычные
INSERT/UPDATE/DELETE. Миграция данных (UPDATE order SET status = 'CANCELLED' WHERE status = 'OLD_NAME') идёт без ALTER TYPE. - Дополнительные атрибуты (
description,sort_order,is_terminal) — там же, рядом. - Удобно админке: справочник не требует деплоя.
- Foreign key защищает от опечаток.
Минусы:
- 20 байт на значение (varchar(20)) против 4 у enum. Для редких таблиц — не важно.
- При больших таблицах — лишний джойн на каждый
SELECT. Часто решается денормализацией атрибутов в основную таблицу (еслиis_terminalнужен в каждом запросе — копировать).
Вариант 3 — text + CHECK IN (...)
CREATE TABLE order_doc (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
status text NOT NULL DEFAULT 'NEW'
CHECK (status IN ('NEW', 'PAID', 'SHIPPED', 'DELIVERED', 'CANCELLED'))
);
Плюсы:
- Никаких типов. Миграция =
ALTER TABLE ... DROP CONSTRAINT ... ADD CONSTRAINT .... - Не нужен справочник.
Минусы:
- Нет foreign key, нельзя ссылаться из других таблиц по логике перечисления.
- Дополнительные атрибуты (description, sort_order) надо тащить отдельно.
- Большой
CHECK IN (...)— некрасиво и не масштабируется (10+ значений выглядит плохо).
3. Когда что брать
PG-T-051 Правила выбора:
| Случай | Выбор |
|---|---|
| Список значений известен, редко меняется, не нужны атрибуты | ENUM |
| Список значений может расти, изредка переименовываться, нужны атрибуты (порядок, описание, флаги) | reference table |
| Список валидируется только на уровне БД, простой, без атрибутов, до 5–7 значений | CHECK IN (...) |
| Часто меняется в админке | reference table |
Практика:
- Статусы доменных сущностей (
order_status,payment_status) — обычно reference table. Они приобретают атрибуты со временем. - Технические перечисления (
event_kind,notification_channel) —ENUMилиCHECK IN. - Двузначные / трёхзначные коды (страна, валюта, язык) — отдельная reference table со стандартами (ISO 3166, 4217, 639).
4. Миграционные ловушки ENUM
PG-M-020 Добавление значения в enum (PG12+) — ALTER TYPE ... ADD VALUE 'X'. Делается мгновенно, но в отдельной транзакции.
ALTER TYPE order_status ADD VALUE 'PARTIALLY_REFUNDED';
Важно: в той же транзакции, где добавили значение, его нельзя сразу использовать:
BEGIN;
ALTER TYPE order_status ADD VALUE 'PARTIALLY_REFUNDED';
INSERT INTO order_doc (status) VALUES ('PARTIALLY_REFUNDED'); -- ОШИБКА
COMMIT;
Это значит — миграция enum и миграция данных, использующих новое значение, должны быть в разных changeset'ах Liquibase.
PG-M-021 Переименование значения (PG10+):
ALTER TYPE order_status RENAME VALUE 'OLD_NAME' TO 'NEW_NAME';
PG-M-022 Удаление значения нативно невозможно. Замена enum реализуется так:
- Создать новый тип (
order_status_v2) с нужным набором значений. - Добавить временную колонку с новым типом.
- Backfill: маппинг старых значений в новые.
- Дропнуть старую колонку, переименовать новую.
- Дропнуть старый тип.
Это две-три отдельные миграции с релизом приложения между ними. Дорого. Если предполагается, что значения будут уходить — берите reference table сразу.
5. Java-сторона: как маппить
PG-T-052 Для перечислений в Java — enum, не String.
Для PG ENUM:
- jOOQ умеет маппить — генерируется
enum-класс по типу из БД. - Spring Data JPA тоже умеет (
@Enumerated(EnumType.STRING)).
Для reference table / CHECK IN:
- Колонка типа
text/varchar, в Java мапится на свойenumчерезEnumType.STRING(или конвертер jOOQ). - Per
feedback_jooq_only— вся persistence-логика через jOOQ-генерацию + явный конвертерenum ↔ text. Свой java-enum— единственное место, где значения перечисления зафиксированы в коде; БД выступает справочником.
6. Не путайте: status vs lifecycle
Не любое поле «один из набора» — это enum. Для статусов с переходами по правилам (NEW → PAID → SHIPPED, но не NEW → SHIPPED) одного типа недостаточно — нужна state machine в коде. Тип решает только «какие значения вообще валидны», переходы — отдельная логика. См. Уровень 1 UCP — пример state machine на статусах каталожной позиции.
Чек-лист на ревью
- [ ] Boolean —
boolean, неsmallint/char(1). - [ ] Перечисления, которые могут расти и иметь атрибуты — reference table, не PG
ENUM. - [ ] Технические короткие перечисления —
ENUMлибоCHECK IN (...). - [ ] При
ENUMмиграция «добавить значение» и «использовать значение» — в разных changeset'ах. - [ ] В Java —
enum, неString, для всех перечислений.
Связанные
- Строки —
text/varcharдля значений перечисления. - Антипаттерны.