Выбор типа первичного ключа — одно из первых решений при создании таблицы, и оно влияет на производительность, масштабирование и архитектуру системы. В PostgreSQL есть несколько способов, и каждый имеет свои нюансы.
SERIAL: устаревший, но распространённый
SERIAL — это псевдотип, который разворачивается в INTEGER + последовательность + DEFAULT nextval(...):
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
email TEXT NOT NULL
);
-- Эквивалентно:
-- CREATE SEQUENCE users_user_id_seq;
-- user_id INTEGER DEFAULT nextval('users_user_id_seq') NOT NULL
Проблема: последовательность создаётся отдельно и не является жёстко привязанной к таблице. При pg_dump со специфическими опциями или при ручных манипуляциях можно легко потерять связь между таблицей и последовательностью.
SERIAL — целое число (4 байта), максимум ~2.1 миллиарда. Для небольших таблиц — норма. Для пользователей высоконагруженного сервиса — риск переполнения.
-- SMALLSERIAL: 1 до 32 767 — только для совсем маленьких справочников
-- SERIAL: 1 до 2 147 483 647
-- BIGSERIAL: 1 до 9 223 372 036 854 775 807
BIGSERIAL: когда нужен большой диапазон
BIGSERIAL — то же самое, что SERIAL, но тип BIGINT (8 байт). Максимальное значение: 9.2 × 10¹⁸.
CREATE TABLE events (
event_id BIGSERIAL PRIMARY KEY,
payload JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Если ваша система генерирует миллионы записей в день — считайте:
- 1 млн в день × 365 = 365 млн в год.
SERIAL(2.1 млрд) хватит на ~5.7 лет.BIGSERIALхватит на 25 000 лет при том же темпе.
Практическое правило: используйте BIGSERIAL или BIGINT GENERATED ALWAYS AS IDENTITY для любой таблицы, которая может существенно вырасти.
GENERATED AS IDENTITY: современный стандарт SQL
GENERATED AS IDENTITY появился в PostgreSQL 10 и является частью стандарта SQL:2003. Это рекомендуемый способ создания автоинкрементных колонок:
-- GENERATED BY DEFAULT: можно вставить своё значение явно
CREATE TABLE products (
product_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL
);
-- GENERATED ALWAYS: нельзя вставить своё значение без OVERRIDING SYSTEM VALUE
CREATE TABLE orders (
order_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
total NUMERIC(12,2)
);
Отличие от SERIAL:
- Последовательность жёстко привязана к столбцу — не существует отдельно.
- Чище интегрируется с
pg_dumpи репликацией. GENERATED ALWAYSзащищает от случайной вставки своего ID.
-- Настройка начального значения и шага
CREATE TABLE invoices (
invoice_id BIGINT GENERATED ALWAYS AS IDENTITY
(START WITH 1000 INCREMENT BY 1) PRIMARY KEY,
amount NUMERIC(12,2)
);
-- Вставка с явным значением (только для GENERATED BY DEFAULT)
INSERT INTO products (product_id, name) VALUES (9999, 'Тестовый товар');
-- Для GENERATED ALWAYS нужен спецсинтаксис
INSERT INTO orders (order_id, total)
OVERRIDING SYSTEM VALUE
VALUES (9999, 100.00);
UUID: глобально уникальные идентификаторы
UUID (Universally Unique Identifier) — 128-битный идентификатор, уникальный без координации между серверами.
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE sessions (
session_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id BIGINT REFERENCES users(user_id),
expires_at TIMESTAMPTZ NOT NULL
);
UUID v4 vs UUID v7
UUID v4 полностью случаен. Выглядит как 550e8400-e29b-41d4-a716-446655440000.
UUID v7 (RFC 9562, PostgreSQL 17+) включает временну́ю метку в начале, что делает UUID монотонно возрастающими:
-- PostgreSQL 17+
SELECT gen_random_uuid(); -- UUID v4 (случайный)
SELECT uuidv7(); -- UUID v7 (монотонный, только PG17+)
Для PostgreSQL < 17 можно использовать расширение pg_uuidv7.
Плюсы UUID
- Уникальность гарантирована без обращения к БД — ID можно генерировать на клиенте или в распределённой системе.
- Безопасность: ID не предсказуемы, нельзя угадать следующий (
/users/1001→ понятно, что следующий/users/1002). - Идеален для публичных API, распределённых систем, микросервисов.
Минусы UUID v4
- Случайность = фрагментация индекса. При вставке UUID v4 B-tree индекс первичного ключа пишет в случайные места, что приводит к частым
page splitоперациям и большим файлам индекса. - Занимает 16 байт против 8 байт у
BIGINT— все внешние ключи становятся тяжелее. - Нечитаем в логах и при отладке.
UUID v7 решает проблему фрагментации — благодаря временно́й метке новые записи всегда вставляются в «конец» индекса.
-- Бенчмарк: INSERT 1 млн строк
-- BIGINT IDENTITY: ~3 с, индекс ~40 MB
-- UUID v4: ~8 с, индекс ~90 MB (из-за фрагментации)
-- UUID v7: ~3.5 с, индекс ~45 MB
Числа примерные, но порядок соотношений верный.
Составные первичные ключи
Составной PK используется в промежуточных таблицах связей Many-to-Many:
CREATE TABLE order_items (
order_id BIGINT REFERENCES orders(order_id) ON DELETE CASCADE,
product_id BIGINT REFERENCES products(product_id),
quantity INT NOT NULL DEFAULT 1,
PRIMARY KEY (order_id, product_id) -- составной PK
);
Составной PK автоматически создаёт индекс (order_id, product_id). Для запросов по product_id отдельно нужен дополнительный индекс:
CREATE INDEX idx_order_items_product ON order_items(product_id);
Когда что выбрать
| Сценарий | Рекомендация |
|---|---|
| Внутренняя таблица, небольшой объём | INTEGER GENERATED ALWAYS AS IDENTITY |
| Любая таблица с ростом > 1 млн строк | BIGINT GENERATED ALWAYS AS IDENTITY |
| Распределённая система / микросервисы | UUID v7 (или UUID v4 если нет PG17) |
| Публичный API (скрыть объём данных) | UUID |
| Промежуточная таблица M:M | Составной PK из FK-столбцов |
| Legacy-код | BIGSERIAL (но мигрируйте к IDENTITY) |
Итог
SERIAL и BIGSERIAL работают, но GENERATED AS IDENTITY — современный стандарт, который стоит использовать в новых проектах. UUID удобен для распределённых систем и публичных API, но помните о фрагментации индекса при использовании v4. UUID v7 сочетает преимущества обоих подходов.
Закрепите понимание проектирования баз данных в нашем тренажёре SQL.