Skip to content
Terug naar blog
Tutorials

Wat is een ULID? Gids voor de sorteerbare identifier

Wat is een ULID? Hoe de sorteerbare 128-bits identifier werkt: de structuur van tijdstempel plus willekeur, Crockford Base32-codering en wanneer je hem boven een UUID kiest.

12 min leestijd

Wat is een ULID? De sorteerbare unieke identifier, uitgelegd

Elke willekeurige UUIDv4 die je als primaire sleutel invoegt, belandt op een onvoorspelbare plek in de database-index. Doe dat een paar miljoen keer en de index raakt gefragmenteerd, de cache loopt vast en schrijfacties worden trager. Een ULID lost dat op zonder op te geven wat je in UUID’s waardeerde: je kunt er nog steeds overal eentje aanmaken, zonder centrale coördinatie, maar hij belandt op tijdvolgorde in plaats van overal verspreid.

Hoe sorteert een string van 26 tekens zichzelf dan op tijd? Dat is de hele truc, en het loont om hem te begrijpen voordat je naar een ULID grijpt.

Een ULID (Universally Unique Lexicographically Sortable Identifier) is een 128-bits identifier geschreven als 26 Crockford Base32-tekens. De eerste 10 tekens coderen een tijdstempel in milliseconden en de laatste 16 coderen willekeurige bits, waardoor ULID’s die later zijn aangemaakt altijd ná eerdere worden gesorteerd wanneer je ze als gewone strings vergelijkt. Het is een sorteerbare unieke identifier die je offline kunt genereren.

Deze gids haalt dat uit elkaar: de anatomie teken voor teken ontleed, het bewijs dat hij echt sorteert, de B-tree-rekensom achter het databasevoordeel en een eerlijke blik op wat de ingebedde tijdstempel prijsgeeft. Je kunt het volgen met een echte waarde in de ULID-generator: genereer er een, decodeer hem, zet hem om naar een UUID terwijl je leest.

Wat is een ULID?

Een ULID (Universally Unique Lexicographically Sortable Identifier) is een 128-bits identifier die is ontworpen als een beter sorteerbaar, compacter alternatief voor een UUID. Hij wordt geschreven als 26 tekens Crockford Base32: de eerste 10 bevatten een 48-bits tijdstempel in milliseconden sinds de Unix-epoch, en de overige 16 bevatten 80 bits willekeur. Omdat de tijd vooraan staat, sorteert de string chronologisch.

Die laatste eigenschap is de reden dat het formaat bestaat. UUIDv4 is volledig willekeurig, wat geweldig is voor uniciteit maar betekent dat twee ID’s die een seconde na elkaar zijn aangemaakt geen onderling verband hebben. ULID’s behouden het coördinatievrije, overal-genereren-model en voegen daar tijdvolgorde aan toe, zodat een kolom met ULID’s vanzelf op aanmaaktijd is gesorteerd, zonder iets extra’s.

Hier is het formaat in één oogopslag:

EigenschapWaarde
Bits128
Codering26 Crockford Base32-tekens
Indeling48-bits tijdstempel + 80-bits willekeur

De rest van dit artikel vult in hoe elk onderdeel werkt. De codering en de sorteerbaarheid verdienen hun eigen secties, dus we komen zo bij Base32 en het sorteerbewijs. Eerst de indeling.

Anatomie van een ULID: 48 bits tijd + 80 bits willekeur

De 26 tekens van een ULID splitsen netjes in twee helften. De eerste 10 tekens zijn de tijdstempel; de laatste 16 zijn het willekeurige deel. Leg het canonieke voorbeeld uit en de grens is duidelijk:

01ARYZ6S41   TSV4RRFFQ69G5FAV
└────────┘   └──────────────┘
 10 chars        16 chars
48-bit ms      80-bit random
timestamp

Twee componenten, twee taken. De ene legt wanneer vast; de andere garandeert uniciteit. Laten we ze allebei decoderen.

De 48-bits tijdstempel (eerste 10 tekens)

De eerste 10 tekens coderen een 48-bits geheel getal: het aantal milliseconden sinds de Unix-epoch op het moment dat de ULID werd aangemaakt. Neem het canonieke voorbeeld rechtstreeks uit de specificatie:

01ARYZ6S41  ->  1469918176385 ms  ->  2016-07-30T22:36:16.385Z

Dat is een echte, omkeerbare decode: plak 01ARYZ6S41TSV4RRFFQ69G5FAV in een decoder en je krijgt precies 2016-07-30T22:36:16.385Z terug. De tijdcomponent is gewone data, geen hash, dus hem uitlezen kost niets.

