Przejdź do treści
Toova
Wszystkie narzędzia

UUID v4 vs v7 — Różnice i Kiedy Używać Każdego

Toova

UUID v4 jest domyślnym wyborem dla rozproszonych kluczy głównych od ponad dekady. Wygeneruj losową 128-bitową wartość, sformatuj jako osiem grup hex, gotowe. Bez wymaganej koordynacji, praktycznie zerowe prawdopodobieństwo kolizji, działa wszędzie. Więc dlaczego UUID v7 - ustandaryzowane w RFC 9562 w 2024 - rozprzestrzenia się szybko w systemach produkcyjnych w 2026?

Krótka odpowiedź: wydajność bazy danych. UUID v4 niszczy lokalność indeksu B-tree. UUID v7 to naprawia, zachowując wszystko inne, co programiści kochają w UUID. Ten artykuł wyjaśnia, czym faktycznie jest każda wersja, dlaczego różnica ma znaczenie w skali, kiedy wybierać każdą i jak migrować bez przestoju.

Potrzebujesz wygenerować UUID teraz? Wypróbuj Toova UUID generator, obsługujący zarówno v4, jak i v7 zbiorczo. Dla innych losowych identyfikatorów, generator losowych ciągów i generator haseł pokrywają krótsze tokeny.

UUID v4 — Czysta Losowość

UUID wersja 4 używa kryptograficznie bezpiecznego pseudolosowego generatora liczb (CSPRNG) do wypełnienia 122 ze 128 bitów. Pozostałe sześć bitów jest stałych: cztery bity kodują wersję (0100), a dwa bity kodują wariant (10). Wynik wygląda tak:

f47ac10b-58cc-4372-a567-0e02b2c3d479
              ^^^^
              wersja = 4 (losowa)

Losowość to funkcja. Dwa niezależne systemy mogą generować UUID bez koordynacji i nigdy nie kolidować - prawdopodobieństwo kolizji w zestawie 2,71 kwadrylionów UUID v4 wynosi około 50%, co oznacza, że dla każdej praktycznej aplikacji ryzyko jest pomijalne. Nie potrzebujesz scentralizowanego serwera ID, sekwencji bazy danych ani rozproszonej blokady.

Jak generuje się v4

import { v4 as uuidv4 } from 'uuid';
const id = uuidv4();
// => 'f47ac10b-58cc-4372-a567-0e02b2c3d479'

Biblioteka uuid (JavaScript), Pythonowe uuid.uuid4(), Go google/uuid i każdy główny runtime języka deleguje do CSPRNG systemu operacyjnego - /dev/urandom na Linuksie, CryptGenRandom na Windows. Koszt generowania jest efektywnie zerowy.

Problem: losowe wstawianie zabija indeksy B-tree

Indeks B-tree trzyma dane posortowane. Gdy wstawiasz nowy wiersz, baza danych znajduje, gdzie nowy klucz pasuje w posortowanej kolejności, i umieszcza go tam. Jeśli każdy nowy klucz jest losowy, ląduje na losowej pozycji w indeksie - co oznacza, że każde wstawienie musi załadować inną stronę z dysku do buffer poola. Przy niskim wolumenie jest to niewidoczne. Przy wysokim wolumenie (miliony wierszy, wysoki stopień INSERT), tworzy to wzorzec zwany fragmentacją indeksu: indeks wypełnia się półpustymi stronami, ponieważ każde wstawienie trafia w inne miejsce, a buffer pool ciągle się obraca, ponieważ gorący zestaw roboczy obejmuje cały indeks zamiast przewidywalnego ostatniego wycinka.

Objawy w produkcji: opóźnienie INSERT rośnie, narzut autovacuum wzrasta (PostgreSQL), ciśnienie checkpointu rośnie, a wydajność odczytu dla ostatnich rekordów degraduje, ponieważ "ostatnie" nie mapuje się już na żadną lokalność w indeksie. To nie jest hipotetyczne - to dobrze udokumentowany punkt bólu na dużych tabelach PostgreSQL i MySQL z kluczami głównymi UUID v4.

UUID v7 — Losowość Uporządkowana Czasowo

UUID v7 został zaprojektowany jawnie, aby rozwiązać problem fragmentacji B-tree. Koduje 48-bitowy znacznik milisekund Unix w najznaczniejszych bitach, następnie bity wersji, 12 losowych bitów, bity wariantu i 62 dodatkowe losowe bity. Łączna losowość: 74 bity - nadal znacznie więcej niż potrzeba, aby zapobiec kolizjom.

018f4e6b-a23c-7d45-9abc-0e02b2c3d479
 ^^^^^^^^^^^^^^
 48-bitowy znacznik czasu Unix (precyzja ms)
                   ^
                   wersja = 7

Ponieważ znacznik czasu zajmuje wysokie bity, UUID generowane później sortują się po UUID generowanych wcześniej. Wstawienia są zawsze dołączane do prawej krawędzi indeksu. Baza danych musi tylko trzymać najnowszą stronę indeksu gorącą w buffer poolu, nie cały indeks. Przy wysokich stopach INSERT, samo to może obciąć opóźnienie zapisu o 30-60% i zredukować I/O o rząd wielkości na tabelach z dziesiątkami milionów wierszy.

