UUID, ULID или sequence в PostgreSQL: что выбрать
Последовательность компактна и упорядочена, UUID удобно создавать на разных сервисах, а ULID добавляет временную сортировку. Разбираем, как выбрать идентификатор для PostgreSQL 16–17 и где каждый вариант создаёт проблемы.

Последовательность в PostgreSQL даёт компактные и обычно возрастающие ключи, UUID позволяет создавать идентификаторы независимо от базы, а ULID добавляет к случайной части временную метку. Выбор влияет на размер индексов, порядок записей и то, какую информацию о данных можно узнать по самому ID.
Сначала определите, где создаётся запись
Если все новые строки проходят через одну базу, проще всего использовать встроенную последовательность. Если записи создают несколько сервисов или приложение должно присвоить ID до обращения к базе, удобнее UUID или ULID. Если важен приблизительный порядок создания при генерации вне базы, присмотритесь к ULID — но он не заменяет надёжную сортировку по времени события.
Примеры ниже рассчитаны на PostgreSQL 16–17. В этих версиях для обычного UUID доступна встроенная функция gen_random_uuid(), которая создаёт UUID версии 4. ULID не относится к встроенным типам PostgreSQL: его генерацию и проверку формата обеспечивает приложение или отдельное расширение.
Три варианта и цена каждого
| Вариант | Где создаётся | Порядок и размер | Главный компромисс |
|---|---|---|---|
bigint sequence / identity | В PostgreSQL | 8 байт; значения обычно растут | Нужен доступ к базе при вставке, последовательность раскрывает порядок выдачи |
| UUID v4 | В PostgreSQL или приложении | 16 байт; случайный порядок | Удобен для распределённой генерации, но вставки идут по разным участкам индекса |
| ULID | Обычно в приложении | 128 бит; текстовая запись — 26 символов | Сортируется примерно по времени, но требует согласованной библиотеки и формата хранения |
Размер здесь — размер значения, а не обещание точного размера всей строки или индекса. Индекс зависит также от страниц, заполнения, ссылочных столбцов и других данных. Если таблица большая, сравните фактический размер на своей схеме.
Sequence: компактно и предсказуемо, но не без пробелов
Для таблицы, где ID назначает база, используйте столбец identity. Например, для PostgreSQL 16–17:
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT now()
);
Identity связывает столбец с последовательностью, а первичный ключ создаёт индекс. При вставке PostgreSQL назначит ID сам. Если приложение передаёт собственное значение, режим ALWAYS по умолчанию не позволит тихо подменить генерацию.
У последовательности есть важная особенность: значения могут пропускаться. Например, транзакция получила номер, а затем откатилась; либо запрос использовал номер, но не вставил строку. Поэтому sequence подходит для уникального ключа, но не для требования «номера строго без пропусков». Для юридически или операционно значимой непрерывной нумерации нужна отдельная логика и понимание её цены при параллельных операциях.
И ещё: sequence показывает порядок выделения номеров, а не гарантированное время фиксации транзакций. Две параллельные транзакции могут получить ID в одном порядке, а завершиться в другом. Для ленты событий сортируйте по отдельной временной метке и учитывайте разрешение часов, а не трактуйте ID как точные часы.
UUID v4: независимая генерация без координации
Если ID нужен до вставки или его создают разные узлы, UUID снимает зависимость от единой последовательности. В PostgreSQL 16–17 можно назначить UUID значением по умолчанию:
CREATE TABLE api_clients (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL
);
UUID v4 содержит случайные биты, поэтому строки обычно попадают в разные места B-tree-индекса. На больших таблицах это может ухудшить локальность вставок и увеличить давление на индекс по сравнению с возрастающим ключом. Это не означает, что UUID всегда медленнее: результат зависит от объёма данных, нагрузки, кэша и числа индексов. Проверяйте план и метрики на своей схеме.
UUID удобен, когда несколько систем создают записи автономно или идентификатор должен быть известен до обращения к базе. Но он занимает больше места, чем bigint, и этот тип распространяется на внешние ключи и связанные индексы. Если таких связей много, посчитайте стоимость не только для одной таблицы.
ULID: сортировка по времени с оговорками
ULID — 128-битный идентификатор: временная часть кодирует миллисекунды, а оставшиеся биты добавляют случайность. В текстовом виде он занимает 26 символов и обычно использует алфавит Crockford Base32. В отличие от UUID v4, корректно закодированные ULID можно сортировать лексикографически, чтобы получить приблизительный временной порядок.
PostgreSQL не предоставляет отдельный тип ULID. На практике приложение генерирует ULID, а база хранит его как text или в другом выбранном формате. Если важна лексикографическая сортировка строк, согласуйте регистр, алфавит и collation во всех сервисах; для предсказуемого сравнения ASCII-строк можно задать collation C. Добавьте проверку длины и допустимых символов на границе приложения или схемы — иначе в столбце окажутся значения разных форматов.
Сортировка ULID не превращает его в точную хронологию: часы узлов могут расходиться, а несколько генераторов могут выпускать ID в пределах одной миллисекунды. Поведение при генерации множества ULID за одну миллисекунду зависит от библиотеки и её режима. Если строгий порядок событий важен, храните отдельное время события или номер последовательности.
Как выбирать на практике
- Одна база назначает ID, важны компактность и простота: берите
bigint GENERATED ... AS IDENTITY. Не используйте номер как счётчик без пропусков и не считайте его точной временной меткой. - Записи создаются в нескольких сервисах или офлайн: UUID v4 избавит от согласования диапазонов ID. Учтите размер и случайные вставки в индекс.
- Нужны внешняя генерация и приблизительная сортировка по времени: ULID может подойти, если вы закрепите формат, библиотеку, правила сравнения и обработку часов.
- ID попадает в URL или показывается пользователю: не считайте UUID или ULID механизмом авторизации. Случайный идентификатор не отменяет проверку прав доступа.
Если сомневаетесь между sequence и UUID, начните с вопроса «может ли запись появиться без доступной центральной базы?». Если нет — identity обычно проще. Если да — UUID часто уменьшает связанность. ULID выбирайте только при конкретной потребности в приблизительной сортировке по времени, а не потому, что он выглядит короче в логах.
Идентификатор решает задачу уникальности, но сам по себе не гарантирует хронологию, непрерывную нумерацию или безопасность доступа.
Что проверить до запуска
- Посчитайте распространение ключа. Найдите все внешние ключи, индексы и сообщения, в которых передаётся ID. Замена типа затронет не одну колонку.
- Проверьте генерацию при параллельной записи. Для UUID и ULID создайте ID в тех же сервисах и процессах, которые будут работать в проде; убедитесь, что все используют один формат.
- Сравните индексы на реалистичном объёме. Загрузите репрезентативное число строк и измерьте размер индекса, скорость вставок и чтения. Зафиксируйте версию PostgreSQL, схему, оборудование и параллельность, чтобы сравнение имело смысл.
- Отделите ключ от времени. Если продукту нужна сортировка по событию, храните отдельный столбец времени и задайте обработку одинаковых временных значений.
Для проверки размера индекса используйте статистику PostgreSQL и функции оценки размера отношения, а скорость измеряйте на тестовом стенде с той же схемой и настройками, что в рабочей среде. Универсального процента ускорения для одного типа ID нет: важны распределение вставок, объём кэша и нагрузка на все связанные индексы.
Термин: B-tree — наиболее распространённый тип индекса в PostgreSQL, который упорядочивает ключи и помогает быстро находить диапазоны значений. Локальность вставок означает, насколько новые значения попадают рядом с уже существующими ключами в таком индексе.
Следующий шаг
Возьмите таблицу с самым большим числом строк или вставок и сравните для неё два реально подходящих варианта на копии данных. Снимите размер первичного и внешних индексов, скорость вставки и задержку основных запросов. Затем проверьте, не использует ли приложение ID как время, счётчик или секретный токен: эти ожидания лучше выразить отдельными полями и проверками.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: PostgreSQL 17: функции UUID, Спецификация ULID
Частые вопросы
Может ли sequence пропускать значения?
UUID v4 в PostgreSQL 16–17 можно генерировать без расширения?
ULID есть в PostgreSQL как отдельный тип?
ULID гарантирует точный порядок создания записей?
UUID или ULID защищает запись от доступа посторонних?
Можно ли сменить bigint на UUID в уже работающей базе?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.