Eén klein detail waar mensen over struikelen: het eerste teken van een ULID ligt altijd tussen 0 en 7. Een Crockford-teken bevat 5 bits, en 48 bits is geen veelvoud van 5. De tijdstempel beslaat de onderste 48 van de 50 bits die 10 tekens kunnen dragen, waardoor de bovenste 2 bits van het eerste teken permanent nul zijn. Twee nul-bits maximeren de waarde van dat teken op 7. Zie je ooit een ULID die met 8 of hoger begint, dan is hij ongeldig.

De 80 bits willekeur (laatste 16 tekens)

De overige 16 tekens dragen 80 bits willekeur, en in deze helft zit de uniciteit. De bits horen uit een cryptografisch veilige bron te komen: crypto.getRandomValues in de browser, niet Math.random. Het verschil telt: Math.random is voorspelbaar genoeg dat een aanvaller waarden zou kunnen raden of laten botsen, een CSPRNG niet.

Hoeveel ruimte is 80 bits? Ongeveer 1,2 × 10²⁴ mogelijke waarden, en dat is per milliseconde. Zelfs als je miljoenen ULID’s binnen één milliseconde aanmaakt, blijft de kans dat twee dezelfde 80 bits trekken verwaarloosbaar klein. Anders dan de tijdstempel draagt deze helft geen decodeerbare betekenis; het is ruis met als enige doel elke ULID uniek te maken.

Crockfords Base32: waarom ULID’s I, L, O en U weglaten

ULID’s worden gecodeerd met Crockfords Base32, een alfabet van 32 symbolen: de cijfers 09 en de letters AZ met er vier verwijderd.

0123456789ABCDEFGHJKMNPQRSTVWXYZ

De ontbrekende letters zijn I, L, O en U. Drie zijn weggelaten omdat ze op cijfers lijken — I en L lijken op 1, O lijkt op 0 — zodat iemand die een ULID van een scherm afleest een letter niet voor een cijfer kan aanzien. De keerzijde is vergevingsgezinde invoer: een conforme decoder zet I en L terug naar 1 en O naar 0, en behandelt de hele string hoofdletterongevoelig. U is apart uitgesloten, om te voorkomen dat er per ongeluk aanstootgevende woorden ontstaan.

De bit-rekensom is de andere reden. Elk Base32-teken codeert 5 bits, waar een hexadecimaal teken er maar 4 codeert. Pak 128 bits in op 5 bits per teken en je hebt er 26 nodig; pak dezelfde 128 bits in op 4 bits per stuk — zoals een UUID doet — en je hebt er 32 nodig, plus vier koppeltekens, dus 36 tekens. Een ULID is dus merkbaar korter dan een UUID en, zonder koppeltekens, past hij zo in een URL, een bestandsnaam of een header zonder dat je iets hoeft te escapen.

Crockfords Base32 is een alfabet van 32 symbolen (09 en AZ min I, L, O, U) dat 5 bits per teken codeert. ULID’s gebruiken het om 128 bits in te pakken in 26 hoofdletterongevoelige, URL-veilige tekens. Cruciaal is dat het alfabet in oplopende volgorde staat, en juist dat zorgt ervoor dat de gecodeerde string op dezelfde manier sorteert als de ruwe bits.

Waarom ULID’s op tijd sorteren

Veel artikelen vertellen je dát ULID’s op tijd sorteren. Minder laten zien waarom, dus hier is het werkelijke argument. Het rust op twee feiten die je al hebt: de tijdstempel is het meest significante deel van de waarde, en Crockfords alfabet staat in oplopende volgorde.

Zet die naast elkaar en je krijgt een keten van gelijkwaardigheden:

string compare  ==  128-bit integer compare  ==  creation-time compare

Lees hem van links naar rechts. Twee ULID’s teken voor teken vergelijken (zoals een string-sortering werkt) geeft hetzelfde antwoord als hun onderliggende 128-bits gehele getallen vergelijken, omdat het alfabet de volgorde behoudt — een “hoger” teken betekent altijd een hogere waarde. De 128-bits gehele getallen vergelijken geeft hetzelfde antwoord als aanmaaktijden vergelijken, omdat de tijdstempel in de meest significante bits zit en dus de vergelijking domineert; de willekeurige staart verbreekt alleen gelijke standen binnen dezelfde milliseconde. Stringvolgorde, bitvolgorde en tijdvolgorde zijn dezelfde volgorde.

Een snelle demonstratie. Twee ULID’s die één milliseconde na elkaar zijn aangemaakt:

