UUIDv7: az elsődleges kulcs, ami nem lő szét minden indexet

A random UUIDv4 primary key évek óta ismert teljesítményprobléma B-fa indexeknél. Az RFC 9562-es UUIDv7 időrendi sorrendbe teszi az azonosítókat, Node.js 26.1-től és PostgreSQL 18-tól natívan is elérhető — nézzük meg, hogyan működik, és mibe kerül.

Ha valaha UUID-t használtál elsődleges kulcsként egy adatbázistáblában, valószínűleg gen_random_uuid()-t vagy crypto.randomUUID()-t hívtál, ami egy teljesen véletlenszerű UUIDv4-et ad vissza. Kényelmes, ütközésmentes, elosztottan generálható – és pont ez a véletlenszerűség az, ami tönkreteszi az adatbázis írási teljesítményét. 2024 májusában megjelent egy frissített UUID-specifikáció, ami erre ad natív megoldást, és mostanra a legfontosabb eszközök – Node.js, PostgreSQL – is leszállítják a támogatását.

UUIDv7 bitfelépítése: időbélyeg, verzió, random adat és variáns mezők egy 128 bites azonosítóban

Miért fáj egy random kulcs a B-fának

Egy relációs adatbázis az elsődleges kulcs indexét (jellemzően B-fát) a beszúrt értékek sorrendjében tartja karban. Ha a kulcs egy auto-increment szám, minden új sor a fa jobb szélére kerül – ez a legolcsóbb művelet, amit egy B-fa ismer. Ha viszont a kulcs egy egyenletesen véletlenszerű UUIDv4, minden beszúrás egy véletlen helyre kerül a fában. Ez azt jelenti, hogy a tábla méretének növekedésével egyre nagyobb eséllyel kell egy olyan indexlapot betölteni a memóriába (vagy lemezről), ami éppen nincs a cache-ben – az pedig lapfelosztásokhoz, extra write-ahead log (WAL) forgalomhoz és romló cache hit rate-hez vezet.

Ez nem elméleti probléma. Tomas Vondra 2018-as, sokat idézett mérésében azt mérte, hogy nagy, memóriát meghaladó méretű táblánál a random UUID kulcs a szekvenciális generátorokhoz képest kb. 20%-os áteresztőképességre esik vissza, a WAL-forgalom 20 GB-ról 2,5 GB-ra csökken sorrendtartó kulcsokkal, a cache hit ratio pedig 85%-ról közel 99%-ra javul. A mechanizmus – B-fa, lapfelosztás, WAL – azóta nem változott, ezért ezek a számok ma is jól illusztrálják, mekkora az ára a „csak legyen egyedi” típusú kulcsválasztásnak.

Mi az az UUIDv7

Az RFC 9562 (2024 május, ez váltotta le a régi RFC 4122-t) definiálja a 6-os, 7-es és 8-as UUID-verziókat. Az UUIDv7 128 bitje így oszlik meg:

  • 48 bit – Unix-időbélyeg ezredmásodpercben, big-endian sorrendben
  • 4 bit – verziómező, fix érték: 0111
  • 12 bitrand_a, véletlen adat (vagy finomabb időfelbontás/számláló, lásd lentebb)
  • 2 bit – variánsmező, fix érték: 10
  • 62 bitrand_b, további véletlen adat

A lényeg: az időbélyeg a legfelső biteken van, tehát az UUID-k szövegesen és bájtonként is nagyjából időrendi sorrendbe rendeződnek – pont úgy viselkednek a B-fában, mint egy auto-increment szám, miközben megmarad az az előny, hogy bármelyik kliens, szerver vagy háttérfolyamat központi koordináció nélkül tud generálni egyet.

Kipróbálva

A specifikáció annyira egyszerű, hogy pár sorban végig lehet írni – ez jól megmutatja, mi történik a kulisszák mögött (Node.js-ben futtatva, beépített node:crypto modullal):

const crypto = require('node:crypto');

function uuidv7() {
  const bytes = crypto.randomBytes(16);
  const ms = BigInt(Date.now());

  // bit 0-47: unix_ts_ms
  for (let i = 0; i < 6; i++) {
    bytes[i] = Number((ms >> BigInt((5 - i) * 8)) & 0xffn);
  }

  // bit 48-51: verzió (0111), bit 52-63: rand_a (marad véletlen)
  bytes[6] = 0x70 | (bytes[6] & 0x0f);
  // bit 64-65: variáns (10), bit 66-127: rand_b (marad véletlen)
  bytes[8] = 0x80 | (bytes[8] & 0x3f);

  const hex = bytes.toString('hex');
  return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;
}

function extractTimestamp(uuid) {
  const hex = uuid.replace(/-/g, '').slice(0, 12);
  return new Date(parseInt(hex, 16));
}

console.log(uuidv7());
// pl. 019fe28a-e717-76a3-a5ec-937aa01c9291
console.log(extractTimestamp('019fe28a-e717-76a3-a5ec-937aa01c9291').toISOString());
// 2026-08-08T18:03:01.783Z

