Опирается на правила:
PG-T-040…PG-T-044из PostgreSQL Style Guide → раздел UUID и идентификаторы.
Важно знать
- Тип
uuid(16 байт). Никогдаvarchar(36)/char(36)/text(36+ байт, посимвольное сравнение, без валидации).- UUID v7 для PK/FK, не v4. v7 = 48-бит timestamp + 74 случайных бита — соседние по времени id близки в btree.
- v4 случаен — соседние вставки в случайные позиции btree, страницы заполняются на 30-60%, cache trashing.
- v7 — страницы заполняются на 90%+, range-scan для «последние N» возможен.
- Генерация на стороне приложения (
UuidCreator.getTimeOrderedEpoch()) — id известен до commit.bigint IDENTITYдешевле, UUID — когда нужен по делу (multi-service, public API, до commit).- bigint для внутренних FK + uuid для public_id — типичный middle ground.
- При UUID-PK — обязательно индекс по FK с такой же
uuid-колонкой.
UUID — стандарт для распределённых сервисов: id генерируется на клиенте до записи, нет конфликтов между сервисами, удобно ссылаться извне. Цена — производительность на больших таблицах. UCP формулирует: тип uuid (не varchar(36)), версия v7 (не v4), генерация на стороне Java.
Тип uuid, не varchar(36)
PG-T-040:
-- ✓
CREATE TABLE customer (
id uuid PRIMARY KEY,
name text NOT NULL
);
-- ✗
CREATE TABLE customer_bad (
id varchar(36) PRIMARY KEY,
name text NOT NULL
);
Что теряем при varchar(36):
| Свойство | uuid | varchar(36) |
|---|---|---|
| Размер | 16 байт | 36 байт + 1-4 overhead |
| Сравнение | 2× int64 ops | посимвольное |
| Валидация формата | при INSERT | пропускает 'not-a-uuid' |
| Нормализация регистра | да | нет (два UUID разного регистра = разные) |
На больших схемах с FK — десятки гигабайт экономии.
v7 vs v4
PG-T-041: для PK/FK — UUID v7.
v4 — случайные id, проблема для btree
INSERT a1b2c3... → btree page 47
INSERT 4d5e6f... → btree page 9123 (другой блок)
INSERT 9a8b7c... → btree page 218
Соседние вставки попадают в случайные позиции индекса:
- Каждая вставка тащит из диска новую страницу.
- B-tree разбивает страницы при вставках в середину → заполняются на 30-60%.
- Postgres cache постоянно вытесняется.
- Range scan «последние 100 заказов» невозможен — id перемешаны по диску.
На таблице 100M строк — 2-5x разница в скорости вставки + просадки prograwn cache.
v7 — time-sortable
UUID v7 = 48-bit timestamp + 74 случайных бита + версия.
2026-05-07T12:00:00.001 → 0190abcd-...
2026-05-07T12:00:00.002 → 0190abce-...
2026-05-07T12:00:00.003 → 0190abcf-...
Все попадают в одну страницу btree:
- Страницы заполняются на 90%+.
- Cache не вытесняется при последовательных вставках.
- Range scan «последние N заказов» = range scan по индексу (быстро).
Плюсы UUID сохраняются:
- Глобальная уникальность без координации.
- Не угадывается извне (74 случайных бита достаточно).
- Генерится на клиенте.
Цена перехода — никакая. Тот же тип uuid PostgreSQL, тот же 16-байтный формат. Меняется только генератор.
Генерация UUID v7
PG-T-042: на стороне приложения.
Java — рекомендуется
implementation("com.github.f4b6a3:uuid-creator:5.3.7")
import com.github.f4b6a3.uuid.UuidCreator;
UUID id = UuidCreator.getTimeOrderedEpoch(); // UUID v7
Преимущество — id известен до записи:
- Можно сразу публиковать события.
- Возвращать клиенту до commit (через outbox).
- Использовать в логах с самого начала transaction.
PostgreSQL 18+
INSERT INTO foo (id) VALUES (uuidv7());
Встроенная функция. До PG 18 — расширение или Java-генерация.
В UCP — преимущественно Java-генерация (через UuidGenerator bean, см. Test Strategy → BaseIntegrationTest).
bigint IDENTITY vs UUID
PG-T-043: выбор зависит от требований.
| Критерий | bigint IDENTITY | uuid (v7) |
|---|---|---|
| Размер | 8 байт | 16 байт |
| Performance | быстрее | чуть медленнее |
| Multi-service uniqueness | нужна координация | автоматическая |
| Public exposure | подсказывает объёмы (/order/12345) | opaque |
| Id до commit | нет (sequence срабатывает в INSERT) | да |
| Debug | проще (1234 vs UUID) | сложнее |
UUID оправдан когда:
- Multi-service — id должен быть глобально уникальным без координации.
- Public API — id наружу, не должен подсказывать порядок и объёмы.
/order/12345показывает «у нас примерно 12K заказов». - Id до commit — нужен сразу (для outbox event, для возврата клиенту).
bigint IDENTITY оправдан когда:
- Один сервис, одна БД.
- Внешний контракт не требует UUID — internal id достаточен.
- Простота debug —
1234в логе проще искать.
Middle ground — оба
CREATE TABLE order_doc (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- внутренний
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(), -- внешний (можно v7)
...
);
bigint— для FK и быстрых join.uuid— для API и URL.
Типичная практика для marketplace.
UUID + FK = индекс обязателен
PG-T-044:
PostgreSQL не строит индекс по FK автоматически. На малых таблицах это не страшно, на больших — DELETE родителя уйдёт в seq-scan дочерней.
CREATE TABLE order_doc (
id uuid PRIMARY KEY
);
CREATE TABLE order_item (
id uuid PRIMARY KEY,
order_id uuid NOT NULL REFERENCES order_doc(id)
);
-- ✓ — индекс обязателен для UUID FK
CREATE INDEX ix_order_item_order_id ON order_item(order_id);
Без индекса:
DELETE FROM order_doc WHERE id = ?— seq scan поorder_itemдля cascade check.- На таблице 100M строк — секунды на каждый delete.
Подробнее — Composite-индексы и левый префикс.
Миграция varchar(36) → uuid
Expand-contract pattern (см. Миграции без даунтайма):
- Добавить новую колонку
id_uuid uuid. - Backfill:
UPDATE t SET id_uuid = id::uuid;(batches). - Индексы и FK на новую колонку (
CREATE INDEX CONCURRENTLY). - Переключить приложение читать новую.
- Дропнуть старую
varchar(36)(отдельный релиз).
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
id varchar(36) для UUID | PG-T-040 | uuid |
id char(36) для UUID | PG-T-040 | uuid |
| UUID v4 для PK/FK | PG-T-041 | UUID v7 |
gen_random_uuid() (v4) на стороне БД | PG-T-041 | UuidCreator.getTimeOrderedEpoch() Java |
Random() для UUID | PG-T-041 | библиотека |
| UUID без FK-индекса | PG-T-044 | CREATE INDEX обязателен |
| UUID v7 на стороне БД когда нужен до commit | PG-T-042 | Java-side generation |
bigint IDENTITY для public API URL | PG-T-043 | uuid для exposure |
Куда дальше
- PG → UUID и идентификаторы — нормативные формулировки.
- Числа и точность —
bigint IDENTITY. - Строковые типы — почему не
varchar(36). - Composite-индексы и левый префикс — FK обязательный индекс.
- Миграции и breaking changes без даунтайма — expand-contract.
- Test Strategy → BaseIntegrationTest —
@MockitoBean UuidGenerator. - Distributed → idempotency —
eventIdUUID v7. - Kafka → event design — UUID v7 в payload.