UUID v1 vs v4 vs v7: scegliere una chiave primaria DB

Pubblicato il 2026-04-13 8 min di lettura

Riepilogo (TL;DR)

Per dirla senza mezzi termini: usare UUID v4 come chiave primaria di una tabella a inserimento caldo è un piede sparato. Di recente ho guardato un workload PostgreSQL 16 dove events.id aveva come default gen_random_uuid() (un v4), e ogni INSERT atterrava su una foglia B-tree casuale – raffreddando gli shared_buffers e trascinando la frammentazione dell’indice in cifre alte a una cifra secondo quanto riportato da pgstattuple. Cambiare il default della colonna a un generatore v7 – stesso tipo uuid da 16 byte, stesso indice – ha tagliato la latenza media di INSERT a circa un terzo di quanto era, e i numeri di frammentazione si sono stabilizzati. Scegliere una versione di UUID è una decisione di design del database, non una decisione di crittografia. Un UUID è un valore a 128 bit scritto come cifre esadecimali 8-4-4-4-12, con quattro bit fissati come versione e due o tre bit fissati come variante. La Versione 1 stampa tempo e un identificatore di nodo (storicamente un indirizzo MAC) in quei 128 bit – approssimativamente ordinabile, ma rivela l’host. La Versione 4 riempie i bit non riservati con dati casuali: forte non-indovinabilità, ma nessun ordinamento, quindi gli inserimenti atterrano in posizioni arbitrarie dell’indice. La Versione 7, formalizzata in RFC 9562 (2024), mette un timestamp in millisecondi Unix nei bit alti e una coda casuale, combinando la sicurezza di v4 con la località dell’indice di v1. Per un’API pubblica dove la casualità opaca conta davvero, v4 è ancora il default. Quasi ovunque altro, v7 è la chiave primaria migliore. v1 è legacy – accettato dai database, ma non dovrebbe essere una nuova scelta.

Contesto e concetti

Un UUID è 128 bit. Per convenzione viene stampato come 32 cifre esadecimali raggruppate con trattini: xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx. La cifra M contiene la versione (1, 4, 7, ecc.) e i bit alti di N contengono la variante. Tutto il resto è specifico della versione.

La Versione 1 è stata progettata per l’unicità tra macchine e nel tempo. Il suo timestamp a 60 bit conta intervalli di 100 nanosecondi dal 1582-10-15, un campo clock-sequence gestisce i rollback dell’orologio, e un ID di nodo a 48 bit era originariamente l’indirizzo MAC della rete. La conseguenza sulla privacy è diretta: un UUID v1 coniato sul tuo portatile codifica il MAC del tuo portatile, e un uuid -d di una riga lo inverte. Le librerie moderne a volte randomizzano l’ID del nodo per evitare la fuga, ma molte seguono ancora la regola originale.

La Versione 4 è 122 bit di casualità con 6 bit fissati per versione e variante. Assume un RNG forte (il crypto.randomUUID() del browser o il gen_random_uuid() di PostgreSQL). Le collisioni sono effettivamente impossibili a qualsiasi scala realistica – il bound del compleanno dà circa una probabilità di uno su un trilione dopo aver generato circa un miliardo di v4. Il rovescio è che v4 successivi sono non correlati, quindi inserirli in un indice tocca pagine casuali, danneggiando la località della cache e l’amplificazione di scrittura (WAL più full-page writes in Postgres).

La Versione 7 è parte di RFC 9562, pubblicata nel 2024, che ha reso obsoleto RFC 4122 e ha aggiunto le versioni 6, 7 e 8. Un UUID v7 memorizza un timestamp a 48 bit in millisecondi Unix nei suoi bit alti, seguito dal tag di versione, un piccolo campo rand_a, il tag di variante e una coda rand_b a 62 bit. L’effetto pratico è che valori v7 generati nello stesso millisecondo sono adiacenti nell’ordine di sort, e valori tra millisecondi ordinano cronologicamente. La coda casuale fornisce ancora molta più entropia che serve per l’unicità all’interno di un millisecondo. PostgreSQL 18 include un uuidv7() nativo; su versioni precedenti puoi usare l’estensione pg_uuidv7 o generarli a livello applicativo (Node uuid 9.x, Python uuid6).

