UUID v4 vs v7 — Differenze e Quando Usare Ciascuno
UUID v4 è stata la scelta predefinita per le chiavi primarie distribuite per oltre un decennio. Genera un valore casuale a 128 bit, formattalo come otto gruppi hex, fatto. Nessun coordinamento necessario, probabilità di collisione praticamente zero, funziona ovunque. Allora perché UUID v7 — standardizzato in RFC 9562 nel 2024 — si sta diffondendo rapidamente nei sistemi in produzione nel 2026?
La risposta breve: prestazioni del database. UUID v4 distrugge la località dell'indice B-tree. UUID v7 risolve questo problema mantenendo tutto il resto che gli sviluppatori amano degli UUID. Questo articolo spiega cosa è effettivamente ogni versione, perché la differenza conta su larga scala, quando scegliere ciascuna, e come migrare senza downtime.
Hai bisogno di generare UUID subito? Prova il generatore UUID di Toova, che supporta sia v4 che v7 in blocco. Per altri identificatori casuali, il generatore di stringhe casuali e il generatore di password coprono i token più brevi.
UUID v4 — Pura Casualità
UUID versione 4 usa un generatore di numeri pseudo-casuali crittograficamente sicuro (CSPRNG) per riempire 122 dei 128 bit. I restanti sei bit sono fissi: quattro bit codificano la versione (0100) e due bit codificano la variante (10). Il risultato appare così:
f47ac10b-58cc-4372-a567-0e02b2c3d479
^^^^
versione = 4 (casuale) La casualità è la caratteristica. Due sistemi indipendenti possono generare UUID senza coordinamento e non collidere mai — la probabilità di collisione in un insieme di 2,71 quadrilioni di UUID v4 è circa il 50%, il che significa che per qualsiasi applicazione pratica il rischio è trascurabile. Non hai bisogno di un server ID centralizzato, di una sequenza del database o di un lock distribuito.
Come viene generato v4
import { v4 as uuidv4 } from 'uuid';
const id = uuidv4();
// => 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
La libreria uuid (JavaScript), uuid.uuid4() di Python, google/uuid di Go, e ogni principale runtime di linguaggio delegano al CSPRNG del sistema operativo — /dev/urandom su Linux, CryptGenRandom su Windows. Il costo di generazione è effettivamente zero.
Il problema: l'inserimento casuale distrugge gli indici B-tree
Un indice B-tree mantiene i dati ordinati. Quando inserisci una nuova riga, il database trova dove si adatta la nuova chiave nell'ordine ordinato e la posiziona lì. Se ogni nuova chiave è casuale, atterra in una posizione casuale nell'indice — il che significa che ogni inserimento deve caricare una pagina diversa dal disco nel pool buffer. A basso volume questo è invisibile. Ad alto volume (milioni di righe, alto tasso INSERT), crea un pattern chiamato frammentazione dell'indice: l'indice si riempie di pagine mezzo vuote perché ogni inserimento colpisce una posizione diversa, e il pool buffer si agita costantemente perché il working set caldo si estende sull'intero indice anziché su una slice recente prevedibile.
I sintomi in produzione: la latenza INSERT aumenta, il sovraccarico di autovacuum aumenta (PostgreSQL), la pressione del checkpoint cresce e le prestazioni di lettura per i record recenti si degradano perché "recente" non corrisponde più a nessuna località nell'indice. Non è ipotetico — è un punto dolente ben documentato su tabelle PostgreSQL e MySQL grandi con chiavi primarie UUID v4.
UUID v7 — Casualità Ordinata per Tempo
UUID v7 è stato progettato esplicitamente per risolvere il problema di frammentazione B-tree. Codifica un timestamp Unix in millisecondi a 48 bit nei bit più significativi, seguito da bit di versione, 12 bit casuali, bit di variante e altri 62 bit casuali. Casualità totale: 74 bit — ancora molto più del necessario per prevenire collisioni.
018f4e6b-a23c-7d45-9abc-0e02b2c3d479
^^^^^^^^^^^^^^
timestamp Unix a 48 bit (precisione ms)
^
versione = 7 Poiché il timestamp occupa i bit alti, gli UUID generati successivamente si ordinano dopo quelli generati in precedenza. Gli inserimenti vengono sempre aggiunti al bordo destro dell'indice. Il database deve solo mantenere la pagina dell'indice più recente calda nel pool buffer, non l'intero indice. Ad alti tassi INSERT, questo da solo può ridurre la latenza di scrittura del 30-60% e ridurre l'I/O di un ordine di grandezza su tabelle con decine di milioni di righe.
Come viene generato v7
import { v7 as uuidv7 } from 'uuid';
const id = uuidv7();
// => '018f4e6b-a23c-7d45-9abc-0e02b2c3d479'
La stessa libreria uuid ha aggiunto il supporto v7 nella versione 10. La libreria standard di Python lo ha aggiunto nella versione 3.14. PostgreSQL 17 include uuidv7() come funzione built-in. Se sei su uno stack più vecchio, diverse piccole librerie forniscono la generazione v7 senza dipendenze.
Monotonicità sub-millisecondo
Cosa succede quando due UUID v7 vengono generati nello stesso millisecondo? RFC 9562 consente alle implementazioni di incrementare un contatore monotonico nei bit casuali per garantire l'ordinamento all'interno dello stesso millisecondo. La libreria uuid fa questo per impostazione predefinita. Il risultato: anche se vengono generati 10.000 ID in un millisecondo, si ordinano comunque correttamente.
Il Problema di Frammentazione dell'Indice B-Tree in Dettaglio
Per capire perché questo è importante, considera cosa succede a 10.000 inserimenti al secondo con una chiave primaria UUID v4 su una tabella con 100 milioni di righe esistenti:
- Ogni inserimento genera una chiave casuale a 128 bit che cade in una posizione casuale tra i 100 milioni di voci esistenti.
- Il database deve caricare la specifica pagina B-tree contenente quella posizione nel pool buffer.
- Con 100 milioni di righe e pagine da 8 KB, l'indice si estende su circa 100.000 pagine. Ogni secondo, sono necessarie 10.000 pagine diverse — molte di più di quelle che un tipico shared_buffers da 8 GB può contenere per questo solo indice.
- Ogni cache miss risulta in una lettura dal disco. A 10.000 inserimenti/sec, questo può generare migliaia di letture casuali dal disco al secondo, saturando anche gli SSD su tabelle grandi.
Con UUID v7, tutti i 10.000 inserimenti al secondo atterrano sulla pagina foglia più a destra (o su un piccolo gruppo di pagine recenti). Il pool buffer deve solo mantenere quelle poche pagine calde. Il tasso di cache hit per le scritture si avvicina al 100%. L'amplificazione della scrittura scende drasticamente.
Lo stesso beneficio si applica alle scansioni di intervallo: WHERE created_at BETWEEN x AND y su una tabella v4 richiede una scansione completa dell'indice o un indice timestamp separato. Su una tabella v7, la chiave primaria stessa è l'indice timestamp — la query può cercare direttamente l'intervallo giusto.
Come Usare UUID v7 in PostgreSQL
-- Funziona nativamente con il tipo UUID in PostgreSQL 16+
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- v4 (casuale)
-- Passa a UUIDv7 tramite estensione o generazione lato applicazione
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Con UUIDv7: il timestamp è già incorporato, quindi
-- questa colonna created_at separata è spesso ridondante.
PostgreSQL 17 include uuidv7() nativamente. Per PostgreSQL 14-16, l'estensione pg_uuidv7 fornisce la stessa funzione. Il tipo di dato UUID memorizza sia v4 che v7 in modo identico — 16 byte, nessun overhead. L'unica differenza è il pattern di bit che determina l'ordinamento.
Una conseguenza utile: poiché il timestamp è incorporato, molte tabelle non hanno più bisogno di una colonna created_at separata per l'ordinamento o la visualizzazione. Puoi estrarre il timestamp da un UUID v7 con una singola chiamata di funzione. Questo riduce la complessità dello schema ed elimina una scrittura per inserimento.
UUID v4 vs v7 — Compromessi
| Proprietà | UUID v4 | UUID v7 |
|---|---|---|
| Bit di casualità | 122 | 74 |
| Ordinamento | Casuale | Cronologico |
| Perf. INSERT B-tree | Scarsa (frammentazione) | Eccellente (sequenziale) |
| Divulgazione timestamp | Nessuna | Sì (precisione ms) |
| Standard RFC | RFC 4122 (2005), RFC 9562 (2024) | RFC 9562 (2024) |
| Supporto librerie | Universale | In crescita rapida (2024-2026) |
| created_at incorporato | No | Sì |
Guida alla Migrazione — da v4 a v7
La migrazione di una tabella esistente dalle chiavi primarie UUID v4 a v7 è un'operazione multi-step. Il vincolo chiave: non puoi cambiare il valore di una chiave primaria che è referenziata da chiavi esterne senza aggiornare anche tutte le tabelle che la referenziano. Pianifica una finestra di manutenzione o usa un approccio di doppia scrittura.
-- 1. Aggiungi la nuova colonna
ALTER TABLE orders ADD COLUMN id_v7 UUID;
-- 2. Backfill delle righe esistenti (usa il created_at originale come
-- sorgente del timestamp; usa una funzione libreria UUIDv7 nell'app)
UPDATE orders SET id_v7 = generate_uuidv7(created_at);
-- 3. Verifica che non rimangano NULL
SELECT COUNT(*) FROM orders WHERE id_v7 IS NULL;
-- 4. Scambia le colonne (richiede una breve finestra di manutenzione)
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); Per le migrazioni a zero downtime su tabelle ad alto traffico, l'approccio raccomandato è:
- Aggiungi la nuova colonna
id_v7con un default server che genera UUID v7 in avanti. - Backfill delle righe vecchie in batch durante i periodi a basso traffico, usando il timestamp
created_atesistente come seme per la porzione di timestamp v7. - Aggiorna tutte le colonne di chiave esterna nelle tabelle che le referenziano per puntare a
id_v7. - Rinomina le colonne e rimuovi il vecchio vincolo di chiave primaria durante una breve finestra di manutenzione.
Il passaggio di backfill è il più lungo. A 10.000 righe per batch con un'attesa di 50 ms tra i batch, una tabella da 100 milioni di righe richiede circa 8 ore. Inizia presto.
Impatto nel Mondo Reale
Diversi team di ingegneria hanno pubblicato benchmark che confrontano UUID v4 e v7 su dataset di dimensioni produttive. I risultati consistenti:
- Throughput INSERT: miglioramento da 2 a 5 volte su tabelle con 50M+ righe quando si passa da v4 a v7, con il guadagno che aumenta con la crescita della tabella.
- Latenza di scrittura p99: scende da centinaia di millisecondi (v4, sotto carico) a singole cifre di millisecondi (v7) sullo stesso hardware.
- Dimensione dell'indice: gli indici B-tree sulle colonne v7 sono del 15-30% più piccoli degli indici v4 equivalenti sugli stessi dati perché la frammentazione lascia meno pagine mezzo vuote.
- Efficienza del pool buffer: il tasso di hit degli shared buffers per l'indice di chiave primaria passa da circa il 40% (v4, tabella grande) a circa il 99% (v7), perché solo le pagine recenti devono rimanere calde.
I guadagni sono trascurabili sotto circa 1 milione di righe. Se la tua tabella rimane piccola, usa v4 per semplicità. Sopra i 10 milioni di righe con qualsiasi tasso INSERT significativo, v7 è il default migliore.
Quando Usare Ciascuno
Usa UUID v7 quando:
- Stai progettando un nuovo schema e la tabella crescerà grande (10M+ righe).
- La tabella ha un alto tasso INSERT — eventi, log, ordini, messaggi, notifiche.
- Vuoi una chiave primaria che funge anche da timestamp di creazione (elimina una colonna).
- Sei su PostgreSQL 17, MySQL 8.0+ o un ORM moderno che supporta la generazione v7.
- Ordinare per ID è semanticamente equivalente a ordinare per tempo di creazione — il che varrà per la maggior parte delle tabelle a prevalenza di append.
Usa UUID v4 quando:
- L'identificatore è esposto agli utenti e non deve rivelare il tempo di creazione (codici di invito, link di condivisione, handle di fatturazione).
- La tabella è piccola e stabile — nessun beneficio prestazionale giustifica il costo di migrazione.
- Stai generando ID in un contesto dove il timestamp di creazione è sensibile.
- Hai bisogno di qualcosa come credenziale monouso o token — usa un generatore di segreti dedicato, non qualsiasi versione UUID.
Riepilogo
UUID v4 è casuale, privato e universalmente supportato. UUID v7 è ordinato per tempo, amichevole con i database, e ora standard nelle principali librerie e database. Per i nuovi schemi con tabelle grandi o in rapida crescita, v7 è il default migliore. Per gli ID esposti agli utenti dove la divulgazione del timestamp è un problema, v4 rimane la scelta giusta.
Genera entrambe le versioni istantaneamente con il generatore UUID di Toova — nessun account richiesto. Per token più brevi, il generatore di stringhe casuali produce ID alfanumerici di qualsiasi lunghezza.