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

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

PG-T-080 Антипаттерн. varchar(255) пришёл из MySQL/Oracle и в PostgreSQL не имеет смысла. text и varchar(n) хранятся одинаково и работают одинаково быстро — длина это просто CHECK.

Что делать:

  • Если длина — доменное правило (формат phone E.164, ИНН, ISO-страна) — varchar(n) с явным n по делу.
  • Если просто «ну надо ограничить» — text без длины.

Подробно: Строки, правила PG-T-020, PG-T-021.

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

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

Что делать:

  • timestamptz для всех «когда что-то произошло».
  • timestamp без зоны — только для локального времени с явной отдельной зоной (расписание открытия магазина в локали города).

Подробно: Время и таймзоны, правило PG-T-030.

3. varchar(36) / char(36) / text для UUID

PG-T-082 Антипаттерн. UUID — это 16 байт, а varchar(36) — 36+ байт, без валидации формата, медленнее на сравнениях, чувствителен к регистру.

Что делать:

  • Тип uuid.
  • Для PK / FK — UUID v7, не v4.

Подробно: UUID и идентификаторы, правила PG-T-040, PG-T-041.

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

PG-T-083 Антипаттерн. Двоичная плавающая точка не представляет десятичные дроби точно. 0.1 + 0.2 уже ≠ 0.3. На длинной цепочке расчётов копятся погрешности, которые проявляются на сверке с банком и при налоговой отчётности.

Что делать:

  • numeric(p, s) для всех денежных полей.
  • Для интенсивных расчётов — bigint в копейках с дисциплиной во всём приложении.

Подробно: Числа и точность, правило PG-T-013.

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

PG-T-084 Устаревший паттерн. С PG10+ есть стандартный GENERATED ALWAYS AS IDENTITY. Он лучше связан с колонкой, корректно копируется при pg_dump, защищён от случайных явных вставок.

Что делать:

  • bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY — стандарт.
  • BY DEFAULT — мягкая версия, если бывают старые импорты с явными id.

Подробно: Числа и точность, правило PG-T-012.

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

PG-T-085 Антипаттерн. В PostgreSQL есть boolean — 1 байт, типобезопасный, читается через WHERE is_active. У smallint/char(1) — лишний CHECK, путаница с типами, неудобство в SQL.

Что делать:

  • boolean NOT NULL DEFAULT false.

Подробно: Enum и boolean, правило PG-T-050.

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

PG-T-086 Антипаттерн. Удалить значение из enum нативно невозможно. Переименовать — можно с PG10+, но координация миграции с релизами тяжёлая. Дополнительные атрибуты (порядок, описание) к enum не привесишь.

Что делать:

  • Если значения растут / переименовываются / имеют атрибуты — reference table.
  • Если это техническое короткое перечисление до 5–7 значений — ENUM или CHECK IN.

Подробно: Enum и boolean, правило PG-T-051.

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

PG-T-087 Антипаттерн. Если по полю регулярно фильтруют, сортируют или джойнят — это колонка, не JSON-ключ. JSONB не даёт типобезопасности, сложнее индексируется и через год становится «мы фактически document store, но без преимуществ».

Что делать:

  • В JSONB — только полиморфные / опциональные / редко-используемые атрибуты (содержимое события аудита, настройки интеграций, специфичные атрибуты товара).
  • Все «горячие» поля выносить в обычные колонки с типами и индексами.

Подробно: JSONB, правило PG-T-061.

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

PG-T-088 Антипаттерн. jsonb[] или record[] для строк заказа, элементов корзины, атрибутов с весом — это таблица, перевернутая на бок. Теряются FK, валидация на элемент, INSERT/UPDATE отдельной строки, эффективная индексация.

Что делать:

  • Массив — только простые скалярные значения без идентичности и атрибутов (теги, локали, разрешения).
  • Всё остальное — отдельная таблица с FK.

Подробно: Массивы и range-типы, правило PG-T-071.

10. Две колонки valid_from / valid_to вместо range-типа

PG-T-089 Антипаттерн. Когда сущность семантически — это интервал, две колонки требуют дублировать логику пересечений / включений / открытых концов в каждом запросе. Race-condition при «не пересекаются» решается только триггерами или application-level lock-ами.

Что делать:

  • tstzrange / daterange / int4range.
  • Для непересечения — EXCLUDE USING gist (key WITH =, period WITH &&).

Подробно: Массивы и range-типы, правила PG-T-072, PG-T-073.

11. Тип money

PG-T-090 Антипаттерн. Привязан к глобальной локали сервера. Не хранит код валюты. Для мультивалютной системы бесполезен, для одновалютной — numeric всё равно лучше.

Что делать:

  • numeric(p, s) + отдельная колонка currency char(3) (ISO 4217).

Подробно: Числа и точность, правило PG-T-014.

12. LocalDateTime в Java для timestamptz-колонки

PG-T-091 Антипаттерн. LocalDateTime теряет таймзону. На сервере с TZ=UTC и на ноутбуке разработчика с TZ=Europe/Moscow один и тот же запрос вернёт разные значения.

Что делать:

  • Instant или OffsetDateTime для timestamptz.
  • LocalDateTime — только для timestamp without time zone (что само по себе нежелательно для бизнес-времени).

Подробно: Время и таймзоны, правило PG-T-031.

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

PG-T-092 Антипаттерн. Делает поведение зависимым от системного времени сервера, ломает детерминированность тестов.

Что делать:

  • DateTimeService (или аналог) с моком в интеграционных тестах.

Подробно: Время и таймзоны, правило PG-T-034.

14. UUID v4 для PK

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

Что делать:

  • UUID v7 (timestamp-ordered) для PK / FK.
  • v4 — только если важна максимальная непредсказуемость id (security-чувствительные случаи) и проседание производительности приемлемо.

Подробно: UUID и идентификаторы, правило PG-T-041.


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

Перед 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, не Instant.now().

Связанные

  • Числа и точность
  • Строки
  • Время и таймзоны
  • UUID и идентификаторы
  • Enum и boolean
  • JSONB
  • Массивы и range-типы