Опирается на правила: PG-T-050PG-T-052, PG-M-020PG-M-022 из PostgreSQL Style Guide → раздел Enum, boolean и перечисления.

Важно знать

  • Boolean — это boolean, не smallint 0/1, не varchar('Y'/'N').
  • 3 способа перечисления: PG ENUM, reference table, text + CHECK IN.
  • ENUM — 4 байта, type-safety, но удаление значения нативно невозможно.
  • Reference table — 20 байт + JOIN, но full CRUD + дополнительные атрибуты.
  • CHECK IN — простой, без FK, до 5-7 значений.
  • Статусы доменных сущностей — обычно reference table (растут, приобретают атрибуты).
  • Javaenum, не String. @Enumerated(EnumType.STRING) для JPA.
  • status vs lifecycle — переходы это отдельная state machine, не тип.

Перечислимые значения — статус заказа, тип уведомления, валюта, роль пользователя. UCP формулирует три способа и таблицу выбора между ними.

Boolean — boolean

PG-T-050:

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

-- ✗
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 vs WHERE is_active = 1.
  • Правильно мапится на Java boolean.

Три способа

Пример: статус заказа 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 — пересоздание типа.
  • Удаление значения — нативно невозможно. Только пересоздание типа со всеми зависимостями = большая миграция.
  • Порядок влияет на ORDER BY status, изменение порядка невозможно.
  • Переименование — PG10+ через RENAME VALUE.
  • Между сервисами/репликами 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'
);

Плюсы:

  • Full CRUD на значения: INSERT/UPDATE/DELETE.
  • Миграция данных без ALTER TYPE (UPDATE order SET status = 'X' WHERE status = 'OLD').
  • Дополнительные атрибуты (description, sort_order, is_terminal) рядом.
  • Удобно админке — справочник без деплоя.
  • FK защищает от опечаток.

Минусы:

  • 20 байт на значение (varchar(20)) против 4 у enum.
  • При больших таблицах — лишний JOIN на каждом SELECT (часто решается денормализацией).

Вариант 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 ....
  • Не нужен справочник.

Минусы:

  • Нет FK, нельзя ссылаться из других таблиц по логике перечисления.
  • Дополнительные атрибуты — отдельно.
  • 10+ значений в CHECK IN — некрасиво.

Когда что брать

PG-T-051:

СлучайВыбор
Известный, редко меняется, без атрибутовENUM
Может расти, переименовываться, нужны атрибутыreference table
Простой, до 5-7 значений, без атрибутовCHECK IN
Часто меняется в админкеreference table

Практика:

  • Статусы доменных сущностей (order_status, payment_status) — reference table. Приобретают атрибуты со временем (is_terminal, sort_order).
  • Технические перечисления (event_kind, notification_channel) — ENUM или CHECK IN.
  • 2-3-буквенные коды (страна, валюта, язык) — reference table со стандартами (ISO 3166, 4217, 639).

Миграционные ловушки ENUM

ADD VALUE — мгновенно но в отдельной tx

PG-M-020:

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.

RENAME VALUE — PG10+

PG-M-021:

ALTER TYPE order_status RENAME VALUE 'OLD_NAME' TO 'NEW_NAME';

DROP VALUE — невозможно нативно

PG-M-022: замена enum через несколько релизов.

  1. Создать новый тип (order_status_v2) с нужным набором.
  2. Добавить временную колонку с новым типом.
  3. Backfill: маппинг старых значений в новые.
  4. Дропнуть старую колонку, переименовать новую.
  5. Дропнуть старый тип.

Две-три отдельные миграции с релизом приложения между ними. Если предполагается удаление значений — берите reference table сразу.

Java-сторона

PG-T-052: enum, не String.

public enum OrderStatus {
    NEW, PAID, SHIPPED, DELIVERED, CANCELLED
}

// JPA
@Enumerated(EnumType.STRING)
private OrderStatus status;

// jOOQ — генерирует enum по типу из БД (если PG ENUM)
// либо явный конвертер для reference table / CHECK IN

Для reference table / CHECK IN — колонка text/varchar, в Java enum через EnumType.STRING или jOOQ конвертер.

status vs lifecycle

Не любое поле «один из набора» — это enum. Для статусов с переходами по правилам (NEW → PAID → SHIPPED, но не NEW → SHIPPED) одного типа недостаточно. Тип решает «какие значения вообще валидны», переходы — отдельная state machine в коде (см. DDD → aggregate).

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

АнтипаттернПравилоЧто взамен
Boolean как smallint 0/1PG-T-050boolean
Boolean как char(1) 'Y'/'N'PG-T-050boolean
ENUM для часто меняющегося набораPG-T-051reference table
CHECK IN (...) с 10+ значениямиPG-T-051reference table
ADD VALUE + использование в одной txPG-M-020разные changeset
Удаление enum-значения через UPDATE → DROPPG-M-022новый тип + миграция
Java String для statusPG-T-052enum
Reference table без FKPG-T-051REFERENCES order_status_dict(code)
Тип-уровень для state machine переходовбез правилаотдельный domain code

Куда дальше

  • PG → Enum, boolean и перечисления — нормативные формулировки.
  • Числа и точность — boolean детали.
  • Строковые типы — varchar(20) для кодов.
  • JSONB — когда оправдан, когда нет — для сложных enum-объектов.
  • Миграции и breaking changes без даунтайма — drop enum value.
  • DDD → aggregate — state machine для переходов.
  • Антипаттерны типов — сводка.