francescomargiotta.com
  • 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.

di Francesco Margiotta Casaluci6 min di lettura

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 generatiProbabilità 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%
Approssimazione del compleanno su uno spazio di 2¹²² valori.

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 periodo
Tre UUID v7 generati a un millisecondo di distanza. Il prefisso comune è quello che rende l'inserimento sequenziale.

Poiché 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 v4UUID v7
Bit casuali12274
Ordinabile per tempoNo
Località negli inserimentiNessunaSequenziale
Rivela il momento di creazioneNoSì, al millisecondo
Generabile senza coordinamento
Supporto nelle librerieOvunqueDiffuso 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.

DatabaseTipo correttoOccupazione
PostgreSQLuuid16 byte
MySQL / MariaDBBINARY(16)16 byte
SQL Serveruniqueidentifier16 byte
Qualsiasi, sbagliandoVARCHAR(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

Articoli correlati