UUID v7 come chiave primaria in Laravel 12: perché HasUuids ora genera UUID ordinati e cosa cambia per le performance InnoDB
In una piattaforma marketplace con migliaia di utenti attivi, la tabella orders usava UUID v4 come chiave primaria - una scelta fatta tre anni prima per evitare l'esposizione di ID sequenziali nelle API. Con 2.8 milioni di righe, le performance di insert erano degradate del 40% rispetto alle tabelle con ID auto-increment, e il file .ibd di InnoDB occupava quasi il doppio dello spazio atteso. La causa è documentata da Percona: l'inserimento casuale di UUID v4 in un indice B-tree clusterizzato causa page split continui - fino a 500× più frequenti rispetto a chiavi sequenziali - con pagine che si riempiono solo al 50-69% invece del 94%. La RFC 9562, pubblicata nel maggio 2024, definisce UUID v7 come la soluzione: un timestamp a 48 bit (precisione al millisecondo) nei primi byte garantisce l'ordinamento cronologico, eliminando la frammentazione degli indici.
Cosa cambia con HasUuids in Laravel 12 rispetto a Laravel 9/10?
Il trait HasUuids, introdotto in Laravel 9.30, in Laravel 12 genera UUID v7 di default - il trait HasVersion7Uuids, aggiunto in Laravel 11, è stato rimosso e il suo comportamento è stato fuso in HasUuids. Per chi necessita del vecchio comportamento casuale, esiste ora HasVersion4Uuids. La distinzione è cruciale: Str::orderedUuid(), presente da Laravel 5.6, non genera UUID v7 reali - riordina i byte di un UUID v4 con TimestampFirstCombCodec, un approccio non conforme alla RFC.
Il model con UUID v7 in Laravel 12 è minimale:
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
use HasUuids;
/* HasUuids imposta automaticamente:
* - $incrementing = false
* - $keyType = 'string'
* - newUniqueId() genera UUID v7 (RFC 9562)
*/
protected $fillable = ['customer_id', 'total', 'status'];
public function items(): HasMany
{
return $this->hasMany(OrderItem::class);
}
}
/* Migrazione */
Schema::create('orders', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignId('customer_id')->constrained();
$table->decimal('total', 10, 2);
$table->string('status', 20);
$table->timestamps();
});
Schema::create('order_items', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('order_id')->constrained()->cascadeOnDelete();
$table->foreignId('product_id')->constrained();
$table->integer('quantity');
$table->decimal('unit_price', 10, 2);
$table->timestamps();
});foreignUuid() crea la colonna foreign key come CHAR(36) - lo stesso tipo della primary key UUID. HasUuids genera l'UUID v7 automaticamente nella callback creating, sia per la chiave primaria che per le colonne definite in uniqueIds().
Quando conviene migrare da ID auto-increment o UUID v4 a UUID v7?
La migrazione a UUID v7 per tabelle esistenti è un'operazione ad alto rischio che richiede la modifica della chiave primaria e di tutte le foreign key correlate. Conviene in tre scenari specifici:
Il primo è quando le API espongono ID sequenziali. Un endpoint /api/orders/1547 rivela che esistono almeno 1547 ordini e permette attacchi di enumerazione. UUID v7 (/api/orders/019480f4-8e2a-7b3c-...) mantiene l'ordinamento interno ma non espone la cardinalità - un requisito che le API REST di qualsiasi applicativo dovrebbero soddisfare.
Il secondo è quando le performance di insert degradano su tabelle grandi con UUID v4. Se SHOW ENGINE INNODB STATUS mostra un alto numero di page split nella sezione INSERT BUFFER AND ADAPTIVE HASH INDEX, UUID v7 risolve il problema alla radice - le insert tornano sequenziali come con auto-increment, con page fill al 94%.
Il terzo è in architetture distribuite dove più sistemi generano record per la stessa tabella. UUID v7 garantisce unicità globale senza coordinamento tra nodi - a differenza di auto-increment che richiede sequenze distribuite o offset.
Per tabelle piccole (<100K righe) con ID auto-increment e nessuna esposizione nelle API, la migrazione non porta benefici misurabili - l'overhead di CHAR(36) vs BIGINT(8 byte) sugli indici secondari è un costo senza ritorno.
Errori comuni nella migrazione a UUID
Il primo errore è usare CHAR(36) senza valutare BINARY(16). Un UUID come stringa occupa 36 byte; come binario, 16 byte. Su una tabella con 10 milioni di righe e 5 indici secondari (ognuno include la primary key in InnoDB), la differenza è significativa. Percona documenta che una tabella da 1 miliardo di righe con UUID CHAR(36) occupava ~1TB; riorganizzata con formato binario, è scesa a ~450GB. Laravel usa CHAR(36) di default con $table->uuid() - per sistemi ad alto volume, la conversione a BINARY(16) con cast custom nel model è un'ottimizzazione necessaria.
Il secondo è confondere UUID v7 con ULID. Entrambi hanno un timestamp a 48 bit, ma ULID usa codifica base32 (26 caratteri) e non è conforme alla RFC UUID. In Laravel, HasUlids genera ULID - un formato più compatto e URL-friendly ma incompatibile con colonne UUID in PostgreSQL e con librerie che validano il formato UUID. Per nuovi progetti, UUID v7 è la scelta standard.
Il terzo è migrare la primary key in produzione senza piano di rollback. La sequenza corretta: aggiungere colonna UUID v7 nullable → popolare con script batch → aggiornare le foreign key → switch della primary key. Ogni step deve essere reversibile. I test automatici delle relazioni Eloquent devono passare sia con la vecchia che con la nuova chiave durante la transizione.
La scelta della chiave primaria determina le performance di ogni query che tocca quella tabella - è una decisione che costa quasi nulla se presa all'inizio del progetto e può costare settimane di migrazione se rimandata. L'ottimizzazione delle query Eloquent include l'analisi degli indici e dei page split come primo step diagnostico. Per conoscere il mio approccio alla progettazione di database per applicativi Laravel, visita la mia pagina professionale. Se le performance del tuo database degradano con la crescita e sospetti che la strategia di chiavi primarie sia la causa, contattami per una consulenza dedicata - partiamo dall'analisi degli indici InnoDB e dalla valutazione dell'impatto della migrazione.