UUID v4 / UUID v7 / ULID: подробное сравнение и выбор с точки зрения хронологии, эффективности и удобочитаемости
UUID и ULID — механизмы выдачи «уникальных идентификаторов» в распределённых системах и базах данных. Формат у них похож, но различия проявляются в наличии стандарта, эффективности хранения, удобочитаемости и рисках раскрытия информации. Ниже я систематизирую признаки и критерии выбора трёх ключевых вариантов — UUID v4, UUID v7, ULID, а затем кратко напоминаю об особенностях UUID v1 / v6.
Вот набор инструментов, которые генерируют идентификаторы и извлекают время в браузере — без передачи данных наружу:
- Генерация UUIDv4
- Генерация UUIDv7
- Извлечение времени из UUIDv7
- Генерация ULID
- Извлечение времени из ULID
UUID v4: полностью случайный формат
У UUID v4 из 128 бит 122 бита занимают случайные данные, поэтому вероятность коллизий ничтожна. Обратная сторона — отсутствие естественной сортировки по времени. В B-Tree-индексах страницы будут часто расщепляться, и локальность вставок ухудшается. Такой формат уместен для сессионных идентификаторов или одноразовых ключей, где «чистая случайность» допустима.
UUID v7: упорядоченный по времени стандарт
UUID v7 закреплён в RFC 9562 (2024). Первые 48 бит содержат миллисекундное время Unix-эпохи, оставшиеся биты заполняет случайность. Лексикографический порядок строк совпадает с порядком генерации, поэтому даже в роли первичного ключа индекс сохраняет хорошую локальность. Идентификатор можно напрямую хранить в типах uuid
или BINARY(16)
, что упрощает внедрение.
- Длина случайной части: за вычетом битов version/variant остаётся примерно 74 бита случайности. Реализации могут применять монотонную генерацию (чтобы избежать коллизий в одной миллисекунде) и слегка переназначать случайные биты, но статистическое качество при этом сохраняется.
- Риск раскрытия информации: из идентификатора можно восстановить момент генерации (до миллисекунд). При больших объёмах выдачи и слабой реализации генератора случайностей предсказать соседние идентификаторы становится проще. Внутренние системы обычно терпимы к этому, но публичные эндпоинты, где нежелательны «полусерийные» ID, требуют дополнительного контроля.
ULID: человекоориентированный идентификатор
ULID предложен в 2016 году и стал де-факто стандартом. Формат — 26 символов Crockford Base32, из набора исключены легко путаемые символы (O/I/L/1), что облегчает вставку в URL и копирование. Как и UUID v7, ULID содержит миллисекундное время в первых 48 битах, так что лексикографический порядок строк равен порядку по времени.
- Гарантия сортировки: строковое сравнение ULID в Base32 всегда соответствует хронологии. Монотонная генерация для устранения коллизий в одной миллисекунде получила широкое распространение.
- Форма хранения: в
TEXT(26)
идентификатор читается напрямую. Для бинарного хранения в БД потребуется конвертация Base32↔16 байт. Ширина остаётся 128 бит, но типuuid
в СУБД обычно несовместим с ULID. - Риск раскрытия информации: аналогичен UUID v7 — миллисекундное время видно. Если ресурсный ID в публичном API не должен раскрывать порядок выдачи, ULID стоит применять осторожно.
Сводная таблица трёх форматов (практический ракурс)
Параметр | UUID v4 | UUID v7 | ULID |
---|---|---|---|
Стандартизация | RFC 4122 | RFC 9562 (2024) | Не стандартизирован (де-факто стандарт) |
Битовая структура | 122 бита случайности из 128 | 48bit=UNIX ms + оставшиеся случайные биты (опционально монотонно) |
48bit=UNIX ms + 80bit случайности |
Энтропия | ≈122 бита | ≈74 бита (оценка с учётом version/variant) | 80 бит |
Строковое представление | 36 символов (hex + дефисы) | 36 символов (hex + дефисы) | 26 символов (Crockford Base32) |
Естественная сортировка (строки) | × (случайно) | ○ (лексикографический порядок = хронология) | ○ (лексикографический порядок = хронология) |
Естественная сортировка (байты) | × | ○ (memcmp по BINARY(16) даёт возрастающий порядок) |
○ (если первые 6 байт времени хранятся в big-endian, memcmp даёт возрастающий порядок) |
Характер хранения в БД | Лучше всего uuid / BINARY(16) |
Лучше всего uuid / BINARY(16) |
TEXT(26) удобно читать, BINARY(16) требует конверсии / обычно несовместим с типом UUID |
Локальность индекса | Низкая (случайные вставки дробят страницы) | Высокая (аппенды в конец / следить за hot-spot) | Высокая (аппенды в конец / следить за hot-spot) |
Стоимость равенства | Низкая (сравнение 16 байт) | Низкая (сравнение 16 байт) | Для TEXT чуть выше (зависит от collation) / для BINARY низкая |
Затраты на кодирование | Hex↔16 байт (минимальные) | Hex↔16 байт (минимальные) | Base32↔16 байт (чуть тяжелее) |
Удобочитаемость / пригодность для URL | Низкая | Низкая | Высокая |
Раскрытие времени | Нет | Есть (миллисекунды) | Есть (миллисекунды) |
Типичные сценарии | Сессионные ID, одноразовые токены | PK в БД, идентификаторы событий, журналы по времени | URL, публичные ID, визуализация логов |
Дополнение про hot-spot у v7/ULID. Если все вставки в одном шарде или на единственном лидере приходятся на «хвост» B-Tree, листовые страницы перегреваются. Подумайте о лёгкой перестановке старших битов (prefix shuffle) или добавочном ключе шардинга.
Заметки по реализации в БД (PostgreSQL / MySQL / SQLite)
- PostgreSQL
- v4/v7: тип
uuid
оптимален. При записи v7 вuuid
строкой мы автоматически получаем сортировку по времени. - ULID: используйте
char(26)
/text
или конвертируйте вbytea(16)
.
- v4/v7: тип
- MySQL/InnoDB
BINARY(16)
обеспечивает высокую скорость и компактность для v4/v7. UUID v7 в big-endian-формате сравнивается по времени через memcmp. ULID можно хранить вCHAR(26)
либо вBINARY(16)
после конверсии.
- SQLite
- Специализированного типа UUID нет. Используйте
BLOB(16)
илиTEXT
. ИндексированныйBLOB
сравнивается достаточно эффективно.
- Специализированного типа UUID нет. Используйте
Дополнительные меры безопасности
- Устойчивость к прогнозированию: у v7/ULID одинаковые временные биты. Если в одной миллисекунде выдаётся много идентификаторов и генератор случайностей слабый, растёт риск угадать соседние значения. Используйте только криптографический PRNG ОС и минимизируйте смещение монотонного увеличения.
- Утечка метаданных: по ID можно оценить момент генерации и примерный объём выдачи. В публичных API разумно разделять внутренние и внешние идентификаторы.
Итоговые рекомендации по выбору
- UUID v4: полностью случайный. Не сортируется. Теоретически коллизии возможны, пусть и с мизерной вероятностью. Применим к сессионным ID и токенам CSRF.
- UUID v7: стандартизированный упорядоченный UUID. Лучший кандидат для внутренних систем и первичных ключей БД. Монотонная генерация делает коллизии практически невозможными.
- ULID: более «человеческий» формат. Подходит, когда важна читаемость или наглядность логов, хотя в роли внутреннего PK чаще уступает v7. Монотонная генерация также позволяет избегать коллизий на практике.
Дополнение: кратко про UUID v1 / v6
-
UUID v1 (время + MAC) Содержит 60-битный таймстемп (точность 100 нс) и идентификатор узла (часто MAC-адрес). Обеспечивает хорошую хронологию, но раскрывает информацию о хосте и требует аккуратно обрабатывать откат системного времени. Из соображений конфиденциальности в новых системах формат почти не советуют.
-
UUID v6 (переупорядоченный v1) Переставляет таймстемп v1 в big-endian-порядке, чтобы лексикографическая сортировка была естественной. Когда-то его рассматривали как «v1, которым удобно сортировать», но стандартизация в итоге сошлась на v7. В новых проектах обычно выбирают v7 вместо v6.
Чек-лист для реализации
- Генератор случайных чисел должен быть криптографическим PRNG из ОС.
- В v7/ULID при монотонной генерации важно избегать коллизий в одной миллисекунде и снижать смещение случайных битов.
- В БД предпочтительнее
uuid
/BINARY(16)
, чтобы не терять производительность из-за collation строковых колонок. - Заранее определите политику публичных идентификаторов (можно ли разглашать порядок выдачи). При необходимости разделяйте внутренний и внешний ID.
Вывод: сейчас UUID v7 — главный кандидат для внутренних первичных ключей. Если важна читаемость или есть технологические ограничения, ULID — достойная альтернатива. Требование полностью скрыть временную метку может вернуть нас к v4 или другим схемам.