I bit di variante contano perché distinguono la famiglia RFC 9562 / 4122 dagli UUID legacy Microsoft e Apollo. Per questa guida, assumi la variante RFC (la prima cifra esadecimale del gruppo N è 8, 9, a o b).

Il formato di storage è una questione separata. Il tipo uuid nativo di PostgreSQL 16 memorizza il valore come 16 byte; MySQL usa tipicamente BINARY(16) o CHAR(36) – la forma stringa raddoppia lo storage e rende i confronti carattere per carattere invece che byte per byte. La scelta della versione e la scelta del formato di storage interagiscono: ordinare v7 come valore binario è economico e corretto, ordinarlo come stringa esadecimale è corretto ma più lento, e ordinare v4 è privo di senso indipendentemente dallo storage.

Confronto e dati

Proprietàv1v4v7
Input di generazioneTimestamp + clock sequence + ID nodo122 bit casualiTimestamp Unix ms a 48 bit + coda casuale
PrivacyRivela l’ID nodo (spesso il MAC)Nessuna info su host o tempoRivela il tempo di creazione in ms, nessun host
Ordinabile per tempoSì (ma ordine byte ≠ ordine tempo senza riarrangiamento)NoSì – l’ordine lessicografico corrisponde all’ordine cronologico
Località dell’indiceModerataScarsa (inserimenti casuali attraverso il B-tree)Buona (quasi monotona)
Uso tipicoSistemi legacy, alcuni ID COM/WindowsID di API pubbliche, token di sessione, saltLog di eventi, inserimenti ad alto volume, tabelle paginate per tempo
EntropiaBassa (la maggior parte dei bit è tempo/nodo)Alta (circa 122 bit)Coda alta (circa 74 bit), basse collisioni entro il ms

Un modello mentale grezzo: v4 massimizza la non-indovinabilità al costo del comportamento dell’indice, v7 mantiene abbastanza non-indovinabilità per la maggior parte delle applicazioni mentre ripristina il pattern di insert-in-coda che i database adorano, e v1 è un artefatto storico che vale la pena riconoscere ma non scegliere.

Scenari reali

Scenario 1 — Log di eventi pesante in append. Questo è esattamente il workload dell’aneddoto iniziale. Una tabella che ingerisce milioni di righe al giorno ed è solitamente interrogata come “ultime 24 ore, ordinate per tempo” beneficia direttamente di v7. Le nuove righe atterrano in coda all’indice della chiave primaria, quindi le pagine calde restano calde e le scansioni per intervallo su finestre temporali mappano su segmenti di indice contigui. Migrare da v4 a v7 qui riduce spesso la latenza di scrittura e la frammentazione dell’indice senza cambiare alcun codice di query – tipicamente una modifica di una riga al default della colonna.

Scenario 2 — ID pubblici visibili all’utente. Link di condivisione come /orders/{id} devono essere non indovinabili così che i visitatori non possano enumerare gli ordini di altri utenti. v4 è il default sicuro. Se vuoi anche i benefici di v7, sii consapevole che un v7 rivela il suo timestamp di creazione al millisecondo, che potrebbe andare bene per gli ordini ma potrebbe far trapelare segnali di business (es. volume di checkout esatto per minuto) in contesti più sensibili. Il compromesso che ho raccomandato ai team è un pattern a doppio ID: v7 internamente per la chiave primaria, e un v4 separato o uno slug casuale corto esposto al mondo esterno. Mantieni il comportamento dell’indice senza far trapelare timing esternamente.

