UUID или автоинкремент: какой первичный ключ выбрать
Практика4 минРедакция Innovialabs
Коротко
Для типичного веб- и мобильного бэкенда разумный выбор по умолчанию — UUIDv7 в нативном типе uuid: его можно генерировать где угодно, он не раскрывает объёмы и вставляется почти как автоинкремент; bigint identity хорош для внутренних таблиц в одной базе.
Короткий ответ
- bigint identity (автоинкремент) — самый компактный и быстрый ключ, если всё живёт в одной базе и ID не видны снаружи.
- UUIDv4 — уникален везде, но случайный порядок вредит индексам на больших таблицах.
- UUIDv7 и ULID — уникальны везде и упорядочены по времени, поэтому вставляются почти как автоинкремент.
- Практичный выбор по умолчанию: UUIDv7 для сущностей, которые видны в API и URL, bigint — для внутренних таблиц вроде логов и связующих таблиц.
Варианты в одной таблице
| Тип | Размер | Порядок вставки | Угадываемость в URL | Генерация вне БД |
|---|---|---|---|---|
integer / bigint identity | 4 / 8 байт | Последовательный | Легко перебрать | Нет |
| UUIDv4 | 16 байт | Случайный | Не угадать | Да |
| UUIDv7 | 16 байт | По времени | Не угадать, но видно время создания | Да |
| ULID | 16 байт (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, ожидая хронологический порядок.