Nowy w temacie UUID? Zacznij od naszego Czym jest UUID? Kompletny przewodnik po podstawach formatu UUID, wersji i zastosowań.
UUID v4 vs v7 vs ULID vs Snowflake: przewodnik po ID (2026)
Wybór niewłaściwego schematu ID może drogo kosztować. Losowe klucze główne UUID v4 w tabeli ze 100 milionami wierszy powodują nawet 10× więcej podziałów stron indeksu niż ID sekwencyjne. Snowflake ID wymagają centralnego rejestru workerów, który staje się pojedynczym punktem awarii. ULID wyglądał na idealny kompromis — dopóki UUID v7 nie pojawił się jako standard IETF.
Niniejszy przewodnik dostarcza schematu decyzyjnego, twardych liczb wydajności i przykładów kodu, by wybrać właściwy identyfikator dla danego systemu.
Szybkie drzewo decyzyjne
| Wymaganie | Najlepszy wybór | Dlaczego |
|---|---|---|
| Klucz główny bazy danych (nowy projekt) | UUID v7 | Uporządkowany czasowo, standardowy typ kolumny uuid, najlepsza wydajność indeksu |
| Uniwersalne unikalne ID (bez wymogu kolejności) | UUID v4 | Powszechne wsparcie, zerowa konfiguracja, 122 bity losowości |
| Deterministyczne ID z określonych danych wejściowych | UUID v5 | Te same namespace + nazwa zawsze dają to samo UUID |
| System rozproszony o wysokiej przepustowości (>100 tys. ID/s/węzeł) | Snowflake ID | 64-bitowy integer, monotoniczny w obrębie workera, natywna kolumna BIGINT |
| Krótki, bezpieczny dla URL token lub ID po stronie klienta | NanoID | 21 znaków, alfabet bezpieczny dla URL, regulowana długość |
| Istniejący system korzystający już z ULID | ULID | Zachować — funkcjonalnie równoważny UUID v7, migracja nie jest warta zachodu |
UUID — wersje pod lupą
UUID v1: czas + adres MAC (przestarzały)
UUID v1 koduje 60-bitowy timestamp oraz 48-bitowy adres MAC maszyny. Był to oryginalny „sortowalny UUID”, ma jednak dwie krytyczne wady: ujawnia tożsamość sprzętu i używa niestandardowej epoki timestampa (15 października 1582 r.). RFC 9562 formalnie wycofuje v1 na rzecz v6/v7. Nie należy stosować v1 w nowych projektach.
UUID v4: identyfikatory losowe
UUID v4 wypełnia 122 ze swoich 128 bitów kryptograficznie bezpiecznymi danymi losowymi. To najczęściej używana wersja — prosta, prywatna i powszechnie wspierana.
Mocne strony:
- Zerowa konfiguracja, brak potrzeby koordynacji
- Pełna anonimowość — żaden timestamp ani informacja o sprzęcie nie wyciekają
- Wsparcie w każdej bazie danych, języku i framework
Słabość:
- Losowy rozkład powoduje fragmentację indeksu B-tree. W tabelach intensywnie zapisywanych z milionami wierszy klucze główne v4 mogą degradować wydajność wstawiania o 2–10× względem ID sekwencyjnych z powodu nadmiernych podziałów stron.
// Generate UUID v4 — built-in in all modern browsers and Node.js
const id = crypto.randomUUID();
// → "550e8400-e29b-41d4-a716-446655440000"
UUID v5: deterministyczny hash
UUID v5 oblicza hash z UUID-a namespace i ciągu znaków stanowiącego nazwę przy użyciu SHA-1, by uzyskać deterministyczne UUID. Te same dane wejściowe zawsze dają taki sam wynik.
Zastosowania: generowanie stabilnych ID z URL-i, nazw DNS lub dowolnych powtarzalnych danych wejściowych. Lepiej wybrać v5 niż v3 (które używa słabszego MD5).
import uuid
# Same inputs → same UUID, every time
id = uuid.uuid5(uuid.NAMESPACE_DNS, "example.com")
# → "cfbff0d1-9375-5685-968c-48ce8b15ae17"
UUID v7: uporządkowany czasowo i losowy (rekomendowany)
UUID v7 (RFC 9562, maj 2024) osadza 48-bitowy Unix timestamp w milisekundach w najbardziej znaczących bitach, po czym następują 74 bity kryptograficznej losowości.
Dlaczego v7 to nowy domyślny wybór dla kluczy bazodanowych:
- Sekwencyjne wstawianie: nowe UUID są zawsze większe od poprzednich (z dokładnością do milisekundy), więc wstawienia w B-tree zawsze trafiają na koniec indeksu
- Do 90% mniej podziałów stron w porównaniu z v4 przy obciążeniach intensywnie zapisujących
- Naturalne sortowanie chronologiczne bez dodatkowej kolumny
created_at - Standardowy typ kolumny
uuid— żadnych zmian schematu przy migracji z v4 - 74 bity losowości — wystarczająco dla niemal wszystkich zastosowań (v4 ma 122 bity)
Kompromis: timestamp utworzenia jest osadzony w ID. Jeśli potrzebne są nieprzejrzyste ID, które nie ujawniają czasu utworzenia, lepiej pozostać przy v4.
// UUID v7 generation (Node.js 20+)
import { v7 as uuidv7 } from "uuid";
const id = uuidv7();
// → "01906b5e-4a3e-7234-8f56-b8c12d4e5678"
// Older IDs always sort before newer ones
Wydajność w PostgreSQL i MySQL: v4 vs v7
Benchmarki na tabeli PostgreSQL 16 z 50 milionami wierszy (klucz główny B-tree):
| Metryka | UUID v4 | UUID v7 | Poprawa |
|---|---|---|---|
| Przepustowość wstawień (wiersze/s) | 12 400 | 28 600 | 2,3× szybciej |
| Rozmiar indeksu po 50M wierszy | 4,2 GB | 2,8 GB | 33% mniej |
| Podziały stron przy bulk insert | 1,2M | 84K | 93% mniej |
| Sekwencyjne skanowanie po wstawieniu | 320 ms | 180 ms | 44% szybciej |
W MySQL/InnoDB efekt jest jeszcze bardziej dramatyczny, ponieważ klucz główny JEST indeksem klastrowanym — losowe UUID v4 wymuszają stałą reorganizację stron, podczas gdy v7 zachowuje się jak auto-increment.
Alternatywne schematy ID
ULID — mistrz sprzed v7
ULID (Universally Unique Lexicographically Sortable Identifier) powstał w 2016 r., by rozwiązać problem sortowalności UUID v4. Koduje 48-bitowy timestamp w milisekundach, a po nim 80 bitów losowości w 26-znakowym ciągu Crockford Base32.
01AN4Z07BY 79KA1307SR9X4MV3
|----------| |----------------|
Timestamp Randomness
48 bits 80 bits
ULID vs UUID v7 — czy warto się przesiąść?
| Aspekt | ULID | UUID v7 |
|---|---|---|
| Sortowalny | Tak | Tak |
| Długość ciągu | 26 znaków | 36 znaków |
| Rozmiar | 16 bajtów | 16 bajtów |
| Standard | Specyfikacja społecznościowa | IETF RFC 9562 |
| Natywny typ DB | Nie (CHAR(26) lub BYTEA) | Tak (uuid) |
| Wsparcie języków | npm, PyPI, crates.io | Wbudowane w większość bibliotek standardowych |
Werdykt: zaczynając od zera, lepiej wybrać UUID v7 — daje tę samą sortowalność przy znacznie lepszym wsparciu ekosystemu i natywnych typach bazodanowych. Jeśli system już używa ULID, nie ma pilnej potrzeby migracji; oba są funkcjonalnie równoważne.
Snowflake ID — systemy rozproszone o wysokiej przepustowości
Snowflake ID (stworzony przez Twitter w 2010 r.) pakuje 64-bitowy integer w postaci:
0 | 41 bits timestamp | 10 bits worker ID | 12 bits sequence
- 41-bitowy timestamp: milisekundy od własnej epoki (~69 lat zakresu)
- 10-bitowe ID workera: obsługuje 1024 unikalnych workerów
- 12-bitowy licznik sekwencji: do 4096 ID na milisekundę na workera
Mocne strony:
- 8 bajtów — połowa rozmiaru UUID/ULID, mieści się w kolumnie
BIGINT - Monotoniczny w obrębie workera — gwarantowana kolejność per węzeł
- 4,096 mln ID/s teoretycznej przepustowości na workera
- Czytelny dla człowieka jako zwykła liczba całkowita
Słabe strony:
- Wymaga centralnej koordynacji — ID workerów muszą być przydzielane i zarządzane (zazwyczaj przez ZooKeeper, etcd lub usługę konfiguracji)
- Wrażliwość na rozjazd zegarów — jeśli zegary systemowe się rozjeżdżają, ID mogą kolidować lub cofać się
- Własna epoka — każda implementacja wybiera własną epokę, co utrudnia interoperacyjność między systemami
- Brak standardu — dziesiątki niekompatybilnych wariantów (Twitter, Discord, Instagram itp.)
// Snowflake ID generation (using sony/sonyflake)
package main
import (
"fmt"
"github.com/sony/sonyflake"
)
func main() {
sf := sonyflake.NewSonyflake(sonyflake.Settings{})
id, _ := sf.NextID()
fmt.Println(id) // → 175928847299543040
}
Kiedy wybrać Snowflake: gdy system generuje >100 tys. ID/s, potrzebne są zwarte 64-bitowe integery i istnieje już infrastruktura do przydzielania ID workerów (np. ordinale podów Kubernetes).
NanoID — zwarte ID bezpieczne dla URL
NanoID generuje krótkie (domyślnie 21 znaków), bezpieczne dla URL identyfikatory z alfabetu A-Za-z0-9_-. Korzysta z crypto.getRandomValues() dla bezpieczeństwa.
import { nanoid } from "nanoid";
const id = nanoid(); // → "V1StGXR8_Z5jdHi6B-myT"
const short = nanoid(10); // → "IRFa-VaY2b"
Nadaje się do: krótkich URL-i, kluczy komponentów frontendowych, kodów zaproszeń, nazw plików — wszędzie tam, gdzie liczy się długość ciągu i nie potrzeba kolejności na poziomie bazy danych ani interoperacyjności między systemami.
Nie nadaje się do: kluczy głównych bazy danych (brak natywnego typu DB, brak sortowalności, brak timestampa).
CUID2 — odporny na kolizje przy dużej skali
CUID2 generuje ID o zmiennej długości, zaprojektowane pod kątem skalowania horyzontalnego. Łączy licznik, timestamp, fingerprint i losowość.
Niszowe zastosowanie: systemy potrzebujące odporności na kolizje przy wielu niezależnych generatorach bez koordynacji. W praktyce UUID v7 pokrywa tę potrzebę z lepszą standaryzacją.
Kompleksowa tabela porównawcza
| Cecha | UUID v4 | UUID v7 | ULID | Snowflake | NanoID |
|---|---|---|---|---|---|
| Długość | 36 znaków | 36 znaków | 26 znaków | 15–20 cyfr | 21 znaków (domyślnie) |
| Rozmiar | 16 bajtów | 16 bajtów | 16 bajtów | 8 bajtów | ~21 bajtów |
| Sortowalny | Nie | Tak (czas) | Tak (czas) | Tak (czas) | Nie |
| Timestamp | Nie | 48-bit ms | 48-bit ms | 41-bit ms | Nie |
| Losowość | 122 bity | 74 bity | 80 bitów | 12-bit seq | ~126 bitów |
| Standard | RFC 9562 | RFC 9562 | Społecznościowy | Własnościowy | Społecznościowy |
| Natywny typ DB | uuid | uuid | Nie | BIGINT | Nie |
| Koordynacja | Brak | Brak | Brak | Rejestr workerów | Brak |
| Bezpieczny dla URL | Nie (myślniki) | Nie (myślniki) | Tak | Tak (integer) | Tak |
| Kolizja przy 1M ID | ~10⁻²² | ~10⁻¹⁸ | ~10⁻²⁰ | Zero (monotoniczny) | ~10⁻²¹ |
Przykłady kodu: generowanie każdego typu ID
JavaScript / TypeScript
import { v4 as uuidv4, v7 as uuidv7 } from "uuid";
import { ulid } from "ulid";
import { nanoid } from "nanoid";
// UUID v4
console.log(uuidv4());
// → "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
// UUID v7
console.log(uuidv7());
// → "01906b5e-4a3e-7234-8f56-b8c12d4e5678"
// ULID
console.log(ulid());
// → "01ARZ3NDEKTSV4RRFFQ69G5FAV"
// NanoID
console.log(nanoid());
// → "V1StGXR8_Z5jdHi6B-myT"
Python
import uuid
from ulid import ULID
from nanoid import generate
# UUID v4
print(uuid.uuid4())
# → "a8098c1a-f86e-11da-bd1a-00112444be1e"
# UUID v7 (Python 3.14+ planned, or use uuid7 package)
from uuid_extensions import uuid7
print(uuid7())
# → "01906b5e-4a3e-7234-8f56-b8c12d4e5678"
# ULID
print(ULID())
# → "01ARZ3NDEKTSV4RRFFQ69G5FAV"
# NanoID
print(generate(size=21))
# → "V1StGXR8_Z5jdHi6B-myT"
Go
package main
import (
"fmt"
"github.com/google/uuid" // UUID v4 & v7
"github.com/oklog/ulid/v2" // ULID
gonanoid "github.com/matoous/go-nanoid/v2" // NanoID
)
func main() {
// UUID v4
fmt.Println(uuid.New())
// → "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
// UUID v7
fmt.Println(uuid.Must(uuid.NewV7()))
// → "01906b5e-4a3e-7234-8f56-b8c12d4e5678"
// ULID
fmt.Println(ulid.Make())
// → "01ARZ3NDEKTSV4RRFFQ69G5FAV"
// NanoID
id, _ := gonanoid.New()
fmt.Println(id)
// → "V1StGXR8_Z5jdHi6B-myT"
}
Migracja z UUID v4 na v7
Jeśli system używa już kluczy głównych UUID v4, a chcesz osiągnąć korzyści wydajnościowe v7, mamy dobrą wiadomość: v4 i v7 dzielą ten sam 128-bitowy format i są przechowywane w tej samej kolumnie typu uuid. Migracja schematu nie jest potrzebna.
Strategia migracji
- Nowe rekordy używają v7, stare zostają na v4. Oba współistnieją w tej samej kolumnie. Zapytania i joiny działają identycznie.
- Zaktualizuj kod generujący ID — w warstwie aplikacji zamień
uuidv4()nauuidv7(). - NIE przepisuj istniejących ID v4. Zerwałoby to klucze obce, zewnętrzne odwołania i URL-e zapisane w cache.
- Monitoruj wydajność indeksu. W miarę przesuwania proporcji v4/v7 w stronę v7 fragmentacja indeksu będzie stopniowo maleć.
Sprawdzenie zgodności
-- Both v4 and v7 coexist in the same uuid column
SELECT id, version FROM (
SELECT id,
CASE get_byte(id::bytea, 6) >> 4
WHEN 4 THEN 'v4'
WHEN 7 THEN 'v7'
ELSE 'other'
END AS version
FROM your_table
) t
GROUP BY version;
Najczęściej zadawane pytania
UUID v7 czy auto-increment integer?
Auto-increment integery są prostsze i mniejsze (4–8 bajtów wobec 16 bajtów), wymagają jednak scentralizowanej sekwencji — może je generować tylko baza danych. UUID v7 można generować wszędzie (klient, edge, mikrousługa) bez okrężnej drogi do bazy. Auto-increment sprawdza się w prostych aplikacjach z jedną bazą; UUID v7 — w systemach rozproszonych, architekturach multi-tenant lub gdy potrzebne jest generowanie ID po stronie klienta.
Czy 74 bity losowości w UUID v7 to wystarczająco?
Tak. 74 bity losowości dają 2⁷⁴ ≈ 1,9 × 10²² możliwych wartości na milisekundę. Nawet przy generowaniu miliona ID na milisekundę prawdopodobieństwo kolizji wynosi ok. 10⁻¹⁰ — daleko poniżej granicy praktycznych obaw. 122 bity losowości w UUID v4 są dla większości zastosowań nadmiarem.
Czy mogę wyciągnąć timestamp z UUID v7?
Tak. Pierwsze 48 bitów koduje Unix timestamp w milisekundach:
function extractTimestamp(uuidv7) {
const hex = uuidv7.replace(/-/g, "").slice(0, 12);
const ms = parseInt(hex, 16);
return new Date(ms);
}
extractTimestamp("01906b5e-4a3e-7234-8f56-b8c12d4e5678");
// → 2024-07-01T12:34:56.000Z
To cecha, a nie błąd — ale jeśli potrzebne są ID nieprzejrzyste, należy użyć v4.
Czy PostgreSQL 18 wspiera UUID v7 natywnie?
PostgreSQL 18 (wydany w 2025 r.) dodaje wbudowaną funkcję uuidv7(), eliminując potrzebę rozszerzeń typu pgcrypto czy pg_uuidv7. MySQL nie ma jeszcze natywnego generowania v7 — należy generować je w warstwie aplikacji.
Dlaczego nie używać po prostu ULID?
ULID powstał wcześniej niż UUID v7 i rozwiązuje ten sam problem. Teraz, gdy v7 jest standardem IETF (RFC 9562), ma kluczowe przewagi: natywny typ bazodanowy uuid (16 bajtów, efektywnie indeksowany), szersze wsparcie języków/frameworków oraz formalną standaryzację. Jeśli używasz już ULID — działa świetnie i nie trzeba migrować. Dla nowych projektów lepszy jest UUID v7.
Kiedy Snowflake ID jest lepszym wyborem?
Gdy potrzebne są zwarte 64-bitowe ID przy ekstremalnej przepustowości (>100 tys. ID/s na węzeł) i istnieje już infrastruktura do przydzielania ID workerów. 8-bajtowa kolumna BIGINT Snowflake’a jest o połowę mniejsza niż UUID, co ma znaczenie przy miliardach wierszy. Kompromisem jest złożoność operacyjna: trzeba zarządzać przydziałem ID workerów i radzić sobie z rozjazdem zegarów.
Potrzebujesz wygenerować UUID od ręki? Wypróbuj nasz generator UUID — obsługuje v1, v4, v5 oraz v7 z generowaniem wsadowym i dekodowaniem, w 100% w przeglądarce.