SecuredKit

Опубліковано: · Оновлено: · автор:

UUID v4 чи v7: що обрати і коли (гайд 2026)

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

uuid v4 чи 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–935,7~281 000медіана 349 350, найраніше 24 102
10 символів, A–Z a–z 0–959,5~1,1 млрдне моделював
UUID v7 (лише в межах однієї мс)62–74~2,5–162 млрд за мсне моделював
UUID v4122~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 v4UUID v7
Усього бітів128128
Випадкових бітів12262–74
Компонент часуНемає48-бітна мітка часу Unix у мс
Сортування за часом створенняНіТак (порядок байтів = хронологічний)
Локальність в індексі БДПогана (випадковий розкид)Відмінна (послідовне дописування), крім uniqueidentifier у SQL Server
Видає час створенняНіТак
Передбачуваний / перелічуванийНіЧастково (префікс із міткою часу)
СтандартRFC 4122, тепер RFC 9562RFC 9562 (2024)
Для чого найкращийПублічні ID, непередбачуваністьПервинні ключі бази даних

Вибір за сценарієм

Ваша ситуаціяЩо брати
Первинний ключ у MySQL InnoDB або PostgreSQLv7
Таблиця з інтенсивними вставками, потрібен природний порядок за часомv7
Заміна автоінкрементного INT первинного ключаv7
Кластерний GUID-ключ у SQL ServerNEWSEQUENTIALID() або некластерний 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, які не мають видавати час створення.

Спробуйте безкоштовний генератор паролів

Генеруйте надійні, безпечні паролі миттєво. 100% приватно, на стороні клієнта.

Відкрити генератор паролів