Перейти к содержанию
Innovialabs

UUID или автоинкремент: какой первичный ключ выбрать

Практика4 мин

Коротко

Для типичного веб- и мобильного бэкенда разумный выбор по умолчанию — UUIDv7 в нативном типе uuid: его можно генерировать где угодно, он не раскрывает объёмы и вставляется почти как автоинкремент; bigint identity хорош для внутренних таблиц в одной базе.

Короткий ответ

  • bigint identity (автоинкремент) — самый компактный и быстрый ключ, если всё живёт в одной базе и ID не видны снаружи.
  • UUIDv4 — уникален везде, но случайный порядок вредит индексам на больших таблицах.
  • UUIDv7 и ULID — уникальны везде и упорядочены по времени, поэтому вставляются почти как автоинкремент.
  • Практичный выбор по умолчанию: UUIDv7 для сущностей, которые видны в API и URL, bigint — для внутренних таблиц вроде логов и связующих таблиц.

Варианты в одной таблице

ТипРазмерПорядок вставкиУгадываемость в URLГенерация вне БД
integer / bigint identity4 / 8 байтПоследовательныйЛегко перебратьНет
UUIDv416 байтСлучайныйНе угадатьДа
UUIDv716 байтПо времениНе угадать, но видно время созданияДа
ULID16 байт (26 символов в тексте)По времениНе угадать, но видно время созданияДа

Размер индекса и скорость вставки

Размер. UUID занимает 16 байт против 8 у bigint. Разница умножается на каждый индекс и каждый внешний ключ, который ссылается на таблицу. Хранить UUID нужно в нативном типе (uuid в PostgreSQL, BINARY(16) в MySQL), а не строкой: текстовый вариант больше чем вдвое тяжелее и медленнее сравнивается.

Вставка. B-tree индекс любит, когда новые значения приходят в конец:

  • Автоинкремент, UUIDv7 и ULID растут со временем — новые записи дописываются в «правый край» индекса, горячие страницы остаются в памяти.
  • UUIDv4 случаен — каждая вставка попадает в произвольное место индекса. На больших таблицах это означает больше разбиений страниц, больше чтений с диска и менее эффективный кеш.

В MySQL InnoDB эффект сильнее: первичный ключ там кластерный, то есть сама таблица хранится в порядке ключа.

Безопасность: угадываемые ID в URL

С автоинкрементом адрес /orders/1042 подсказывает, что существует /orders/1041. Это приводит к двум проблемам:

  • Перебор чужих объектов. Если на сервере забыли проверить права, злоумышленник просто увеличивает число. Важно: защищает от этого проверка авторизации, а не формат ID. Случайный ID лишь затрудняет атаку, но не заменяет проверку.
  • Утечка бизнес-данных. По двум ID с разницей в месяц конкурент оценит, сколько заказов вы получаете.

У UUIDv7 и ULID есть своя особенность: в них закодировано время создания. Обычно это безобидно, но если момент регистрации пользователя — чувствительная информация, учитывайте это.

Слияние баз и распределённые системы

Автоинкремент уникален только внутри одной таблицы одной базы. Проблемы появляются, когда:

  • нужно объединить данные из двух баз (два региона, поглощение другого продукта) — ID пересекаются;
  • мобильное приложение работает офлайн и создаёт записи до синхронизации — ему нужен ID сразу, без обращения к серверу;
  • сервисы создают связанные записи параллельно и должны знать ID заранее.

UUID любого вида решают всё это: его можно сгенерировать на клиенте, в сервисе или в базе без координации.

Как выбрать

  • Одна база, ID не видны наружу, огромные таблицы (события, логи, связующие таблицы) — bigint generated always as identity.
  • Публичные сущности: пользователи, заказы, документы — UUIDv7.
  • Офлайн-клиенты и генерация ID на устройстве — UUIDv7 или ULID.
  • UUIDv4 — когда нельзя раскрывать даже время создания, а таблица не настолько большая, чтобы случайные вставки стали проблемой.

Альтернативная схема — два идентификатора: внутренний bigint для JOIN и внешних ключей и отдельный публичный UUID с уникальным индексом для API. Это экономит место, но усложняет код: везде нужно помнить, какой ID где используется.

CREATE TABLE orders (
  id uuid PRIMARY KEY,          -- UUIDv7, генерируется в приложении или в БД
  customer_id uuid NOT NULL REFERENCES customers (id),
  created_at timestamptz NOT NULL DEFAULT now()
);

Встроенная генерация UUIDv7 есть не во всех версиях СУБД. Если её нет, используйте проверенную библиотеку в приложении.

Частые ошибки

  • Хранить UUID в varchar(36).
  • Использовать integer вместо bigint для растущих таблиц и упереться в предел через несколько лет.
  • Считать случайный ID заменой проверки прав доступа.
  • Сортировать по UUIDv4, ожидая хронологический порядок.

Частые вопросы

Можно, но это миграция с подготовкой: добавить колонку UUID, заполнить её, обновить внешние ключи и код, затем переключить первичный ключ. Часто проще оставить внутренний bigint и добавить публичный UUID только для API.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.