Опубліковано: · Оновлено: · автор: kodewright
UUID v4 чи v7: що обрати і коли (гайд 2026)
UUID v4 — це 122 випадкові біти, v7 починається з мітки часу в мілісекундах, тож індекс БД росте послідовно. Розклад бітів, тест на колізії і що обрати.

Вибір між UUID v4 і v7 зводиться до одного: v4 повністю випадковий, v7 починається з мітки часу. Для первинних ключів у базі даних беріть v7, бо впорядкований за часом префікс робить вставки послідовними. v4 потрібен там, де ID не повинен нічого казати про час свого створення. Обидва мають 128 бітів, і жоден не дасть колізії на будь-якому реальному обсязі.
Роками v4 був вибором за замовчуванням, і в кластерному індексі він коштує продуктивності запису на кожній вставці. RFC 9562 (травень 2024) стандартизував v7, щоб це виправити. SQL Server — єдина поширена СУБД, де v7 не допомагає; чому, пояснюю нижче.
128 бітів по поличках
Кожен UUID має 128 бітів, а версія визначає, на що вони йдуть:
UUID v4 — 128 bits, 122 of them random
┌──────────────────────────┬──────┬──────────────────────────┐
│ random (48 bits) │ ver │ random + variant │
│ ████████████████████████ │ 0100 │ ██ variant 10 ██████████ │
└──────────────────────────┴──────┴──────────────────────────┘
no time, no order — every bit is noise except 6 fixed ones
UUID v7 — 128 bits, time-ordered
┌────────────────────────┬──────┬────────┬──────┬────────────┐
│ unix_ms timestamp (48) │ ver │ rand 12│ var │ random 62 │
│ 1716800000000… │ 0111 │ ▓▓▓▓ │ 10 │ ██████████ │
└────────────────────────┴──────┴────────┴──────┴────────────┘
first 48 bits = creation time → sorts chronologically
Уся різниця в перших 48 бітах. v4 заповнює їх шумом, v7 записує туди мітку часу Unix у мілісекундах і лишає решту випадковою. Так розкиданий ідентифікатор стає послідовним, а для індексу бази даних важить саме ця властивість.
Обидві версії мають 6 фіксованих бітів: 4-бітне поле версії (0100 для v4, 0111 для v7) і 2-бітне поле варіанта (10). 12 бітів, позначені rand 12, не обов'язково випадкові: RFC 9562 дозволяє використати їх для дробової частини мілісекунди або для лічильника, щоб ID, створені в ту саму мілісекунду, теж ішли по порядку. uuidv7() у PostgreSQL 18 кладе туди дробову частку мілісекунди.
Короткі випадкові ID збігаються, UUID — ні
Я сам ловив цей баг: у проєкті використовувалися короткі випадкові ID, і два з них збіглися. Короткий ID здається достатньо випадковим, бо кожен окремо виглядає унікальним, але ймовірність колізії залежить від загальної кількості ID, а не від якогось одного. Це парадокс днів народження: для b випадкових бітів перша колізія стає підкиданням монетки приблизно на
n₅₀ ≈ 1.177 × √(2^b)
де n₅₀ — кількість уже згенерованих ID. Підступ у квадратному корені. Подвоєння довжини ID підносить безпечну кількість до квадрата, а 32-бітний ID дає лише близько 77 000 ID, перш ніж шанс дубліката перевищить 50%.
Щоб звірити формулу з реальністю, я генерував ID через crypto.randomBytes і crypto.randomInt у Node 22, доки не з'являвся перший дублікат, по 50 запусків на кожен формат:
import { randomBytes } from 'node:crypto'
function idsUntilCollision(gen) {
const seen = new Set()
for (;;) { const id = gen(); if (seen.has(id)) return seen.size + 1; seen.add(id) }
}
idsUntilCollision(() => randomBytes(4).toString('hex')) // 8 hex chars = 32 bits
| Формат ID | Випадкових бітів | n₅₀ (теорія) | Перший дублікат, 50 запусків |
|---|---|---|---|
| 8 hex-символів | 32 | ~77 000 | медіана 81 205, найраніше 9 983 |
| 6 символів, A–Z a–z 0–9 | 35,7 | ~281 000 | медіана 349 350, найраніше 24 102 |
| 10 символів, A–Z a–z 0–9 | 59,5 | ~1,1 млрд | не моделював |
| UUID v7 (лише в межах однієї мс) | 62–74 | ~2,5–162 млрд за мс | не моделював |
| UUID v4 | 122 | ~2,7 × 10¹⁸ | не моделював |
Для продакшену важить стовпець «найраніше»: один запуск із 50 натрапив на дублікат, коли восьмисимвольних ID було менше ніж 10 000. Таблиця на кілька тисяч рядків уже в зоні ризику, якщо унікальність ніщо не перевіряє.
Рядки з UUID — зовсім інший світ. 2,7 × 10¹⁸ для v4 означає генерувати мільярд ID на секунду протягом приблизно 86 років. У v7 випадкових бітів менше, але мітка часу розводить ID з різних мілісекунд, тож збігтися можуть лише ID, створені в ту саму мілісекунду. Навіть якщо 12 бітів rand_a пішли на субмілісекундний годинник, це близько 2,5 млрд ID усередині однієї мілісекунди. За реальної швидкості вставок колізії не буде в жодній із версій. Якщо вам потрібні короткі ID для URL, тримайте на стовпці обмеження унікальності і повторюйте генерацію при конфлікті: математика вище каже, що колись це знадобиться.
Звідки беруться випадкові біти, важить не менше, ніж скільки їх. Якщо бібліотека заповнює їх через Math.random(), ви отримуєте xorshift128+ із V8, внутрішній стан якого відновлюється з кількох виходів, і після цього всі наступні ID передбачувані. crypto.randomUUID() у браузері бере дані з того самого CSPRNG (криптографічно стійкого генератора псевдовипадкових чисел), що й crypto.getRandomValues(); перевірте, що ваша серверна бібліотека робить так само.
Чому v4 шкодить індексу бази даних
Первинні ключі живуть у B-tree індексі, а B-дерево найкраще справляється з послідовними вставками. Випадковий ключ v4 щоразу потрапляє у випадкову листову сторінку:
v4 — a random key lands mid-page, so a full page splits in two
┌───────────────┐ ┌─────────┐ ┌───────┐
│ page: 3 5 8 9 │ + insert 6 → │ 3 5 6 │ │ 8 9 │ ◄ SPLIT
└───────────────┘ └─────────┘ └───────┘
…and this repeats for nearly every insert, all over the tree
v7 — a time-ordered key always lands at the right edge, no split
┌────────────────┐ ┌────────────────┐
│ page: …97 98 │ + insert 99 → │ …97 98 99 │ ◄ just appends
└────────────────┘ └────────────────┘
pages fill densely, writes stay sequential, the hot page stays cached
З v4 кожна вставка може зачепити холодну сторінку будь-де в дереві: спершу її треба прочитати, а заповнені сторінки розщеплюються, щоб звільнити місце. Результат — фрагментація індексу, погана локальність у буферному пулі й посилення запису (write amplification), яке росте разом із таблицею.
| Поведінка індексу | v4 (випадковий) | v7 (за часом) |
|---|---|---|
| Куди потрапляє рядок | У випадкову листову сторінку | У крайню праву (найновішу) сторінку |
| Розщеплення сторінок | Часті | Рідкісні |
| Фрагментація індексу | Висока | Низька |
| Влучання в кеш буферного пулу | Рідкісні (холодні сторінки) | Часті (гаряча сторінка використовується повторно) |
| Посилення запису | Високе, росте з таблицею | Мінімальне |
Найдорожче це обходиться в СУБД із кластерним індексом, де первинний ключ і є фізичним порядком рядків:
- MySQL InnoDB: кластерний PK визначає розташування даних на диску. Випадкові ключі v4 — найгірший сценарій, і v7 це виправляє. Вбудованої функції для v7 у MySQL немає, тож генеруйте його в застосунку.
- PostgreSQL: таблиці зберігаються як heap, тож PK — це вторинний індекс. Штраф м'якший, але v7 однаково покращує локальність індексу. PostgreSQL 18 має вбудовану
uuidv7(), аuuid_extract_timestamp()дістає з ID час назад. - SQL Server: за замовчуванням кластеризує за PK, але порівнює значення
uniqueidentifier, починаючи з останніх шести байтів, а не з перших. Тому v7, збережений якuniqueidentifier, сортується за своїм випадковим хвостом. У тесті dbi services на 25 000 рядків він фрагментувався точнісінько якNEWID()(99,1%), тоді якNEWSEQUENTIALID()тримався на 0,5%. У SQL Server або лишайтеNEWSEQUENTIALID(), або робіть GUID некластерним ключем і кластеризуйте за стовпцемIDENTITY.
v7 виправляє вставки, але з одним нюансом
UUID v7 дає глобальну унікальність UUID і характер вставок автоінкрементного цілого числа. Старші 48 бітів — це мітка часу, тож порядок байтів збігається з хронологічним, і приблизне «створено о» ви отримуєте безкоштовно.
Нюанс: v7 видає час створення. Будь-хто з ID на руках прочитає вбудовану мітку часу з точністю до мілісекунди і зможе передбачити старші біти ID, створених приблизно тоді ж. Для внутрішнього первинного ключа це нормально, для публічного ідентифікатора — проблема:
- Не використовуйте v7 у capability URL («секретному посиланні»). Мітка часу звужує простір для вгадування і розкриває метадані.
- Не використовуйте v7 там, де час створення чутливий (медичні записи, фінансові події, усе, що ви маскували б у логах).
У таких випадках суцільний шум v4 і є перевагою. А для справжніх секретів (API-ключів, токенів сесій, ключів підпису) не годиться жодна з версій. UUID має максимум 122 випадкові біти і фіксований, легко впізнаваний формат; 256-бітне значення просто з CSPRNG не має структури, яка щось видасть.
Якщо UUID v4 потрібні просто зараз, мій Генератор UUID викликає crypto.randomUUID() прямо у вкладці браузера, тож ID ніколи не надсилаються на сервер. Він генерує лише v4, не v7, і до 100 штук за раз, якщо ви наповнюєте тестові дані.
v4 чи v7: повне порівняння
| Властивість | UUID v4 | UUID v7 |
|---|---|---|
| Усього бітів | 128 | 128 |
| Випадкових бітів | 122 | 62–74 |
| Компонент часу | Немає | 48-бітна мітка часу Unix у мс |
| Сортування за часом створення | Ні | Так (порядок байтів = хронологічний) |
| Локальність в індексі БД | Погана (випадковий розкид) | Відмінна (послідовне дописування), крім uniqueidentifier у SQL Server |
| Видає час створення | Ні | Так |
| Передбачуваний / перелічуваний | Ні | Частково (префікс із міткою часу) |
| Стандарт | RFC 4122, тепер RFC 9562 | RFC 9562 (2024) |
| Для чого найкращий | Публічні ID, непередбачуваність | Первинні ключі бази даних |
Вибір за сценарієм
| Ваша ситуація | Що брати |
|---|---|
| Первинний ключ у MySQL InnoDB або PostgreSQL | v7 |
| Таблиця з інтенсивними вставками, потрібен природний порядок за часом | v7 |
| Заміна автоінкрементного INT первинного ключа | v7 |
| Кластерний GUID-ключ у SQL Server | NEWSEQUENTIALID() або некластерний GUID + IDENTITY |
| Публічний ID, який не можна перелічити | v4 |
| Не можна розкривати, коли створено запис | v4 |
| Розподілені ID на клієнті, порядок неважливий | v4 |
| Токен безпеки, capability URL або API-секрет | Жоден: 256-бітний секрет із CSPRNG |
А як щодо v1, v6 і v8?
RFC 9562 (який замінив RFC 4122) описує ще кілька версій, які трапляються на практиці:
- v1: мітка часу плюс MAC-адреса машини. Базується на часі, але видає апаратний ідентифікатор, а поля мітки часу записані молодшими бітами вперед, тож він не сортується. RFC радить, де можливо, брати v7 замість v1 і v6.
- v6: той самий v1, але з переставленими полями часу, щоб він сортувався. Розумний варіант, якщо ви вже сидите на v1; для нових систем v7 простіший.
- v8: явно «кастомна» версія. Специфікація фіксує лише біти версії й варіанта, а решту ви визначаєте самі, коли треба вбудувати власні структуровані дані.
Для нової системи вибір майже завжди між v7 для ключів і v4 для всього, що має лишатися непрозорим.
Зберігання і реалізація
UUID займає 16 байтів. Якщо зберігати його як канонічний рядок із 36 символів (CHAR(36)), він займе більш ніж удвічі більше місця і сповільнить кожне порівняння в індексі. Зберігайте його як BINARY(16) у MySQL або нативний тип uuid у PostgreSQL: менші сторінки, швидші порівняння, щільніші індекси.
-- MySQL: store the 16 raw bytes, not the 36-char string
id BINARY(16) PRIMARY KEY
-- PostgreSQL 18+: native uuid type with a built-in v7 default
id uuid PRIMARY KEY DEFAULT uuidv7()
Якщо стара таблиця InnoDB має ключі v1, функція MySQL UUID_TO_BIN(uuid, 1) переставляє поля часу на початок, і вони сортуються. Не застосовуйте цей прапорець до v7 (він і так упорядкований) і до v4 (там немає часу, який можна перенести). Таблицю, повну ключів v4, на місці не виправити: нові рядки переводьте на v7, а старі просто лишаться розкиданими.
З чого почати
Якщо ви обираєте схему ID сьогодні: v7 для первинного ключа (якщо це не SQL Server), v4 із Генератора UUID для всього публічного і 256-бітне значення у hex або base64 із генератора секретних ключів для всього, що має лишатися секретом. Звідки береться випадковість для всіх трьох, пояснює стаття про CSPRNG.
FAQ
Чи можна використовувати UUID v7 як токен безпеки?
Ні. Префікс із міткою часу читається відкрито і передбачуваний для ID, створених приблизно в той самий момент. Для публічних ID, які не повинні вгадуватися, беріть v4, а для справжніх токенів — 256-бітний секрет із CSPRNG.
Чи замінює UUID v7 версію v4?
Ні. RFC 9562 радить v7 замість v1 і v6, а не замість v4. v7 — кращий вибір за замовчуванням для первинних ключів, а v4 лишається правильним для зовнішніх ID, які не мають видавати час створення.