Az extractTimestamp function működését a PostgreSQL 18 dokumentációjának saját példa-UUID-jén is leellenőriztem: a 019535d9-3df7-79fb-b466-fa907fa17f9e első 12 hex karakteréből (019535d93df7) számolva pontosan 2025-02-24T02:46:24.503Z jön ki, ami a doksiban feltüntetett 2025-02-23 21:46:24.503-05 időponttal (UTC−5 zónaidő) egyezik. Vagyis az időbélyeg tényleg egyszerű bináris kivágással visszafejthető – ez nem mellékhatás, hanem szándékos tervezési döntés.

A monotonitás nem ingyenes

Van egy csapda, amibe könnyű belefutni. Ha a fenti generátorral egymás után, ugyanabban az ezredmásodpercben többször kérünk UUID-t, a sorrendjük nem garantált, mert a rand_a és rand_b mezők tisztán véletlenek:

const ids = Array.from({ length: 5 }, () => uuidv7());
const sorted = [...ids].sort();
console.log(JSON.stringify(ids) === JSON.stringify(sorted));
// false – ugyanazon az ezredmásodpercen belül nem stabil a sorrend

Ez pontosan megfelel annak, amit a Node.js dokumentációja is leír a saját, natív crypto.randomUUIDv7() implementációjáról (Node.js 26.1.0-tól elérhető): „a beágyazott időbélyeg nem monoton órán alapul, és nem garantált, hogy szigorúan növekvő legyen”. Az RFC emiatt megenged három különböző módszert a monotonitás javítására – a legegyszerűbb egy számláló beépítése a rand_a mezőbe. A PostgreSQL 18 saját uuidv7() függvénye pont ezt teszi: a 12 bites rand_a helyén egy ezredmásodperc-alatti finomabb időbélyeget tárol, így egy munkamenetben garantáltan növekvő sorozatot ad – ezért a gyakorlatban Postgres alatt jobb sorrendtartást kapunk, mint egy naiv, csak véletlen adatot használó implementációtól.

Natív támogatás mindenhol

A fenti kézzel írt kód főleg oktatási célra jó – éles kódban nincs értelme újraimplementálni, amit a platform már ad:

  • Node.js 26.1.0+: crypto.randomUUIDv7([options]), disableEntropyCache opcióval, ha nem szeretnénk a beépített 128-as UUID-előgenerálási cache-t használni.
  • PostgreSQL 18 (2025 szeptemberében jelent meg): uuidv7() a generáláshoz, uuid_extract_timestamp() és uuid_extract_version() a visszafejtéshez – közvetlenül SQL-ből.
  • Régebbi Node-verzión vagy böngészőben a népszerű uuid npm csomag v7() függvénye ugyanezt adja userlandból.

Fontos: a natív crypto.randomUUID() (a v4-es verzió) böngészőben és Node-ban is évek óta elérhető, de az UUIDv7 egyelőre nem szabvány Web API – a böngészőben egyelőre a uuid csomagra kell hagyatkozni.

Amit elveszítünk: az időbélyeg kiszivárog

Az UUIDv7 pont attól hasznos, hogy elárulja a keletkezés időpontját – ez viszont adatvédelmi szempontból nem mindegy. Az RFC 9562 biztonsági megfontolásokról szóló szakasza kifejezetten kimondja: ha egy alkalmazásban az UUID-t bármilyen biztonsági művelethez használjuk, akkor UUIDv4-et kell választani, mert az időbélyeg (számlálóval kombinálva) elárulja a rekordok létrehozási sorrendjét. Gyakorlati példa: ha a felhasználói fiók ID-ja UUIDv7, egy támadó két ID összehasonlításából megbecsülheti, melyik fiók jött létre korábban, vagy durván beazonosíthatja a regisztráció időpontját. Meghívó tokeneknél, jelszó-visszaállító linkeknél, session azonosítóknál ez elég jó indok arra, hogy maradjunk a random v4-nél, és az UUIDv7-et csak ott vezessük be, ahol a rendezhetőség ér többet, mint a keletkezési idő elrejtése – tipikusan: elsődleges kulcsok, log- és eseményazonosítók, elosztottan generált rekordok.

Mikor éri meg

Meglévő, UUIDv4 kulcsokkal teli táblát nem lehet utólag „időrendbe rakni” – a régi sorok kulcsai megmaradnak véletlenszerűnek, így a migráció gyakorlatilag egy új tábla feltöltését jelenti. Új projekteknél viszont, ahol amúgy is UUID mellett döntenél elosztott generálhatóság miatt, az UUIDv7 szinte ingyen ad cserébe jobb index-lokalitást és rendezhetőséget – a fő ár mindössze az, hogy tudatosan kell dönteni arról, hol számít a keletkezési idő elrejtése.

Források

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük