Опирается на правила: PG-T-040PG-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):

Свойствоuuidvarchar(36)
Размер16 байт36 байт + 1-4 overhead
Сравнение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 IDENTITYuuid (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 достаточен.
  • Простота debug1234 в логе проще искать.

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 (см. Миграции без даунтайма):

  1. Добавить новую колонку id_uuid uuid.
  2. Backfill: UPDATE t SET id_uuid = id::uuid; (batches).
  3. Индексы и FK на новую колонку (CREATE INDEX CONCURRENTLY).
  4. Переключить приложение читать новую.
  5. Дропнуть старую varchar(36) (отдельный релиз).

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

АнтипаттернПравилоЧто взамен
id varchar(36) для UUIDPG-T-040uuid
id char(36) для UUIDPG-T-040uuid
UUID v4 для PK/FKPG-T-041UUID v7
gen_random_uuid() (v4) на стороне БДPG-T-041UuidCreator.getTimeOrderedEpoch() Java
Random() для UUIDPG-T-041библиотека
UUID без FK-индексаPG-T-044CREATE INDEX обязателен
UUID v7 на стороне БД когда нужен до commitPG-T-042Java-side generation
bigint IDENTITY для public API URLPG-T-043uuid для exposure

Куда дальше

  • PG → UUID и идентификаторы — нормативные формулировки.
  • Числа и точность — bigint IDENTITY.
  • Строковые типы — почему не varchar(36).
  • Composite-индексы и левый префикс — FK обязательный индекс.
  • Миграции и breaking changes без даунтайма — expand-contract.
  • Test Strategy → BaseIntegrationTest — @MockitoBean UuidGenerator.
  • Distributed → idempotency — eventId UUID v7.
  • Kafka → event design — UUID v7 в payload.