01ARYZ6S41...   (created at T)
01ARYZ6S42...   (created at T + 1 ms)

Het tiende teken springt van 1 naar 2, en een gewone tekstsortering zet de tweede ná de eerste, zonder tijdstempelkolom of speciale comparator. De praktische opbrengst, die de volgende sectie uitwerkt, is één regel: ORDER BY id geeft rijen in chronologische volgorde terug zonder extra index.

ULID’s als primaire sleutels in databases: B-tree-lokaliteit

Hier verdienen ULID’s hun plek. De meeste relationele databases slaan een primaire-sleutelindex op als een B-tree, en waar een nieuwe sleutel in die boom belandt bepaalt hoe duur de insert is.

Een willekeurige UUIDv4 belandt bij elke insert ergens onvoorspelbaars:

UUIDv4: elke nieuwe sleutel richt zich op een willekeurige bladpagina. Die pagina is vaak vol, dus de engine splitst hem, kopieert de helft van de rijen ergens anders heen en vervuilt pagina’s overal in de boom. Over miljoenen rijen fragmenteert dit de index, verdrijft het nuttige pagina’s uit de buffercache en remt het de insert-doorvoer af. (Voor de harde cijfers over indexpaginasplitsingen — doorgaans een verschil van 2–10× op schrijfintensieve tabellen — zie de vergelijkingsgids.)

Een ULID met tijdprefix belandt elke keer aan het eind:

ULID: omdat de hoge bits een tijdstempel zijn, is elke nieuwe sleutel groter dan de vorige, zodat hij wordt toegevoegd aan of vlak bij de rechterrand van de index. Inserts blijven sequentieel, paginasplitsingen verdwijnen nagenoeg, de index blijft compact, en een range scan over een tijdvenster leest een aaneengesloten reeks pagina’s.

Je krijgt de coördinatievrije generatie van een UUID met de insert-lokaliteit van een auto-increment integer, zonder een raadbare oplopende teller bloot te geven, want de willekeurige staart verbergt nog steeds de exacte volgende waarde.

Opslagtip: sla de 128 bits op als 16 binaire bytes (een uuid-kolom in PostgreSQL, BINARY(16) in MySQL), niet als een tekstveld van 26 tekens, want dat verspilt ruimte en doet de index opzwellen. Codeer pas naar de Base32-string aan de randen waar een mens of een URL hem ziet. Het tabblad Convert van de generator zal een ULID omzetten naar een UUID precies hiervoor, aangezien de twee vormen dezelfde 128 bits zijn.

Monotone ULID’s: strikte volgorde binnen een milliseconde

Het sorteerbewijs heeft één eerlijk gat: binnen een enkele milliseconde zijn gewone ULID’s niet strikt geordend. Ze delen hetzelfde tijdprefix van 10 tekens, maar hun willekeurige staarten van 80 bits worden onafhankelijk getrokken, dus welke van twee ULID’s uit dezelfde milliseconde eerst sorteert is in wezen kop of munt. Voor de meeste toepassingen is dat prima. Wanneer je strikte volgorde nodig hebt, zelfs bij snelheden onder de milliseconde, is het dat niet.

Monotone generatie dicht het gat. De regel is simpel: de eerste ULID in een bepaalde milliseconde krijgt zoals gebruikelijk verse willekeur, en elke latere ULID in diezelfde milliseconde wordt gemaakt door de vorige willekeurige waarde van 80 bits te nemen en met één te verhogen (behandeld als een big-endian geheel getal, met indien nodig overdracht naar hogere bits). Elke waarde is daardoor strikt groter dan de voorgaande.

Je ziet het in een batch die binnen één milliseconde is gegenereerd, waarbij alleen het laatste teken beweegt:

01KVT0F720ZK9N4T2QX7VR8WMC
01KVT0F720ZK9N4T2QX7VR8WMD
01KVT0F720ZK9N4T2QX7VR8WME

…WMC < …WMD < …WME, gegarandeerd. Dit telt telkens wanneer rijen sneller kunnen worden aangemaakt dan de milliseconde-klok tikt: hoge-doorvoer-inserts, event-logs, message-ID’s in een strakke lus. Wanneer de klok doortikt naar de volgende milliseconde, valt de generatie terug op verse willekeur en herhaalt de cyclus zich.

ULID vs UUID: wanneer kies je welke

De vraag waarmee de meeste mensen eigenlijk komen is ULID vs UUID. Hier is de gerichte vergelijking: ULID tegenover de twee UUID-versies die je er realistisch tegen zou afwegen. (Voor de volledige beslismatrix met vijf opties, inclusief Snowflake en NanoID, zie de volledige vergelijking van ULID, UUID en Snowflake.)

