Опирается на правила: PG-T-080PG-T-093 из PostgreSQL Style Guide → раздел Антипаттерны типов.

Сводный список того, что чаще всего встречается в продовых схемах и больше всего мешает потом. Каждый пункт — со ссылкой на развёрнутую статью.

1. varchar(255) по привычке

PG-T-080: наследие MySQL/Oracle.

В PostgreSQL text и varchar(n) хранятся одинаково. Длина — это CHECK, ничего больше.

→ Строковые типы

2. timestamp без TZ для бизнес-времени

PG-T-081: «локальное время непонятно где».

Через год никто не помнит, в какой зоне работал сервер. Заказ от 23:30 пятницы превращается в воскресенье после переезда на UTC-host.

→ Время и таймзоны

3. varchar(36) для UUID

PG-T-082: 16 vs 36+ байт, без валидации, чувствителен к регистру.

→ UUID и идентификаторы

4. float/real/double для денег

PG-T-083: 0.1 + 0.2 ≠ 0.3.

На длинной цепочке расчётов копятся погрешности, проявляются на сверке с банком.

→ Числа и точность

5. serial/bigserial в новой схеме

PG-T-084: устарел.

С PG10+ — GENERATED ALWAYS AS IDENTITY. Лучше связан с колонкой, корректно копируется при pg_dump.

→ Числа и точность

6. smallint 0/1 или char(1) Y/N вместо boolean

PG-T-085: boolean — 1 байт, типобезопасный, удобен в SQL.

→ Enum, boolean и перечисления

7. PG ENUM для часто-меняющегося списка

PG-T-086: удалить значение нативно невозможно. Атрибуты не привесишь.

Reference table если растёт / переименовывается / нужны атрибуты.

→ Enum, boolean и перечисления

8. JSONB как «гибкая схема» для основных полей

PG-T-087: если по полю фильтруют — это колонка.

JSONB — только полиморфные / опциональные / редко-используемые.

→ JSONB — когда оправдан, когда нет

9. Массив там, где должна быть таблица

PG-T-088: jsonb[] для order items — таблица, перевернутая на бок.

Массив — только скаляры без идентичности (теги, локали). Объекты — отдельная таблица с FK.

→ Массивы и range-типы

10. valid_from/valid_to вместо range

PG-T-089: race condition без EXCLUDE.

tstzrange / daterange + EXCLUDE USING gist (key WITH =, period WITH &&).

→ Массивы и range-типы

11. Тип money

PG-T-090: привязан к локали, нет кода валюты.

numeric(p, s) + currency char(3) (ISO 4217).

→ Числа и точность

12. LocalDateTime в Java для timestamptz

PG-T-091: теряет таймзону.

На UTC-сервере и MSK-ноутбуке — разные значения.

Instant или OffsetDateTime.

→ Время и таймзоны

13. Instant.now() напрямую в коде

PG-T-092: ломает детерминированность тестов.

DateTimeService (или аналог) с моком в integration tests.

→ Время и таймзоны

14. UUID v4 для PK

PG-T-093: субоптимально.

Случайные UUID разбрасывают вставки по btree — больше random IO, плохая упаковка страниц.

UUID v7 (timestamp-ordered).

→ UUID и идентификаторы

Сводный чек-лист на ревью схемы

Перед merge миграции:

  • [ ] Все id — bigint GENERATED ALWAYS AS IDENTITY или uuid (v7).
  • [ ] Все «когда что-то произошло» — timestamptz, не timestamp.
  • [ ] Все деньги — numeric(p, s), не float.
  • [ ] Все строки без бизнес-длины — text, не varchar(255).
  • [ ] UUID — тип uuid, не varchar(36).
  • [ ] Boolean — boolean, не smallint/char(1).
  • [ ] Перечисления, которые могут расти — reference table, не PG ENUM.
  • [ ] JSONB — только полиморфные / опциональные атрибуты, не основные поля.
  • [ ] Массивы — только скаляры без идентичности; объекты — отдельная таблица.
  • [ ] Интервалы — range-типы с EXCLUDE для непересечения.
  • [ ] На Java — Instant/OffsetDateTime для timestamptz, enum для перечислений.
  • [ ] DateTimeService и UuidGenerator вместо прямых вызовов.
  • [ ] FK обязательно с индексом.
  • [ ] Функциональные индексы для LOWER().

Чек-лист на ревью индексов

  • [ ] Composite индексы — левый префикс соответствует частому WHERE.
  • [ ] Range-условия — последние в composite.
  • [ ] FK имеет индекс (PG не строит автоматически).
  • [ ] UNIQUE через CONCURRENTLY в проде.
  • [ ] Partial index если 90% строк не нужны.
  • [ ] LOWER(email) через functional index, не seq scan.

Чек-лист на ревью миграции

  • [ ] lock_timeout в начале миграции.
  • [ ] CREATE INDEX CONCURRENTLY + runInTransaction="false".
  • [ ] SET NOT NULL через CHECK NOT VALID + VALIDATE.
  • [ ] ADD FK через NOT VALID + VALIDATE.
  • [ ] Rename — expand-contract на 3+ релиза.
  • [ ] UPDATE миллионов — backfill-job, не миграция.
  • [ ] N-1 совместимость с предыдущей версией кода.

Куда дальше

  • PG → Антипаттерны типов — нормативные формулировки.
  • Числа и точность
  • Строковые типы
  • Время и таймзоны
  • UUID и идентификаторы
  • Enum, boolean и перечисления
  • JSONB
  • Массивы и range-типы
  • Composite-индексы
  • Миграции без даунтайма