Jak generuje się v7

import { v7 as uuidv7 } from 'uuid';
const id = uuidv7();
// => '018f4e6b-a23c-7d45-9abc-0e02b2c3d479'

Ta sama biblioteka uuid dodała wsparcie v7 w wersji 10. Standardowa biblioteka Pythona dodała ją w 3.14. PostgreSQL 17 zawiera uuidv7() jako funkcję wbudowaną. Jeśli jesteś na starszym stosie, kilka małych bibliotek dostarcza generowanie v7 bez zależności.

Monotoniczność sub-milisekundowa

Co się dzieje, gdy dwa UUID v7 są generowane w ramach tej samej milisekundy? RFC 9562 pozwala implementacjom zwiększać monotoniczny licznik w bitach losowych, aby zagwarantować uporządkowanie w ramach tej samej milisekundy. Biblioteka uuid robi to domyślnie. Wynik: nawet jeśli 10 000 ID jest generowanych w jednej milisekundzie, nadal sortują się poprawnie.

Problem Fragmentacji Indeksu B-Tree Szczegółowo

Aby zrozumieć, dlaczego to ma znaczenie, rozważ, co się dzieje przy 10 000 wstawień na sekundę z kluczem głównym UUID v4 na tabeli ze 100 milionami istniejących wierszy:

  • Każde wstawienie generuje losowy 128-bitowy klucz, który ląduje na losowej pozycji wśród 100 milionów istniejących wpisów.
  • Baza danych musi załadować konkretną stronę B-tree zawierającą tę pozycję do buffer poola.
  • Ze 100 milionami wierszy i 8 KB stronami, indeks obejmuje mniej więcej 100 000 stron. Każdej sekundy potrzeba 10 000 różnych stron - znacznie więcej, niż typowy 8 GB shared_buffers może pomieścić tylko dla tego indeksu.
  • Każdy cache miss skutkuje odczytem z dysku. Przy 10 000 wstawień/sek, to może generować tysiące losowych odczytów dyskowych na sekundę, nasycając nawet SSD na dużych tabelach.

Z UUID v7 wszystkie 10 000 wstawień na sekundę ląduje na najbardziej wysuniętej w prawo stronie liścia (lub małej garstce ostatnich stron). Buffer pool musi tylko trzymać te kilka stron gorące. Wskaźnik trafień cache dla zapisów zbliża się do 100%. Wzmocnienie zapisu drastycznie spada.

Ta sama korzyść stosuje się do skanowań zakresowych: WHERE created_at BETWEEN x AND y na tabeli v4 wymaga pełnego skanowania indeksu lub osobnego indeksu znacznika czasu. Na tabeli v7 sam klucz główny jest indeksem znacznika czasu - zapytanie może szukać bezpośrednio do właściwego zakresu.

Jak Używać UUID v7 w PostgreSQL

-- Działa natywnie z typem UUID w PostgreSQL 16+
CREATE TABLE events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- v4 (losowy)
  -- Przełącz na UUIDv7 przez rozszerzenie lub generowanie po stronie aplikacji
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Z UUIDv7: znacznik czasu jest już osadzony, więc
-- ta osobna kolumna created_at jest często redundantna.

PostgreSQL 17 dostarcza natywnie uuidv7(). Dla PostgreSQL 14-16, rozszerzenie pg_uuidv7 dostarcza tę samą funkcję. Typ danych UUID przechowuje zarówno v4, jak i v7 identycznie - 16 bajtów, bez narzutu. Jedyna różnica to wzorzec bitów określający kolejność sortowania.

Jedna użyteczna konsekwencja: ponieważ znacznik czasu jest osadzony, wiele tabel nie potrzebuje już osobnej kolumny created_at do porządkowania lub wyświetlania. Możesz wydobyć znacznik czasu z UUID v7 pojedynczym wywołaniem funkcji. To redukuje złożoność schematu i eliminuje zapis na wstawienie.

UUID v4 vs v7 — Kompromisy

Właściwość UUID v4 UUID v7
Bity losowości 122 74
Kolejność sortowania Losowa Chronologiczna
Wydajność INSERT B-tree Słaba (fragmentacja) Doskonała (sekwencyjna)
Wyciek znacznika czasu Brak Tak (precyzja ms)
Standard RFC RFC 4122 (2005), RFC 9562 (2024) RFC 9562 (2024)
Wsparcie bibliotek Uniwersalne Szybko rosnące (2024-2026)
Osadzone created_at Nie Tak

Przewodnik Migracji — v4 do v7

Migrowanie istniejącej tabeli z UUID v4 do kluczy głównych v7 to wielokrokowa operacja. Kluczowe ograniczenie: nie możesz zmienić wartości klucza głównego, do którego odwołują się klucze obce, bez aktualizacji wszystkich tabel referencjonujących też. Zaplanuj okno konserwacji lub użyj podejścia z podwójnym zapisem.

-- 1. Dodaj nową kolumnę
ALTER TABLE orders ADD COLUMN id_v7 UUID;