EigenschapULIDUUIDv4UUIDv7
Lengte26 tekens36 tekens36 tekens
CoderingCrockford Base32Hex met koppeltekensHex met koppeltekens
Sorteerbaar op tijd?JaNeeJa
Bedt tijdstempel in?Ja (48-bits ms)NeeJa (48-bits ms)
Gestandaardiseerd?Community-specRFC 9562RFC 9562
Best voorKorte sorteerbare ID’sOndoorzichtige willekeurige ID’sSorteerbare ID’s in UUID-formaat

In gewone taal: grijp naar een ULID wanneer je de kortste, URL-veilige, sorteerbare string wilt. Grijp naar UUIDv4 wanneer je een ondoorzichtige, volledig willekeurige identifier wilt zonder ingebedde tijd, bijvoorbeeld een openbare token waarbij je liever niet prijsgeeft wanneer hij is aangemaakt. Grijp naar UUIDv7 wanneer je tijdvolgorde nodig hebt maar binnen het standaard UUID-formaat moet blijven, met versie- en variant-bits op hun vaste posities en een native uuid-kolom om hem in te plaatsen.

Alle drie zijn 128 bits, dus de conversie ULID ↔ UUID is beide kanten op verliesvrij. Het verband tussen ULID en ulid vs uuid v7 is hechter dan het lijkt: UUIDv7 is in wezen de door de IETF gestandaardiseerde uitwerking van hetzelfde idee met tijdprefix dat ULID pionierde. Ben je helemaal nieuw met UUID’s, begin dan eerst met de basis en kom daarna terug bij deze vergelijking.

De privacy-afweging: ULID’s verraden hun aanmaaktijd

De ingebedde tijdstempel is tegelijk een feature en een lek, afhankelijk van wie de ID leest. Iedereen die een ULID in handen heeft, kan in één stap de tijdstempel decoderen en precies de milliseconde leren waarop het record is aangemaakt, zonder toegang tot je database.

Binnen je eigen systemen is dat puur voordeel: directe auditing, gratis ordening, eenvoudig debuggen. Op een publiek zichtbare identifier is het een echte onthulling. De aanmaaktijd kan op zichzelf bedrijfsgevoelig zijn, en een handvol ULID’s die over de tijd worden bemonsterd verraden je aanmaaktempo — hoeveel bestellingen, accounts of berichten je per seconde aanmaakt — precies het soort gegeven dat concurrenten en scrapers graag inschatten.

Eerlijk is eerlijk: dit is een smaller lek dan UUIDv1, dat van oudsher het MAC-adres van de genererende machine inbedde; een ULID legt alleen tijd bloot, nooit hardware-identiteit. Toch: weeg het af. De simpele beperking is om ULID’s intern te houden en een volledig willekeurige UUIDv4 uit te delen voor publiek zichtbare ID’s waar ordening niet uitmaakt.

Veelvoorkomende valkuilen bij ULID’s

De meeste ULID-problemen zijn een handvol vermijdbare engineering-keuzes, geen fouten in het formaat. De terugkerende:

  • Aannemen dat gewone ULID’s uit dezelfde milliseconde geordend zijn. Ze delen een tijdprefix maar hebben onafhankelijke willekeurige staarten, dus hun volgorde is ongedefinieerd. Oplossing: gebruik de monotone modus wanneer je strikte ordening nodig hebt bij snelheden onder de milliseconde.
  • Een ULID opslaan als tekst van 26 tekens. Dat verspilt ruimte en doet de index opzwellen. Oplossing: sla de 128 bits op als 16 bytes (uuid / BINARY(16)) en codeer pas naar Base32 aan de randen.
  • Verwachten dat een conversie ULID→UUID zich als v4 of v7 voordoet. De conversie hercodeert dezelfde bits; ze zet de versie- en variantvelden van de UUID niet, dus een bibliotheek die ze inspecteert ziet geen getagde versie. Oplossing: behandel het resultaat als een ondoorzichtige 128-bits waarde, of genereer een echte UUIDv7 wanneer je de tag nodig hebt.
  • De willekeur vullen met Math.random. Die is voorspelbaar en kan botsen. Oplossing: gebruik altijd een CSPRNG zoals crypto.getRandomValues.
  • ULID’s publiek blootstellen zonder het tijdstempellek af te wegen. Zie de privacysectie hierboven. Oplossing: ULID’s intern, willekeurige UUIDv4 voor publieke ID’s.
  • I, L, O of U met de hand in een ULID typen. Die letters zitten niet in het alfabet, en overtypen lokt fouten uit. Oplossing: kopieer ULID’s, typ ze niet over.

