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