-- 2. Backfill istniejących wierszy (zachowaj oryginalne created_at jako
--    źródło znacznika czasu; użyj funkcji biblioteki UUIDv7 w swojej aplikacji)
UPDATE orders SET id_v7 = generate_uuidv7(created_at);

-- 3. Zweryfikuj, że nie pozostały NULL-e
SELECT COUNT(*) FROM orders WHERE id_v7 IS NULL;

-- 4. Zamień kolumny (wymaga krótkiego okna konserwacji)
ALTER TABLE orders ALTER COLUMN id_v7 SET NOT NULL;
ALTER TABLE orders ALTER COLUMN id_v7 SET DEFAULT generate_uuidv7(now());
ALTER TABLE orders RENAME COLUMN id TO id_v4_old;
ALTER TABLE orders RENAME COLUMN id_v7 TO id;
ALTER TABLE orders ADD PRIMARY KEY (id);

Dla migracji bez przestoju na tabelach z dużym ruchem, rekomendowane podejście to:

  1. Dodaj nową kolumnę id_v7 z domyślną wartością serwera generującą UUID v7 idące do przodu.
  2. Backfilluj stare wiersze w batchach podczas okresów małego ruchu, używając istniejącego znacznika czasu created_at jako zarodka dla części znacznika czasu v7.
  3. Zaktualizuj wszystkie kolumny kluczy obcych w referencjonujących tabelach, aby wskazywały na id_v7.
  4. Przemianuj kolumny i upuść stare ograniczenie klucza głównego podczas krótkiego okna konserwacji.

Krok backfillu jest najdłuższy. Przy 10 000 wierszy na batch z 50 ms snem między batchami, tabela 100-milionowych wierszy zajmuje mniej więcej 8 godzin. Zacznij wcześnie.

Rzeczywisty Wpływ

Kilka zespołów inżynierskich opublikowało benchmarki porównujące UUID v4 i v7 na zbiorach danych o rozmiarze produkcyjnym. Spójne wyniki:

  • Przepustowość INSERT: poprawa 2-5x na tabelach z 50M+ wierszami przy przełączaniu z v4 do v7, z zyskiem rosnącym wraz ze wzrostem tabeli.
  • Opóźnienie zapisu p99: spada z setek milisekund (v4, pod obciążeniem) do jednocyfrowych milisekund (v7) na tym samym sprzęcie.
  • Rozmiar indeksu: indeksy B-tree na kolumnach v7 są o 15-30% mniejsze niż ekwiwalentne indeksy v4 na tych samych danych, ponieważ fragmentacja zostawia mniej półpustych stron.
  • Wydajność buffer pool: wskaźnik trafień shared buffers dla indeksu klucza głównego idzie z ~40% (v4, duża tabela) do ~99% (v7), ponieważ tylko ostatnie strony muszą pozostać gorące.

Zyski są pomijalne poniżej mniej więcej 1 miliona wierszy. Jeśli twoja tabela pozostaje mała, trzymaj się v4 dla prostoty. Powyżej 10 milionów wierszy z jakimkolwiek znaczącym stopiem INSERT, v7 to lepsza wartość domyślna.

Kiedy Używać Każdego

Użyj UUID v7 gdy:

  • Projektujesz nowy schemat, a tabela będzie rosnąć duża (10M+ wierszy).
  • Tabela ma wysoki stopień INSERT - zdarzenia, logi, zamówienia, wiadomości, powiadomienia.
  • Chcesz klucza głównego, który podwójnie służy jako znacznik czasu utworzenia (eliminuje kolumnę).
  • Jesteś na PostgreSQL 17, MySQL 8.0+ lub nowoczesnym ORM wspierającym generowanie v7.
  • Sortowanie po ID jest semantycznie równoważne sortowaniu po czasie utworzenia - co będzie dla większości tabel typu append.

Użyj UUID v4 gdy:

  • Identyfikator jest wystawiany użytkownikom i nie może ujawniać czasu utworzenia (kody zaproszeń, linki udostępnienia, uchwyty rozliczeniowe).
  • Tabela jest mała i stabilna - żadna korzyść wydajnościowa nie uzasadnia kosztu migracji.
  • Generujesz ID w kontekście, gdzie znacznik czasu utworzenia jest wrażliwy (prywatne rekordy użytkowników w produkcie konkurującym na optyce wzrostu).
  • Potrzebujesz czegoś jako jednorazowe poświadczenie lub token - użyj dedykowanego generatora sekretów, nie żadnej wersji UUID.

Podsumowanie

UUID v4 jest losowy, prywatny i uniwersalnie obsługiwany. UUID v7 jest uporządkowany czasowo, przyjazny bazie danych i teraz standardowy w głównych bibliotekach i bazach danych. Dla nowych schematów z dużymi lub szybko rosnącymi tabelami, v7 to lepsza wartość domyślna. Dla ID wystawianych użytkownikom, gdzie wyciek znacznika czasu jest problemem, v4 pozostaje właściwym wyborem.

Generuj obie wersje natychmiast generatorem UUID Toova - bez wymaganego konta. Dla krótszych tokenów, generator losowych ciągów produkuje ID alfanumeryczne o dowolnej długości.