- Backend
- Dati
UUID v4 o v7: quale usare come chiave primaria
La casualità pura del v4 frammenta gli indici B-tree. Il v7 mette un timestamp nei bit alti e restituisce la località negli inserimenti, al prezzo di rivelare quando è stato creato.
Scegliere gli UUID come chiave primaria è una decisione che sembra puramente architetturale e si rivela, mesi dopo, una decisione di prestazioni. La versione 4 — quella che quasi tutti intendono quando dicono «UUID» — ha una proprietà che i database relazionali gradiscono poco: è completamente casuale. La versione 7, standardizzata nel 2024 con RFC 9562, corregge proprio questo, e vale la pena capire cosa cambia davvero.
Com'è fatto un UUID
Un UUID è un identificatore da 128 bit, rappresentato di solito come 32 cifre esadecimali divise in cinque gruppi da trattini. Non tutti i 128 bit sono liberi: alcuni sono riservati a dichiarare quale versione e quale variante sia l'UUID.
85a12264-ce80-4656-a95c-1ad65aed3241
↑ ↑
│ └── variante (i primi bit di questo gruppo)
└─────── versione (qui: 4)La cifra che apre il terzo gruppo indica la versione; i bit più alti del quarto gruppo indicano la variante. Sono in tutto sei bit sottratti allo spazio disponibile: un UUID versione 4 ha quindi 122 bit realmente casuali, non 128.
La versione 4: casualità e basta
Un UUID v4 è 122 bit presi da un generatore casuale crittografico, punto. Nessun timestamp, nessun identificatore di macchina, nessun contatore. È il motivo della sua popolarità: si può generare ovunque — nel client, in un servizio, in un test — senza coordinamento con nessuno e senza rischio pratico di collisione.
| UUID v4 generati | Probabilità di almeno una collisione |
|---|---|
| 1 miliardo (10⁹) | circa 9 × 10⁻²⁰ |
| 1.000 miliardi (10¹²) | circa 9 × 10⁻¹⁴ |
| 1 milione di miliardi (10¹⁵) | circa 9 × 10⁻⁸ |
| 2,3 × 10¹⁸ | 50% |
In termini pratici: la collisione non è il problema di cui preoccuparsi. Il problema è un altro, e non riguarda l'unicità.
Perché la casualità pura fa soffrire il database
Gli indici dei database relazionali sono B-tree, e i B-tree tengono le chiavi in ordine. Quando la chiave primaria è crescente — un intero con autoincremento, per esempio — le righe nuove finiscono tutte nell'ultima pagina dell'indice. Quella pagina è già in memoria, si riempie, se ne apre un'altra, e si va avanti.
Con una chiave casuale ogni inserimento cade in un punto imprevedibile dell'indice. Le conseguenze si sommano:
- Ogni inserimento tocca una pagina diversa, quasi mai già in cache: il database deve leggerla dal disco prima di poterla modificare.
- Le pagine si riempiono a metà e poi si dividono, perché i valori arrivano in ordine sparso. L'indice occupa più spazio di quello che i dati giustificherebbero.
- La parte utile della cache si riduce: invece di lavorare su poche pagine calde, il database ne tocca continuamente di fredde.
- Le righe inserite nello stesso momento finiscono fisicamente lontane, quindi leggere «gli ordini di oggi» significa saltare per tutta la tabella invece di leggere blocchi contigui.
La versione 7: prima il tempo, poi il caso
RFC 9562 introduce un formato che conserva le proprietà utili del v4 e rimuove quella dannosa. I primi 48 bit sono il timestamp Unix in millisecondi; il resto è casuale, con i soliti bit riservati a versione e variante.
01a00f45-6040-7ecf-acc8-989be3043f7f
01a00f45-6041-7c86-943f-4a61a9216ae4
01a00f45-6042-7f2e-a672-9d909e3d0100
└──────┘ └──┘ ↑
│ │ └── versione 7
│ └────── millisecondi (parte bassa): 6040, 6041, 6042
└───────────── timestamp (parte alta), identico nello stesso periodoPoiché il timestamp occupa i bit più significativi, l'ordinamento lessicografico di un UUID v7 coincide con l'ordine cronologico di generazione. Per il B-tree questo significa tornare al comportamento della chiave crescente: inserimenti in coda, pagine calde, nessuna frammentazione da divisioni casuali. I 74 bit casuali che restano continuano a garantire che due UUID generati nello stesso millisecondo su macchine diverse non collidano.
Il confronto
| UUID v4 | UUID v7 | |
|---|---|---|
| Bit casuali | 122 | 74 |
| Ordinabile per tempo | No | Sì |
| Località negli inserimenti | Nessuna | Sequenziale |
| Rivela il momento di creazione | No | Sì, al millisecondo |
| Generabile senza coordinamento | Sì | Sì |
| Supporto nelle librerie | Ovunque | Diffuso ma non universale |
Come memorizzarli
Questa è l'ottimizzazione con il miglior rapporto tra beneficio e sforzo, e viene sbagliata spessissimo. Un UUID sono 16 byte. La sua rappresentazione testuale sono 36 caratteri. Salvarlo come stringa più che raddoppia l'occupazione, e il costo si moltiplica per ogni indice e per ogni chiave esterna che lo referenzia.
| Database | Tipo corretto | Occupazione |
|---|---|---|
| PostgreSQL | uuid | 16 byte |
| MySQL / MariaDB | BINARY(16) | 16 byte |
| SQL Server | uniqueidentifier | 16 byte |
| Qualsiasi, sbagliando | VARCHAR(36) / CHAR(36) | 36+ byte |
Su dieci milioni di righe con tre indici che includono la chiave, la differenza è nell'ordine di centinaia di megabyte in più che devono passare per la cache — e la cache è esattamente la risorsa che stavamo cercando di preservare.
UUID o chiave numerica?
Vale la pena ricordare che l'alternativa esiste. Un intero a 64 bit con autoincremento occupa la metà, è naturalmente ordinato, si legge nei log e nelle URL senza fatica ed è il più veloce di tutti. In cambio è prevedibile — si può enumerare, e da un identificatore si deduce quanti ne esistono — e richiede il database per essere assegnato.
- Sistema singolo, identificatori non esposti pubblicamente: una chiave numerica va benissimo, ed è la scelta più semplice.
- Identificatori generati dal client, o prima che la riga esista, o da servizi diversi che poi convergono: serve un UUID.
- Identificatori esposti in URL pubbliche: UUID, per non rivelare i volumi. E se anche il momento di creazione è sensibile, v4.
- Molti sistemi adottano entrambi: chiave numerica interna per le relazioni, UUID pubblico per l'esterno. Costa una colonna e un indice in più, e disaccoppia le due esigenze in modo pulito.
In sintesi
Per una chiave primaria in un database relazionale, oggi v7 è la scelta predefinita ragionevole: mantiene la generazione distribuita del v4 e restituisce al B-tree la località che il v4 gli toglie. Il v4 resta la scelta giusta quando il momento di creazione non deve essere deducibile, o quando l'identificatore non finisce mai in un indice ordinato — token, chiavi idempotenti, identificatori di correlazione nei log. In entrambi i casi, memorizzateli in 16 byte.
L'autore
Francesco Margiotta Casaluci è backend engineer: progetta e sviluppa microservizi in Java e Spring Boot, pipeline dati e piattaforme cloud-native. Scrive di quello che implementa, e implementa gli strumenti gratuiti pubblicati su questo sito.
Leggi il profilo completo