CUID, ULID и другие идентификаторы

Идентификаторы объектов в программных системах выполняют роль стабильных ключей, позволяющих однозначно ссылаться на сущности в базе данных, API, кеше и распределённых системах. Наиболее распространённым стандартом долгое время оставался UUID (Universally Unique Identifier), определённый в RFC 4122.

UUID представляет собой 128-битное число, обычно отображаемое в виде строки:

550e8400-e29b-41d4-a716-446655440000

Основные версии UUID:

  • v1 — основан на времени и MAC-адресе устройства
  • v3 / v5 — детерминированные, основанные на хеше (MD5 / SHA-1)
  • v4 — случайная генерация

На практике чаще всего используется UUID v4 благодаря простоте и отсутствию утечки информации о системе генерации.

Несмотря на распространённость, UUID имеет ряд особенностей:

  • длинный и неудобный для чтения
  • не сортируется по времени (в случае v4)
  • занимает больше места в индексах БД по сравнению с компактными альтернативами

Эти ограничения привели к появлению более современных идентификаторов.


ULID: упорядоченные идентификаторы с высокой энтропией

ULID (Universally Unique Lexicographically Sortable Identifier) представляет собой альтернативу UUID, разработанную для решения проблемы сортировки и читаемости.

Формат ULID:

01ARZ3NDEKTSV4RRFFQ69G5FAV

Структура:

  • 48 бит — метка времени (Unix time в миллисекундах)
  • 80 бит — случайная часть

Ключевые свойства:

  • лексикографическая сортировка соответствует времени создания
  • более компактное представление по сравнению с UUID
  • URL-safe формат (используется Base32 без специальных символов)
  • высокая уникальность даже при распределённой генерации

ULID особенно полезен в системах, где важна сортировка по времени без дополнительного поля createdAt.

Пример генерации в Jav * aScript:

import { ulid } from "ulid";

const id = ulid();

CUID: идентификаторы, ориентированные на горизонтальное масштабирование

CUID (Collision-resistant Unique IDentifier) разрабатывался с прицелом на устойчивость к коллизиям в распределённых системах и высокую скорость генерации.

Пример CUID:

cjld2cyuq0000t3rmniod1foy

Особенности:

  • генерируется без центрального координационного сервера
  • устойчив к коллизиям при высокой нагрузке
  • использует комбинацию времени, счётчиков и случайных значений
  • часто генерируется быстрее UUID

Существуют версии:

  • CUID (original) — классическая реализация
  • CUID2 — современная версия с улучшенной криптографией и меньшей предсказуемостью

Пример:

import { createId } from "@paralleldrive/cuid2";

const id = createId();

NanoID и компактные идентификаторы

NanoID представляет собой лёгкую альтернативу UUID и CUID, ориентированную на минимальный размер и высокую производительность.

Пример:

V1StGXR8_Z5jdHi6B-myT

Особенности:

  • короткая длина (по умолчанию 21 символ)
  • высокая скорость генерации
  • отсутствие лишних зависимостей
  • возможность кастомизации алфавита и длины

Пример генерации:

import { nanoid } from "nanoid";

const id = nanoid();

NanoID часто применяется в frontend-приложениях, URL-идентификаторах и системах, где важен компактный формат.


Сравнение подходов к идентификаторам

Тип Сортировка по времени Длина Читаемость Основной сценарий
UUID Нет (v4) / частично 36 низкая универсальные системы
ULID Да 26 средняя базы данных, event-log
CUID частично ~25 средняя распределённые системы
NanoID нет 21+ высокая frontend, URL

Выбор идентификатора зависит от требований к индексации, скорости генерации и необходимости временной сортировки.


Валидация идентификаторов с использованием Zod

В системах с типизацией на уровне схем часто требуется строгое описание формата идентификаторов. Библиотека Zod позволяет описывать такие правила декларативно.

Базовая валидация UUID:

import { z } from "zod";

const UUIDSchema = z.string().uuid();

Проверка ULID через регулярное выражение:

const ULIDSchema = z.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/);

CUID2:

const CUID2Schema = z.string().regex(/^[a-z0-9]{24,}$/);

NanoID (по умолчанию 21 символ):

const NanoIDSchema = z.string().regex(/^[A-Za-z0-9_-]{21}$/);

Дискриминированные идентификаторы и типизация схем

В сложных системах один тип сущности может использовать разные форматы идентификаторов. Zod позволяет комбинировать схемы через объединения:

const IDSchema = z.union([
  z.string().uuid(),
  z.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/), // ULID
  z.string().regex(/^[a-z0-9]{24,}$/) // CUID2
]);

Это полезно при интеграции с внешними сервисами или миграции между системами идентификаторов.


Использование предикатов и трансформаций

Zod позволяет не только валидировать, но и преобразовывать данные:

const NormalizedID = z.string().transform((val) => val.trim().toLowerCase());

Для ULID можно дополнительно проверять временную корректность:

const ULIDWithTime = z.string().refine((val) => {
  const timestamp = val.slice(0, 10);
  return !Number.isNaN(parseInt(timestamp, 32));
});

Генерация и проверка в одном потоке схем

В архитектурах с единым источником правды часто требуется связать генерацию и валидацию идентификаторов.

import { nanoid } from "nanoid";

const UserSchema = z.object({
  id: z.string().default(() => nanoid()),
  email: z.string().email()
});

Такой подход позволяет централизовать правила формирования данных и избежать рассинхронизации между клиентом и сервером.


Особенности хранения в базах данных

Разные типы идентификаторов по-разному влияют на производительность:

  • UUID требует больше места в индексах и может ухудшать локальность данных
  • ULID улучшает кластеризацию благодаря временной сортировке
  • NanoID уменьшает размер индексов, но не даёт временной упорядоченности
  • CUID2 балансирует между безопасностью и скоростью генерации

В реляционных БД часто применяют текстовые или бинарные представления UUID/ULID, в зависимости от движка.


Ошибки проектирования при выборе идентификаторов

Типичные проблемы:

  • использование UUID v4 в системах с интенсивной вставкой в индексируемые таблицы
  • попытка использовать NanoID как единственный источник сортировки
  • отсутствие валидации формата на уровне схемы
  • смешивание разных типов идентификаторов без явного разграничения

Zod позволяет частично компенсировать эти ошибки, фиксируя формат данных на границе системы, но не решает архитектурные проблемы выбора идентификатора.