FAQ

Is ULID een officiële standaard zoals UUID?

Nee. ULID is een community-specificatie die op GitHub is gepubliceerd, geen IETF-RFC. Hij wordt breed geïmplementeerd en is stabiel, maar er staat geen standaardisatie-instantie achter. Heb je een gestandaardiseerde, op tijd geordende identifier nodig, dan past UUIDv7 (RFC 9562) hetzelfde idee toe binnen het officiële UUID-formaat.

Uit hoeveel tekens bestaat een ULID, en waarom is hij korter dan een UUID?

26 tekens, tegenover de 36 van een UUID. ULID gebruikt Crockford Base32, dat 5 bits per teken inpakt; het hexadecimaal van een UUID pakt er maar 4 in en voegt vier koppeltekens toe. Dezelfde 128 bits hebben in Base32 dus minder tekens nodig, en geen ervan vraagt om URL-escaping.

Kunnen twee ULID’s ooit botsen?

Praktisch nooit. Binnen één milliseconde heeft een ULID 80 willekeurige bits (ongeveer 1,2 × 10²⁴ mogelijkheden), dus zelfs miljoenen per milliseconde aanmaken houdt de botsingskans verwaarloosbaar klein. De enige eis is dat een cryptografisch veilige RNG de willekeur vult; Math.random maakt de garantie ongeldig.

Kan ik ULID’s opslaan in PostgreSQL of MySQL?

Ja. Een ULID is 128 bits, dus zet hem om naar UUID-vorm en sla hem op in een uuid-kolom (PostgreSQL) of BINARY(16) (MySQL), en render de Base32-string pas aan de randen. Er bestaat geen native ULID-kolomtype, maar de UUID-representatie kost dezelfde 16 bytes en houdt de index compact.

Zijn ULID’s hoofdlettergevoelig?

De canonieke vorm is in hoofdletters, maar Crockford Base32 is bij invoer hoofdletterongevoelig: een decoder leest kleine letters op dezelfde manier, en zet I/L om naar 1 en O naar 0. Om verrassingen bij gelijkheidstoetsen en indexen te voorkomen, normaliseer je naar één lettervorm voordat je opslaat of vergelijkt.

Raakt de 48-bits tijdstempel ooit op?

Nog heel lang niet. 48 bits aan milliseconden reiken tot het jaar 10889 voordat de teller overloopt, dus de tijdstempelcomponent is in feite toekomstbestendig voor elke echte toepassing. Je vervangt het systeem, de taal en de database ruim voordat het formaat ruimte tekortkomt.

Kan ik ULID’s in de browser of op mobiel genereren zonder server?

Ja, en dat is een kernvoordeel. ULID’s hebben geen centrale coördinator nodig, dus elke node, edge worker, browser of apparaat kan er een aanmaken uit zijn klok plus een veilige RNG. Waarden die op verschillende machines zijn aangemaakt, sorteren achteraf nog steeds samen op tijd, omdat de tijdstempel in de ID zelf zit.

Conclusie

ULID’s lossen een specifiek, reëel probleem op — willekeurige sleutels die je index fragmenteren — zonder de gedecentraliseerde generatie weg te nemen. De werking is het onthouden waard:

  • Een ULID is een 48-bits milliseconde-tijdstempel + 80 bits willekeur, gecodeerd als 26 Crockford Base32-tekens.
  • Hij sorteert op tijd omdat de tijdstempel de meest significante component is en het alfabet de volgorde behoudt: stringvolgorde is gelijk aan tijdvolgorde.
  • Die ordening geeft een B-tree de insert-lokaliteit die een willekeurige UUIDv4 mist, waardoor schrijfacties snel blijven en de index compact.
  • Gebruik de monotone modus wanneer je strikte ordening nodig hebt voor ID’s die in dezelfde milliseconde zijn aangemaakt.
  • Weeg het tijdstempellek af voordat je ULID’s blootstelt op publiek zichtbare identifiers.
  • Kies in plaats daarvan UUIDv7 wanneer je binnen het standaard UUID-formaat moet blijven.

Als je het in de praktijk wilt brengen, open dan de ULID-generator om ULID’s volledig in je browser te genereren, decoderen en omzetten. Geen server, geen upload, niets opgeslagen.

Tags: ulid uuid unique-identifier database primary-key

Gerelateerde artikelen

Alle artikelen bekijken