Cos’è un ULID? L’identificatore ordinabile, spiegato
Ogni UUIDv4 casuale che inserisci come chiave primaria finisce in un punto imprevedibile dell’indice del database. Fallo qualche milione di volte e l’indice si frammenta, la cache si satura e le scritture rallentano. Un ULID risolve il problema senza rinunciare a ciò che ti piaceva degli UUID: puoi sempre generarne uno ovunque, senza coordinatore centrale, ma finisce in ordine temporale invece di disperdersi.
Allora come fa una stringa di 26 caratteri a ordinarsi da sola per tempo? È tutto qui il trucco, e conviene capirlo prima di metterne uno in produzione.
Un ULID (Universally Unique Lexicographically Sortable Identifier) è un identificatore a 128 bit scritto come 26 caratteri Crockford Base32. I primi 10 caratteri codificano un timestamp in millisecondi e gli ultimi 16 codificano bit casuali, così gli ULID creati dopo si ordinano sempre dopo quelli precedenti quando li confronti come semplici stringhe. È un identificatore univoco ordinabile che puoi generare offline.
Questa guida lo scompone pezzo per pezzo: l’anatomia decodificata carattere per carattere, la dimostrazione che si ordina davvero, la matematica del B-tree dietro il vantaggio per il database e uno sguardo onesto su cosa rivela il timestamp incorporato. Mentre leggi puoi seguire con un valore reale nel generatore di ULID: generane uno, decodificalo, convertilo in UUID.
Cos’è un ULID?
Un ULID (Universally Unique Lexicographically Sortable Identifier) è un identificatore a 128 bit progettato come alternativa più ordinabile e più compatta a un UUID. Si scrive come 26 caratteri Crockford Base32: i primi 10 contengono un timestamp a 48 bit in millisecondi a partire dall’epoca Unix, e i restanti 16 contengono 80 bit di casualità. Poiché il tempo viene per primo, la stringa si ordina cronologicamente.
Quest’ultima proprietà è la ragione per cui il formato esiste. UUIDv4 è completamente casuale, il che è ottimo per l’unicità ma significa che due ID creati a un secondo di distanza non hanno alcuna relazione tra loro. Gli ULID mantengono il modello senza coordinamento, generabile ovunque, e aggiungono l’ordinamento temporale, così una colonna di ULID risulta naturalmente ordinata per data di creazione senza nulla in più.
Ecco il formato a colpo d’occhio:
| Proprietà | Valore |
|---|---|
| Bit | 128 |
| Codifica | 26 caratteri Crockford Base32 |
| Struttura | timestamp a 48 bit + casualità a 80 bit |
Il resto dell’articolo spiega come funziona ciascun pezzo. La codifica e l’ordinabilità meritano sezioni a sé; cominciamo dalla struttura, poi passiamo a Base32 e alla dimostrazione dell’ordinamento.
Anatomia di un ULID: 48 bit di tempo + 80 bit di casualità
I 26 caratteri di un ULID si dividono nettamente in due metà. I primi 10 caratteri sono il timestamp; gli ultimi 16 sono la parte casuale. Disponi l’esempio canonico e il confine è evidente:
01ARYZ6S41 TSV4RRFFQ69G5FAV
└────────┘ └──────────────┘
10 chars 16 chars
48-bit ms 80-bit random
timestamp
Due componenti, due compiti. Uno registra il quando; l’altro garantisce l’unicità. Decodifichiamoli entrambi.
Il timestamp a 48 bit (primi 10 caratteri)
I 10 caratteri iniziali codificano un intero a 48 bit: il numero di millisecondi a partire dall’epoca Unix nel momento in cui l’ULID è stato creato. Prendi l’esempio canonico direttamente dalla specifica:
01ARYZ6S41 -> 1469918176385 ms -> 2016-07-30T22:36:16.385Z
È una decodifica reale e reversibile — incolla 01ARYZ6S41TSV4RRFFQ69G5FAV in un decodificatore e ottieni esattamente 2016-07-30T22:36:16.385Z. Il componente temporale è dato puro, non un hash, quindi leggerlo non costa nulla.
Un piccolo dettaglio che manda fuori strada: il primo carattere di un ULID è sempre compreso tra 0 e 7. Un carattere Crockford contiene 5 bit, e 48 bit non sono un multiplo di 5: il timestamp occupa i 48 bit bassi dei 50 che 10 caratteri possono trasportare, lasciando i 2 bit alti del primo carattere permanentemente a zero. Due bit a zero limitano il valore di quel carattere a 7. Se ti capita di vedere un ULID che inizia con 8 o più, è malformato.
Gli 80 bit di casualità (ultimi 16 caratteri)
I restanti 16 caratteri trasportano 80 bit di casualità, ed è da questa metà che arriva l’unicità. I bit dovrebbero provenire da una sorgente crittograficamente sicura — crypto.getRandomValues nel browser, non Math.random. La differenza conta: Math.random è abbastanza prevedibile da permettere a un attaccante di indovinare o far collidere i valori, mentre un CSPRNG no.
Quanto spazio sono 80 bit? Circa 1.2 × 10²⁴ valori possibili, e questo per millisecondo. Anche se generi milioni di ULID nello stesso millisecondo, la probabilità che due estraggano gli stessi 80 bit resta infinitesimale. A differenza del timestamp, questa metà non porta alcun significato decodificabile: è rumore il cui unico scopo è rendere distinto ogni ULID.
Crockford Base32: perché gli ULID eliminano I, L, O e U
Gli ULID sono codificati con Crockford Base32, un alfabeto di 32 simboli: le cifre 0–9 e le lettere A–Z con quattro rimosse.
0123456789ABCDEFGHJKMNPQRSTVWXYZ
Le lettere mancanti sono I, L, O e U. Tre sono eliminate perché somigliano a cifre (I e L assomigliano a 1, O assomiglia a 0), così una persona che legge un ULID da uno schermo non può confondere una lettera per un numero. Il rovescio della medaglia è un input tollerante: un decodificatore conforme rimappa I e L su 1 e O su 0, e tratta l’intera stringa senza distinzione tra maiuscole e minuscole. U è esclusa a parte, per evitare di comporre per caso parole offensive.
La matematica dei bit è l’altra ragione. Ogni carattere Base32 codifica 5 bit, dove un carattere esadecimale ne codifica solo 4. Impacchetta 128 bit a 5 bit per carattere e te ne servono 26; impacchetta gli stessi 128 bit a 4 bit ciascuno (come fa un UUID) e te ne servono 32, più quattro trattini, per un totale di 36 caratteri. Quindi un ULID è sensibilmente più corto di un UUID e, senza trattini, entra direttamente in un URL, in un nome di file o in un header senza escaping.
Crockford Base32 è un alfabeto di 32 simboli (0–9 e A–Z meno I, L, O, U) che codifica 5 bit per carattere. Gli ULID lo usano per impacchettare 128 bit in 26 caratteri URL-safe e indipendenti da maiuscole/minuscole. Il dettaglio che conta davvero è che l’alfabeto è in ordine crescente, ed è questo che permette alla stringa codificata di ordinarsi nello stesso modo dei bit grezzi.
Perché gli ULID si ordinano per tempo
Molti articoli ti dicono che gli ULID si ordinano per tempo. Pochi mostrano perché. Il motivo si regge su due fatti che hai già: il timestamp è la parte più significativa del valore, e l’alfabeto di Crockford è disposto in ordine crescente.
Mettili insieme e ottieni una catena di equivalenze:
string compare == 128-bit integer compare == creation-time compare
Leggila da sinistra a destra. Confrontare due ULID carattere per carattere (il modo in cui funziona un ordinamento di stringhe) dà la stessa risposta del confronto dei loro interi a 128 bit sottostanti, perché l’alfabeto preserva l’ordine — un carattere “più alto” significa sempre un valore più alto. Confrontare gli interi a 128 bit dà la stessa risposta del confronto delle date di creazione, perché il timestamp sta nei bit più significativi, quindi domina il confronto; la coda casuale risolve solo i pareggi all’interno dello stesso millisecondo. Ordine delle stringhe, ordine dei bit e ordine temporale sono lo stesso ordine.
Una rapida dimostrazione. Due ULID generati a un millisecondo di distanza:
01ARYZ6S41... (created at T)
01ARYZ6S42... (created at T + 1 ms)
Il decimo carattere scatta da 1 a 2, e un semplice ordinamento testuale mette il secondo dopo il primo: nessuna colonna timestamp, nessun comparatore speciale. Il vantaggio pratico, che la prossima sezione approfondisce, sta in una riga: ORDER BY id restituisce le righe in ordine cronologico senza alcun indice aggiuntivo.
Gli ULID come chiavi primarie di database: località nel B-tree
È qui che gli ULID si guadagnano la pagnotta. La maggior parte dei database relazionali memorizza l’indice della chiave primaria come un B-tree, e il punto in cui una nuova chiave finisce in quell’albero decide quanto costa l’inserimento.
Un UUIDv4 casuale finisce in un punto imprevedibile a ogni inserimento:
UUIDv4: ogni nuova chiave punta a una pagina foglia casuale. Quella pagina è spesso piena, quindi il motore la divide, copia metà delle righe altrove e sporca pagine in tutto l’albero. Su milioni di righe questo frammenta l’indice, espelle pagine utili dalla buffer cache e abbatte il throughput di inserimento. (Per i numeri precisi sugli split di pagina dell’indice — tipicamente una differenza di 2–10× su tabelle con molte scritture — vedi la guida di confronto.)
Un ULID con prefisso temporale finisce ogni volta in fondo:
ULID: poiché i bit alti sono un timestamp, ogni nuova chiave è maggiore della precedente, quindi si aggiunge in coda o vicino al margine destro dell’indice. Gli inserimenti restano sequenziali, gli split di pagina quasi spariscono, l’indice resta compatto e una scansione su un intervallo temporale legge una sequenza contigua di pagine.
Ottieni la generazione senza coordinamento di un UUID con la località di inserimento di un intero auto-incrementale — senza esporre un contatore sequenziale indovinabile, dato che la coda casuale nasconde comunque il valore successivo esatto.
Consiglio di archiviazione: memorizza i 128 bit come 16 byte binari (una colonna uuid in PostgreSQL, BINARY(16) in MySQL), non come un campo di testo di 26 caratteri, che spreca spazio e gonfia l’indice. Codifica nella stringa Base32 solo ai margini dove la vede una persona o un URL. La scheda Convert del generatore permette di convertire un ULID in UUID proprio per questo, dato che le due forme sono gli stessi 128 bit.
ULID monotòni: ordine stretto all’interno di un millisecondo
La dimostrazione dell’ordinabilità ha una lacuna che è onesto ammettere: all’interno di un singolo millisecondo, gli ULID semplici non sono strettamente ordinati. Condividono lo stesso prefisso temporale di 10 caratteri, ma le loro code casuali a 80 bit sono estratte indipendentemente, quindi quale di due ULID dello stesso millisecondo si ordini prima è essenzialmente un lancio di moneta. Per la maggior parte degli usi va bene. Quando serve un ordine stretto anche a frequenze sub-millisecondo, no.
La generazione monotòna colma la lacuna. La regola è semplice: il primo ULID di un dato millisecondo riceve nuova casualità come al solito, e ogni ULID successivo nello stesso millisecondo si produce prendendo il precedente valore casuale a 80 bit e incrementandolo di uno (trattato come intero big-endian, con riporto sui bit più alti se necessario). Ogni valore è quindi strettamente maggiore di quello che lo precede.
Lo si vede in un batch generato dentro un solo millisecondo — si muove solo il carattere finale:
01KVT0F720ZK9N4T2QX7VR8WMC
01KVT0F720ZK9N4T2QX7VR8WMD
01KVT0F720ZK9N4T2QX7VR8WME
…WMC < …WMD < …WME, garantito. Questo conta ogni volta che le righe possono essere create più velocemente di quanto scatti l’orologio in millisecondi: inserimenti ad alto throughput, log di eventi, ID di messaggi in un ciclo serrato. Quando l’orologio avanza al millisecondo successivo, la generazione torna a una nuova casualità e il ciclo si ripete.
ULID vs UUID: quando usare cosa
La domanda che porta qui la maggior parte delle persone è ULID vs UUID. Vediamo un confronto mirato: ULID contro le due versioni di UUID che realisticamente considereresti come alternative. (Per la matrice decisionale completa a cinque vie, comprese Snowflake e NanoID, vedi il confronto completo tra ULID, UUID e Snowflake.)
| Proprietà | ULID | UUIDv4 | UUIDv7 |
|---|---|---|---|
| Lunghezza | 26 caratteri | 36 caratteri | 36 caratteri |
| Codifica | Crockford Base32 | esadecimale con trattini | esadecimale con trattini |
| Ordinabile per tempo? | Sì | No | Sì |
| Incorpora il timestamp? | Sì (48-bit ms) | No | Sì (48-bit ms) |
| Standardizzato? | Specifica comunitaria | RFC 9562 | RFC 9562 |
| Ideale per | ID corti e ordinabili | ID casuali opachi | ID ordinabili in formato UUID |
In prosa: scegli un ULID quando vuoi la stringa più corta, URL-safe e ordinabile. Scegli UUIDv4 quando vuoi un identificatore opaco e completamente casuale senza tempo incorporato — per esempio un token pubblico per cui preferiresti non rivelare quando è stato creato. Scegli UUIDv7 quando ti serve l’ordinamento temporale ma devi restare dentro il formato UUID standard, con i bit di versione e variante nelle loro posizioni fisse e una colonna uuid nativa in cui inserirlo.
Tutti e tre sono a 128 bit, quindi la conversione ULID ↔ UUID è senza perdita in entrambe le direzioni. La relazione tra ULID e ulid vs uuid v7 è più stretta di quanto sembri: UUIDv7 è essenzialmente la versione standardizzata dall’IETF della stessa idea a prefisso temporale che ULID ha aperto la strada. Se sei nuovo agli UUID in generale, parti prima dalle basi, poi torna a questo confronto.
Il compromesso sulla privacy: gli ULID rivelano la loro data di creazione
Il timestamp incorporato è una funzionalità e una fuga di informazioni, a seconda di chi legge l’ID. Chiunque possegga un ULID può decodificare il timestamp in un passaggio e scoprire il millisecondo esatto in cui il record è stato creato — senza alcun accesso al tuo database.
Dentro i tuoi sistemi è tutto vantaggio: auditing istantaneo, ordinamento gratuito, debug facile. Su un identificatore esposto pubblicamente è una vera divulgazione. La data di creazione può essere di per sé sensibile per il business, e un pugno di ULID campionati nel tempo rivela il tuo tasso di creazione, cioè quanti ordini, account o messaggi generi al secondo: il genere di cosa che concorrenti e scraper amano stimare.
Per onestà, è una fuga più ristretta di UUIDv1, che storicamente incorporava l’indirizzo MAC della macchina generatrice; un ULID espone solo il tempo, mai l’identità dell’hardware. Comunque, valutala. La mitigazione è semplice: tieni gli ULID interni e distribuisci un UUIDv4 completamente casuale per gli ID esposti pubblicamente dove l’ordinamento non conta.
Errori comuni con gli ULID
La maggior parte dei problemi con gli ULID nasce da un pugno di decisioni ingegneristiche evitabili, non da bug nel formato. Quelli ricorrenti:
- Dare per scontato che gli ULID semplici dello stesso millisecondo siano ordinati. Condividono un prefisso temporale ma hanno code casuali indipendenti, quindi il loro ordine è indefinito. Soluzione: usa la modalità monotòna quando ti serve un ordinamento stretto a frequenze sub-millisecondo.
- Memorizzare un ULID come testo di 26 caratteri. Spreca spazio e gonfia l’indice. Soluzione: memorizza i 128 bit come 16 byte (
uuid/BINARY(16)) e codifica in Base32 solo ai margini. - Aspettarsi che una conversione ULID→UUID risulti come v4 o v7. La conversione ricodifica gli stessi bit; non imposta i campi di versione e variante dell’UUID, quindi una libreria che li ispeziona non vedrà una versione marcata. Soluzione: tratta il risultato come un valore opaco a 128 bit, oppure genera un vero UUIDv7 quando ti serve il marcatore.
- Riempire la casualità con
Math.random. È prevedibile e può collidere. Soluzione: usa sempre un CSPRNG comecrypto.getRandomValues. - Esporre gli ULID pubblicamente senza valutare la fuga del timestamp. Vedi la sezione sulla privacy qui sopra. Soluzione: ULID interni, UUIDv4 casuale per gli ID pubblici.
- Digitare a mano
I,L,OoUin un ULID. Quelle lettere non sono nell’alfabeto, e ridigitare invita agli errori. Soluzione: copia gli ULID, non riscriverli.
FAQ
ULID è uno standard ufficiale come UUID?
No. ULID è una specifica comunitaria pubblicata su GitHub, non un RFC dell’IETF. È ampiamente implementato e stabile, ma non ha alle spalle alcun organismo di standardizzazione. Se ti serve un identificatore standardizzato e ordinato per tempo, UUIDv7 (RFC 9562) applica la stessa idea dentro il formato UUID ufficiale.
Quanti caratteri ha un ULID, e perché è più corto di un UUID?
26 caratteri, contro i 36 di un UUID. ULID usa Crockford Base32, che impacchetta 5 bit per carattere; l’esadecimale di un UUID ne impacchetta solo 4 e aggiunge quattro trattini. Gli stessi 128 bit richiedono quindi meno caratteri in Base32 — e nessuno di essi ha bisogno di escaping per gli URL.
Due ULID possono mai collidere?
Praticamente mai. In un millisecondo un ULID ha 80 bit casuali — circa 1.2 × 10²⁴ possibilità — quindi anche generandone milioni al millisecondo la probabilità di collisione resta infinitesimale. L’unico requisito è che un RNG crittograficamente sicuro riempia la casualità; Math.random annulla la garanzia.
Posso memorizzare gli ULID in PostgreSQL o MySQL?
Sì. Un ULID è a 128 bit, quindi convertilo in forma UUID e memorizzalo in una colonna uuid (PostgreSQL) o BINARY(16) (MySQL), poi genera la stringa Base32 solo ai margini. Non esiste un tipo di colonna ULID nativo, ma la rappresentazione UUID costa gli stessi 16 byte e mantiene l’indice compatto.
Gli ULID distinguono tra maiuscole e minuscole?
La forma canonica è in maiuscolo, ma Crockford Base32 è indifferente a maiuscole/minuscole in input: un decodificatore legge allo stesso modo le lettere minuscole, e mappa I/L su 1 e O su 0. Per evitare sorprese nei confronti di uguaglianza e negli indici, normalizza a un unico caso prima di memorizzare o confrontare.
Il timestamp a 48 bit si esaurirà mai?
Non per molto, molto tempo. 48 bit di millisecondi raggiungono l’anno 10889 prima che il contatore vada in overflow, quindi il componente timestamp è di fatto a prova di futuro per qualsiasi applicazione reale. Sostituirai il sistema, il linguaggio e il database molto prima che il formato esaurisca lo spazio.
Posso generare ULID nel browser o su mobile senza un server?
Sì — è uno dei vantaggi principali. Gli ULID non hanno bisogno di alcun coordinatore centrale, quindi qualsiasi nodo, edge worker, browser o dispositivo può generarne uno dal proprio orologio più un RNG sicuro. I valori creati su macchine diverse si ordinano comunque insieme per tempo, perché il timestamp vive nell’ID stesso.
Conclusione
Gli ULID risolvono un problema specifico e reale (chiavi casuali che frammentano l’indice) senza togliere la generazione decentralizzata. I meccanismi da tenere a mente:
- Un ULID è un timestamp in millisecondi a 48 bit + 80 bit di casualità, codificato come 26 caratteri Crockford Base32.
- Si ordina per tempo perché il timestamp è il componente più significativo e l’alfabeto preserva l’ordine — l’ordine delle stringhe è uguale all’ordine temporale.
- Quell’ordinamento dà a un B-tree la località di inserimento che a un UUIDv4 casuale manca, mantenendo le scritture veloci e l’indice compatto.
- Usa la modalità monotòna quando ti serve un ordinamento stretto per ID generati nello stesso millisecondo.
- Valuta la fuga del timestamp prima di esporre gli ULID su identificatori rivolti al pubblico.
- Scegli invece UUIDv7 quando devi restare dentro il formato UUID standard.
Quando sei pronto a metterlo in pratica, apri il generatore di ULID per generare, decodificare e convertire ULID interamente nel tuo browser — niente server, niente upload, niente di memorizzato.