Scenario 3 — Sistemi multi-regione o sharded. Il prefisso timestamp di v7 significa che due regioni che generano UUID nello stesso millisecondo si intercaleranno pulitamente per tempo, ma all’interno di un millisecondo non c’è garanzia di ordinamento tra regioni. Se hai bisogno di un ordinamento cross-regione più stretto, ULID (un timestamp a 48 bit + 80 bit di casualità codificati come Crockford Base32) ha proprietà quasi identiche e una forma testuale più compatta di 26 caratteri. Gli ID in stile Snowflake vanno oltre includendo un ID macchina esplicito per ordinamento per nodo (il design originale di Twitter e la variante di Discord sono entrambi a 64 bit), al costo di richiedere coordinamento per allocare quegli ID.

Errori comuni

“Gli UUID sono sempre lenti nei database.” Sono più lenti di un int a 4 byte in termini grezzi, ma il costo reale viene dalla frammentazione da inserimento casuale negli indici B-tree, che v7 elimina in gran parte. Memorizzare gli UUID come 16 byte invece che come stringa da 36 caratteri dimezza la dimensione dell’indice e velocizza i confronti. Molti benchmark “UUID sono lenti” sono in realtà “v4 memorizzato come CHAR(36) in MySQL è lento”.

“v4 è l’unica versione sicura.” La coda casuale di v7 è ancora un grande pool di entropia, e per la maggior parte delle applicazioni – riferimenti di sessione, ID di API – è abbastanza non indovinabile che un attaccante non li enumererebbe. Dove l’indovinabilità conta è il prefisso timestamp: v7 rivela quando la riga è stata creata. Se è accettabile (e di solito lo è), v7 è una scelta ragionevole anche per ID esterni.

“Gli UUID devono essere memorizzati come stringhe.” La forma stringa è 36 caratteri (32 hex più 4 trattini) vs 16 byte binari. Il binario è più compatto e ordina correttamente come byte, il che conta per v1 con il suo ordine di byte non monotono e per v7 dove l’ordine di byte dovrebbe allinearsi con il tempo. Il tipo uuid di PostgreSQL memorizza già il valore come 16 byte, quindi non c’è nulla di extra a cui pensare lì.

“La fuga del MAC di v1 non conta perché nessuno guarda.” Il MAC in un UUID v1 è un’inversione nota – uuid -d e qualsiasi strumento forense lo estraggono. Se i tuoi UUID appaiono in URL, ticket di supporto o dump di log condivisi con terze parti, quello è un vero divulgazione di informazioni.

Lista di controllo

  1. Questo UUID apparirà in un indice di database che riceve inserimenti frequenti? Preferisci v7. Ricadi su v4 solo se la non-indovinabilità del tempo di creazione è essenziale.
  2. L’UUID è visibile agli utenti finali o ai partner? Qualsiasi versione funziona; conferma solo che la fuga di timestamp di v7 sia accettabile per il contesto.
  3. Sei su Postgres? Memorizza come uuid (16 byte). Su MySQL, usa BINARY(16) a meno che la compatibilità stringa superi il costo di dimensione.
  4. Hai bisogno di garanzie di ordinamento tra più generatori? v7 da solo non basta; considera ULID con lo stesso prefisso temporale, o un ID in stile Snowflake con un ID macchina esplicito.
  5. v1 è ancora nel codebase? Documenta la fuga del MAC e pianifica una migrazione quando lo schema lo consente.
  6. Stai generando UUID lato client? Usa una libreria che chiama un RNG crittograficamente forte (crypto.randomUUID() nei browser moderni per v4; le librerie v7 tipicamente avvolgono lo stesso RNG).

Strumento correlato

Il generatore UUID di Patrache Studio produce UUID v4 e v7 localmente, così i valori generati non vengono mai loggati a un servizio di terze parti. Gli UUID viaggiano quasi sempre dentro payload JSON – Formattazione, validazione e schema di JSON nella pratica copre i pattern di schema che mantengono quegli ID ben tipizzati mentre si spostano tra servizi. Quando hai bisogno di una forma testuale compatta – per esempio, un ID corto da 22 caratteri derivato da un UUID a 16 byte – le regole in Base64 e codifica URL: scopo, insidie, uso corretto spiegano perché Base64URL invece di Base64 standard è la scelta giusta.

Riferimenti