SQLLab
Все статьи

UUID в PostgreSQL: когда использовать вместо SERIAL и как оптимизировать

UUID vs SERIAL в PostgreSQL: типы uuid, gen_random_uuid(), UUIDv7, производительность индексов, проблема фрагментации и когда UUID необходим.

18 марта 2026 г.·5 мин чтения·

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 / BIGSERIALUUID
Размер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;

Решения:

  1. UUID v7 — монотонный, нет фрагментации
  2. ULID — аналог UUID v7, библиотека в приложении
  3. VACUUM + REINDEX CONCURRENTLY — периодическая дефрагментация
  4. Заполнение страниц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 оправдан там, где нужна глобальная уникальность или безопасность.

Похожие статьи

Попробуй на практике

Тренажёр с реальными задачами — бесплатно и без регистрации

Открыть тренажёр →