UUID (Universally Unique Identifier) — 128-битный идентификатор, гарантированно уникальный без координации между серверами. Разберём когда UUID оправдан, а когда лучше остаться на SERIAL.
UUID vs SERIAL: основная разница
-- SERIAL: автоинкремент, генерируется БД
CREATE TABLE orders_serial (
id SERIAL PRIMARY KEY,
amount NUMERIC
);
-- UUID: глобально уникальный, можно генерировать где угодно
CREATE TABLE orders_uuid (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
amount NUMERIC
);
| SERIAL / BIGSERIAL | UUID | |
|---|---|---|
| Размер | 4 / 8 байт | 16 байт |
| Генерация | Только в БД | В БД и приложении |
| Предсказуемость | Да (1, 2, 3...) | Нет |
| Монотонность | Да | UUID v4: нет |
| Читаемость | Высокая | Низкая |
| Индексная фрагментация | Низкая | Высокая (UUID v4) |
Типы UUID в PostgreSQL
UUID v4 (случайный) — по умолчанию
SELECT gen_random_uuid();
-- d7c5f3a1-8b2e-4c9d-a0e7-1f3b5d6e8c2a
Функция gen_random_uuid() встроена в PostgreSQL 13+. До этого нужно расширение pgcrypto.
-- Для совместимости
CREATE EXTENSION IF NOT EXISTS pgcrypto;
SELECT gen_random_uuid(); -- или uuid_generate_v4()
UUID v7 (временно-упорядоченный) — PostgreSQL 17+
-- PostgreSQL 17 добавил uuidv7()
SELECT uuidv7();
-- 019508b0-0000-7000-8000-000000000000
-- Начинается с timestamp → монотонно возрастает
-- Решает проблему фрагментации индексов!
Для PostgreSQL < 17 можно использовать расширение pg_uuidv7:
CREATE EXTENSION pg_uuidv7;
SELECT uuid_generate_v7();
Когда UUID необходим
1. Распределённые системы
-- Клиент генерирует ID до вставки в БД
-- Полезно для:
-- - Оффлайн-синхронизации
-- - Нескольких независимых источников данных
-- - Шардинга
INSERT INTO events (id, user_id, type)
VALUES (gen_random_uuid(), 42, 'click');
-- ID создан в приложении до INSERT
2. Безопасность — скрыть количество записей
-- По SERIAL можно угадать: /api/users/1337 → есть 1337+ пользователей
-- По UUID это невозможно: /api/users/d7c5f3a1-8b2e-4c9d-a0e7-1f3b5d6e8c2a
3. Объединение данных из разных источников
-- Слияние таблиц из разных баз без конфликта ID
INSERT INTO global_events SELECT * FROM shard_1.events;
INSERT INTO global_events SELECT * FROM shard_2.events;
-- Нет конфликтов если все используют UUID
Проблема фрагментации индексов с UUID v4
UUID v4 случайный → новые записи вставляются в произвольные места B-tree:
Страница 1: [a3f2..., b1c4..., c7d8...]
Новая запись 1e2f... → вставляется в середину Страницы 1
Нужен page split → фрагментация
Результат: индекс разрастается, bloat, медленный INSERT.
-- Измерить bloat индекса
SELECT
schemaname, tablename, indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
round(100 * (pg_relation_size(indexrelid)::numeric /
pg_relation_size(indrelid) - 1)) AS bloat_pct
FROM pg_stat_user_indexes
JOIN pg_index USING (indexrelid)
WHERE indisprimary = true
ORDER BY pg_relation_size(indexrelid) DESC;
Решения:
- UUID v7 — монотонный, нет фрагментации
- ULID — аналог UUID v7, библиотека в приложении
- VACUUM +
REINDEX CONCURRENTLY— периодическая дефрагментация - Заполнение страниц —
fillfactor = 80для таблиц с UUID
Хранение UUID: тип uuid vs text/varchar
-- Хорошо: нативный тип uuid
CREATE TABLE events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid()
);
-- Плохо: хранить как строку (в 2 раза больше места)
CREATE TABLE events (
id VARCHAR(36) PRIMARY KEY -- 36 символов = 36 байт vs 16 байт
);
Нативный uuid занимает 16 байт, строка 'd7c5f3a1-8b2e-...' — 36 байт. Разница в размере индекса существенна на больших таблицах.
Практические паттерны
Составной ключ: UUID + дата (для шардинга по времени)
CREATE TABLE events (
id UUID NOT NULL DEFAULT gen_random_uuid(),
event_date DATE NOT NULL,
payload JSONB,
PRIMARY KEY (event_date, id) -- партиция по дате, UUID уникален внутри
) PARTITION BY RANGE (event_date);
UUID как внешний ID, BIGSERIAL как внутренний
-- Лучший из двух миров
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY, -- внутренний, для JOIN (компактный)
public_id UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(), -- внешний, для API
email TEXT,
...
);
-- JOIN по id (быстро, 8 байт)
SELECT u.id, u.email FROM users u JOIN orders o ON o.user_id = u.id;
-- API работает с public_id (не раскрывает количество)
GET /api/users/d7c5f3a1-8b2e-4c9d-a0e7-1f3b5d6e8c2a
Генерация в приложении
import uuid
# Python: стандартная библиотека
user_id = uuid.uuid4() # UUID v4
event_id = str(uuid.uuid4()) # строка: 'd7c5f3a1-8b2e-...'
# Вставка в PostgreSQL
cursor.execute(
"INSERT INTO events (id, type) VALUES (%s, %s)",
[str(user_id), 'click']
)
// Node.js
import { randomUUID } from 'crypto';
const id = randomUUID(); // UUID v4
Итог: когда что выбрать
| Сценарий | Рекомендация |
|---|---|
| Простая таблица, один сервер | BIGSERIAL |
| Нужна безопасность (не угадать количество) | UUID |
| Распределённая система / шардинг | UUID v7 / ULID |
| Микросервисы, несколько источников | UUID |
| High-insert, нужна производительность | UUID v7 (монотонный) или BIGSERIAL |
| Публичный API (скрыть ID) | UUID или BIGSERIAL + public UUID |
UUID — не серебряная пуля. На небольших таблицах BIGSERIAL проще и быстрее. UUID оправдан там, где нужна глобальная уникальность или безопасность.