Генерація ідентифікаторів
1) Навіщо приділяти увагу ідентифікаторам
Ідентифікатор (ID) - фундаментальний ключ сутності: рядки БД, повідомлення, файл, замовлення. Від його властивостей залежать:- Унікальність і масштаб (колізії, горизонтальний ріст).
- Порядок і сортування (тимчасова кореляція, реплікація, дедуп).
- Продуктивність зберігання (індекси, гарячі сторінки, розмір ключа).
- Безпека (непередбачуваність, витоки, вгадування).
- Юзабіліті/інтеграції (короткий, URL-safe, не чутливий до регістру).
Вибір ID - компроміс між ентропією, впорядковуваністю, довжиною, швидкістю генерації та експлуатацією.
2) Ключові вимоги та терміни
Унікальність: ймовірність колізії повинна бути нижче прийнятного ризику.
Ентропія: «скільки випадковості» містить ID (біт).
Впорядковуваність (time-sortable/k-sortable): лексикографічне сортування ≈ сортування за часом.
Монотонія: невпинна послідовність всередині вузла/потоку.
Локальність запису: наскільки нова вставка концентрується в «хвості» індексу (небезпека гарячих сторінок).
Передбачуваність: чи можна вгадати сусідні ID (важливо для безпеки/API).
Представлення: бінарний/рядковий, Base16/32/36/58/64, дефіси, регістр.
3) Основні сімейства ідентифікаторів
3. 1 UUID
v4 (random): 122 біти ентропії. Невпорядковуємо, хороший для безпеки і простоти. Мінус: «хаотит» індекси через випадковий розподіл - що, однак, рівномірно розсіює навантаження і прибирає «гарячі сторінки».
v1 (time + MAC): впорядковуємо, але несе МАС/час (приватність); часто уникають.
v7 (time-ordered): мілісекундний час + рандомна частина. Дизайн під лексикографічне сортування за часом і хорошу компресію в БД. Компроміс: з'являється «гарячий хвіст» індексу; лікується шардуванням/префіксами/інкрементом.
Поради
Для зовнішніх API і нестрогих вимог до порядку - v4.
Для подієвих/лігвих БД і «сортованих» ключів - v7.
3. 2 ULID (Crockford Base32)
128 біт: 48 біт часу (мс) + 80 біт випадковості. Лексикографічно сортується за часом, людино-доброзичливий (без'I, L, O, U'), URL-safe. Є монотонна варіація (при однаковій мітці часу випадкова частина збільшується).
Плюси: читаність, впорядковуваність, переносимість.
Мінуси: при дуже високій частоті вставок в один момент часу - «гарячий хвіст».
3. 3 KSUID
160 біт: 32 біт часу (сек) щодо епохи + 128 біт випадковості. Більший часовий діапазон і стабільна сортованість, рядки коротше ULID? (ні - довше, але зі своїм кодуванням), хороший для розподілених логів і об'єктів.
3. 4 Snowflake-подібні (k-sortable flake IDs)
Класична схема (настроюється):
[ timestamp bits ][ region/datacenter bits ][ worker bits ][ sequence bits ]
Властивості: монотонний ріст на вузлі, квазі-глобальна унікальність, коротке (64 біта) бінарне уявлення.
Ризики: залежність від годинника (дрейф/регрес часу), вичерпання sequence в одному тику, координація бітів region/worker.
Лікується: захистом від «clock back», резервом sequence, детектором часу, PTP/NTP дисципліною.
3. 5 Послідовності БД (SEQUENCE/IDENTITY)
Найпростіша монотонна генерація в одній СУБД/шарді.
Плюси: коротко, швидко, зручно для локальних таблиць.
Мінуси: важко глобально в розподіленому кластері; передбачувано (небезпечно як публічний ключ), створює гарячий хвіст індексу.
3. 6 Контент-адресні ID (hash content)
SHA-256/Blake3 від вмісту → стабільний ID, дедуплікація, перевірка цілісності, кешування.
Плюси: детермінізм, захист від підміни.
Мінуси: дорога генерація (CPU), колізії практичні нулі, немає тимчасового сортування, довжина.
4) Колізії та «парадокс днів народження» (інтуїтивно)
Ймовірність колізії для випадкового ID розміру «b» біт при «n» генераціях наближено:
p ≈ 1 - exp (-n (n-1 )/2/2 ^ b) ≈ n ^ 2/2 ^ (b + 1) (for small p)
Приклади:
- UUIDv4 (122 біта) при n = 10 ^ 12 (трильйон) → p ~ 1e-14 (зневажливо).
- 64-біт рандом → при n = 10 ^ 9 вже p ~ 0. 027 (помітний ризик).
- Висновок: 64-біт випадкових часто мало для величезних систем; використовуйте 96/128 біт.
5) Індекси, гарячі сторінки та зберігання
Випадкові ключі (v4) рівномірно розподіляють вставки по дереву індексу → немає «хвоста», але гірше кеш-локальність.
Сортовані за часом (v7/ULID/Snowflake) вставляються «у хвіст» → краща локальність і компресія, але ризик гарячих сторінок під високим паралельним записом.
- префікси/шардинг по tenant/region (додати 1-2 байта перед часом);
- interleaving: частина випадковості в старших бітах;
- батч-вставки, fillfactor в B-дереві, автоперехід на BRIN/кластеризацію для великих логів.
- 'UUID (16B)'vs'BIGINT (8B) '/' INT8'економить пам'ять/кеш; рядки Base32/58/64 збільшують розмір на 20-60%. Для БД зберігайте бінарно, серіалізуйте в рядок на краю.
6) Безпека і приватність
Невикористовуйте SEQUENCE/INT як публічні ID в URL/API: вгадувані → перерахування ресурсів.
Додавайте рандомні, непередбачувані ID (v4/v7/ULID/KSUID) для зовнішніх посилань.
Не кодуйте PII в ID. Якщо потрібно включити атрибут - шифруйте/підписуйте (наприклад, JWE/JWS) або використовуйте непрозорі токени.
URL-безпечні кодування: Base32 Crockford, Base58 (без `0OIl`), Base64url.
7) Мульти-тенантність, префікси і маршрутизація
Формат: '[TENANT _ PREFIX] - [ID]'або бінарно: `tenant_id || id`.
Плюси: швидкі фільтри/партії по орендарю, захист від N + 1 сканів.
Мінуси: може погіршити щільність ентропії в старших бітах → продумайте розподіл (хеш префікса).
Hash-суфікс (2-3 байти) знижує колізії і допомагає shard-роутингу: `shard = hash(id) % N`.
8) Практичні рекомендації щодо вибору
API, публічні посилання, розподілені сервіси без суворого порядку: UUIDv4, ULID/KSUID.
Логи/події/замовлення, де часто сортуємо за часом: UUIDv7 або ULID (монотонний).
Надвисока пропускна з локальною монотонністю і коротким ключем: Snowflake-подібний 64-біт (потрібна дисципліна часу).
Сховища артефактів/білдів/блобів: контент-адресні (SHA-256), а поверх - людино-доброзичлива коротка «вітрина» (Hashids/посилання).
Локальні таблиці в одній БД: SEQUENCE/IDENTITY + зовнішня «обгортка» для публічних посилань (masking).
9) Реалізації та приклади
9. 1 PostgreSQL
Зберігайте UUID бінарно, індекси - «btree» або «hash» за потребою.
sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- или uuid_generate_v4()
created_at timestamptz NOT NULL DEFAULT now(),
tenant smallint NOT NULL
);
-- For time-sortable (UUIDv7) store binary (uuid), generation in the application.
-- If you want a cluster by time:
CREATE INDEX ON orders (created_at DESC);
Sequential hot fix: для time-sorted ID додайте «сіль» в старші біти або партицируйте по tenant:
sql
CREATE TABLE orders_t1 PARTITION OF orders FOR VALUES IN (1);
CREATE TABLE orders_t2 PARTITION OF orders FOR VALUES IN (2);
9. 2 Redis (атомарні лічильники/монутонія)
bash
INCR "seq: orders" # local sequence combine: epoch_ms<<20 (worker_id<<10) (seq & 1023)
9. 3 Snowflake-подібний генератор (псевдокод)
pseudo const EPOCH = 1704067200000 # custom epoch (ms)
state: last_ms=0, seq=0, worker=7, region=3
next():
now = epoch_ms()
if now < last_ms: wait_until(last_ms) # защита от clock back if now == last_ms:
seq = (seq + 1) & ((1<<12)-1) # 12 бит if seq == 0: wait_next_ms()
else:
seq = 0 last_ms = now return (now-EPOCH)<<22 region<<17 worker<<12 seq
9. 4 ULID/UUID в додатках
Go
go
// ULID t:= time. Now(). UTC()
entropy:= ulid. Monotonic(rand. New(rand. NewSource(t. UnixNano())), 0)
id:= ulid. MustNew(ulid. Timestamp(t), entropy)
//UUID v7 (if there is a library)
id:= uuid. Must(uuid. NewV7())
Node. js
js import { ulid } from 'ulid';
import { v4 as uuidv4 } from 'uuid';
const id1 = ulid();
const id2 = uuidv4(); // v4
Python
python import uuid, time id_v4 = uuid. uuid4()
For v7, use a library (for example, uuid6/7 third-party packages)
10) Кодування та представлення
Бінарно в БД («BYTEA», «UUID») → компактно і швидко. На краю конвертуйте в:- Base32 Crockford (ULID): нечутлива до регістру, без візуально схожих символів.
- Base58: коротше Base32/64 для людиночитаних токенів, URL-safe.
- Base64url: коротко, але'-'і'_'в URL.
Стабілізуйте регістр і формат (дефіси/їх відсутність), щоб уникнути дублікатів при порівнянні рядків.
11) Тест-плейбуки та спостережуваність
Колізії: метрика'id _ collision _ total'( повинна бути 0), алерт при> 0.
Розподіл префіксів: гістограма старших байтів - шукаємо скоплення.
Швидкість генерації: 'ids _ per _ sec', p99 латентності генератора.
Clock skew (для Snowflake): офсет вузлів, події «clock went back».
Індексні хвости: p95/p99 `INSERT` latency; частка блокувань/гарячих сторінок.
- Інжект «clock drift/back» → переконуємося, що генератор чекає/перемикається.
- Переповнення «sequence» в мілісекунді → перевірка очікування next_ms.
- Масовий паралелізм → чи немає штормів блокувань в індексі.
12) Анти-патерни
AUTO_INCREMENT/SEQUENCE як публічний ID: вгадується, витоку. Використовуйте публічний непрозорий ID поверх внутрішнього.
UUIDv1 (МАС/час) назовні: Приватність.
64-біт випадковий ID на трильйони записів: реальний ризик колізій.
Глобальний «центральний генератор» без HA: SPOF і вузьке місце.
Time-sorted IDs без захисту від clock back: дублікати/регрес порядку.
Змішування різних форматів ID без явної версії/префікса → хаос в дебазі/міграціях.
Збереження ID як рядки з різними регістрами/формами → приховані дублікати.
13) Чек-лист впровадження
- Обраний формат (v4/v7/ULID/KSUID/Snowflake/SEQ/hash) під доменні вимоги.
- Визначені вимоги до порядку (чи потрібна сортованість).
- Оцінена ймовірність колізій (b біт, n генерацій) і заданий поріг ризику.
- Спроектоване кодування (бінарно в БД + людиночитана вітрина).
- Для time-sorted - захист від clock back, sequence-ліміти і NTP/PTP дисципліна.
- Для публічних ID - непередбачуваність (рандом/ULID/KSUID), відсутність PII.
- Продуманий шард-роутинг (hash (id)% N), мульти-тенантні префікси.
- Спостережуваність: метрики колізій, розподілу, затримок, clock skew.
- Тест-кейси на переповнення sequence/високу конкуренцію/довжину вікна.
- Документація формату, версії, епохи, бітової розмітки і план міграцій.
14) FAQ
Q: Що вибрати «за замовчуванням» для мікросервісів?
A: UUIDv7 або ULID: впорядковуваність за часом, багато ентропії, проста генерація на краю. Для зовнішніх API - ULID/UUIDv4 теж бл.
Q: Потрібен короткий і людиночитаний ID.
A: ULID/KSUID або Base58-кодування 128-біт випадкового/тимчасового ID. Пам'ятайте про довжину і колізії.
Q: Чи можна зробити «короткі числові» ID, але безпечно?
A: Так: зберігайте внутрішній SEQ, а назовні віддавайте opaque токен (рандом 96-128 біт) або Hashids з сіллю + підпис.
Q: Як мігрувати з SEQ на UUIDv7?
A: Введіть новий стовпець'id _ new'( UUID), двупись, публікація посилань на новий ID, потім перемикання РК/зовнішніх ключів і видалення старого.
Q: Чому мої вставки з ULID стали «гарячими»?
A: Вставляєте строго зростаючі ключі в один індекс. Розбийте по партіях/tenant, перемішайте старші біти, використовуйте batch-вставки.
15) Підсумки
Хороший ID - це правильний набір властивостей під задачу: досить ентропії, передбачуване сортування (якщо потрібна), безпечна публічність і здорова експлуатація індексів. Вибирайте UUIDv4/ULID/UUIDv7/KSUID для простоти і розподіленості, Snowflake - для щільної монотонії і коротких ключів (при дисципліні часу), послідовності - для локальних таблиць, хеші контенту - для артефактів. Закладайте спостережуваність і тести - і ідентифікатори перестануть бути джерелом сюрпризів.