Skip to content
Powrót do bloga
Poradniki

Czym jest ULID? Przewodnik po sortowalnym identyfikatorze

Czym jest ULID? Jak działa sortowalny 128-bitowy identyfikator: struktura znacznika czasu i losowości, kodowanie Crockford Base32 oraz kiedy wybrać go zamiast UUID.

12 min czytania

Czym jest ULID? Sortowalny identyfikator unikalny w praktyce

Każdy losowy UUIDv4, który wstawiasz jako klucz główny, ląduje w nieprzewidywalnym miejscu indeksu bazy danych. Powtórz to kilka milionów razy, a indeks się fragmentuje, cache zaczyna się przemiatać, a zapisy zwalniają. ULID rozwiązuje ten problem, nie odbierając tego, za co ceni się UUID-y: nadal można wygenerować taki identyfikator gdziekolwiek, bez centralnego koordynatora, ale ląduje on w kolejności czasowej, zamiast się rozpraszać.

Jak więc 26-znakowy ciąg sam sortuje się według czasu? Na tym polega cała sztuczka i dobrze ją zrozumieć, zanim się po niego sięgnie.

ULID (Universally Unique Lexicographically Sortable Identifier) to 128-bitowy identyfikator zapisany jako 26 znaków Crockford Base32. Pierwszych 10 znaków koduje znacznik czasu w milisekundach, a ostatnich 16 koduje bity losowe, dzięki czemu ULID-y utworzone później zawsze sortują się po tych wcześniejszych, gdy porównuje się je jako zwykłe ciągi znaków. To sortowalny identyfikator unikalny, który można wygenerować offline.

Ten przewodnik rozkłada go na czynniki pierwsze: anatomia rozszyfrowana znak po znaku, dowód, że naprawdę się sortuje, matematyka B-tree stojąca za przewagą w bazie danych oraz uczciwe spojrzenie na to, co ujawnia wbudowany znacznik czasu. Możesz śledzić tekst z żywą wartością w generatorze ULID — wygeneruj jeden, zdekoduj go, przekonwertuj na UUID — w trakcie czytania.

Czym jest ULID?

ULID (Universally Unique Lexicographically Sortable Identifier) to 128-bitowy identyfikator zaprojektowany jako bardziej sortowalna i bardziej zwarta alternatywa dla UUID. Zapisuje się go jako 26 znaków Crockford Base32: pierwszych 10 mieści 48-bitowy znacznik czasu w milisekundach od początku epoki Unix, a pozostałych 16 mieści 80 bitów losowości. Ponieważ czas znajduje się na początku, ciąg sortuje się chronologicznie.

To właśnie ta ostatnia właściwość jest powodem istnienia tego formatu. UUIDv4 jest w pełni losowy, co świetnie sprawdza się przy unikalności, ale oznacza, że dwa identyfikatory utworzone w odstępie sekundy nie mają ze sobą żadnego związku. ULID-y zachowują model „generuj gdziekolwiek” bez koordynacji i dodają do niego porządkowanie czasowe, więc kolumna takich wartości jest naturalnie posortowana według czasu utworzenia — bez niczego dodatkowego.

Oto format w skrócie:

WłaściwośćWartość
Bity128
Kodowanie26 znaków Crockford Base32
Układ48-bitowy znacznik czasu + 80-bitowa losowość

Dalsza część artykułu wyjaśnia, jak działa każdy element. Kodowanie i sortowalność mają osobne sekcje; zacznijmy od układu.

Anatomia ULID: 48 bitów czasu + 80 bitów losowości

26 znaków ULID dzieli się czysto na dwie połowy. Pierwszych 10 znaków to znacznik czasu, ostatnich 16 to część losowa. Wystarczy rozłożyć kanoniczny przykład, a granica staje się oczywista:

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

Dwa komponenty, dwa zadania: jeden zapisuje kiedy, drugi gwarantuje unikalność. Poniżej każdy z osobna.

48-bitowy znacznik czasu (pierwszych 10 znaków)

Wiodące 10 znaków koduje 48-bitową liczbę całkowitą: liczbę milisekund od początku epoki Unix w chwili utworzenia ULID. Weźmy kanoniczny przykład prosto ze specyfikacji:

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

To prawdziwe, odwracalne dekodowanie — wklej 01ARYZ6S41TSV4RRFFQ69G5FAV do dekodera, a otrzymasz dokładnie 2016-07-30T22:36:16.385Z. Komponent czasu to zwykłe dane, a nie hash, więc jego odczytanie nic nie kosztuje.

Jeden drobny szczegół, który zbija ludzi z tropu: pierwszy znak ULID zawsze mieści się między 0 a 7. Znak Crockforda mieści 5 bitów, a 48 bitów nie jest wielokrotnością 5 — znacznik czasu zajmuje niskie 48 z 50 bitów, które potrafi przenieść 10 znaków, przez co górne 2 bity pierwszego znaku są na stałe zerowe. Dwa zerowe bity ograniczają wartość tego znaku do 7. Jeśli kiedykolwiek zobaczysz ULID zaczynający się od 8 lub wyżej, jest on zniekształcony.

80 bitów losowości (ostatnich 16 znaków)

Pozostałych 16 znaków przenosi 80 bitów losowości i to właśnie ta połowa odpowiada za unikalność. Bity powinny pochodzić ze źródła kryptograficznie bezpiecznego — crypto.getRandomValues w przeglądarce, a nie Math.random. Ta różnica ma znaczenie: Math.random jest na tyle przewidywalny, że atakujący mógłby odgadnąć lub doprowadzić do kolizji wartości, podczas gdy CSPRNG już nie.

Ile to miejsca — 80 bitów? Mniej więcej 1,2 × 10²⁴ możliwych wartości, i to na milisekundę. Nawet jeśli wygenerujesz miliony ULID-ów w obrębie jednej milisekundy, szanse, że dwa z nich wylosują te same 80 bitów, pozostają znikomo małe. W przeciwieństwie do znacznika czasu ta połowa nie niesie żadnego dekodowalnego znaczenia — to szum, którego jedynym celem jest sprawić, by każdy ULID był odrębny.

Crockford Base32: dlaczego ULID-y pomijają I, L, O i U

ULID-y są kodowane za pomocą Crockford Base32, alfabetu 32 symboli: cyfr 09 oraz liter AZ z czterema usuniętymi.

0123456789ABCDEFGHJKMNPQRSTVWXYZ

Brakujące litery to I, L, O i U. Trzy zostały usunięte, bo wyglądają jak cyfry — I i L przypominają 1, O przypomina 0 — żeby człowiek odczytujący ULID z ekranu nie pomylił litery z cyfrą. Zaletą tej decyzji jest pobłażliwość na wejściu: zgodny dekoder mapuje I i L z powrotem na 1, a O na 0, i traktuje cały ciąg bez rozróżniania wielkości liter. U jest wykluczone osobno, by przypadkiem nie układać obraźliwych słów.

Drugim powodem jest matematyka bitowa. Każdy znak Base32 koduje 5 bitów, podczas gdy znak szesnastkowy koduje tylko 4. Spakuj 128 bitów po 5 bitów na znak, a potrzebujesz 26; spakuj te same 128 bitów po 4 bity każdy — tak jak robi to UUID — a potrzebujesz 32, plus cztery łączniki, czyli 36 znaków. ULID jest więc zauważalnie krótszy od UUID i — bez łączników — wpada prosto do URL, nazwy pliku czy nagłówka bez konieczności znakowania ucieczki.

Crockford Base32 to alfabet 32 symboli (09 oraz AZ bez I, L, O, U), który koduje 5 bitów na znak. ULID-y wykorzystują go do upakowania 128 bitów w 26 znaków odpornych na wielkość liter i bezpiecznych dla URL, a — co kluczowe — alfabet jest ułożony rosnąco, co pozwala zakodowanemu ciągowi sortować się tak samo jak surowe bity.

Dlaczego ULID-y sortują się według czasu

Wiele artykułów mówi, że ULID-y sortują się według czasu. Mniej pokazuje dlaczego. Uzasadnienie opiera się na dwóch faktach, które już znasz: znacznik czasu jest najbardziej znaczącą częścią wartości, a alfabet Crockforda jest ułożony rosnąco.

Połącz je, a otrzymasz łańcuch równoważności:

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

Czytaj od lewej do prawej. Porównanie dwóch ULID-ów znak po znaku (tak jak działa sortowanie ciągów) daje tę samą odpowiedź co porównanie ich bazowych 128-bitowych liczb całkowitych, ponieważ alfabet zachowuje porządek — „wyższy” znak zawsze oznacza wyższą wartość. Porównanie 128-bitowych liczb całkowitych daje tę samą odpowiedź co porównanie czasów utworzenia, ponieważ znacznik czasu siedzi w najbardziej znaczących bitach, więc dominuje w porównaniu; losowy ogon rozstrzyga jedynie remisy w obrębie tej samej milisekundy. Porządek ciągów, porządek bitów i porządek czasu to ten sam porządek.

Szybka demonstracja. Dwa ULID-y wygenerowane w odstępie jednej milisekundy:

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

Dziesiąty znak przeskakuje z 1 na 2, a zwykłe sortowanie tekstowe stawia drugi za pierwszym — bez kolumny ze znacznikiem czasu, bez specjalnego komparatora. Praktyczna korzyść, którą rozwija następna sekcja, mieści się w jednej linijce: ORDER BY id zwraca wiersze w kolejności chronologicznej bez dodatkowego indeksu.

ULID-y jako klucze główne bazy danych: lokalność w B-tree

Tu właśnie ULID-y odpracowują swoje miejsce. Większość relacyjnych baz danych przechowuje indeks klucza głównego jako B-tree, a to, gdzie nowy klucz wyląduje w tym drzewie, decyduje o koszcie wstawienia.

Losowy UUIDv4 ląduje za każdym razem w nieprzewidywalnym miejscu:

UUIDv4: każdy nowy klucz trafia w losową stronę liścia. Strona jest często pełna, więc silnik ją dzieli, kopiuje połowę wierszy gdzie indziej i brudzi strony w całym drzewie. Na przestrzeni milionów wierszy fragmentuje to indeks, usuwa przydatne strony z bufora cache i obniża przepustowość zapisów. (Po twarde liczby dotyczące podziałów stron indeksu — zwykle różnica 2–10× na tabelach z dużą liczbą zapisów — sięgnij do przewodnika porównawczego).

ULID z prefiksem czasowym ląduje za każdym razem na końcu:

ULID: ponieważ wysokie bity to znacznik czasu, każdy nowy klucz jest większy od poprzedniego, więc dokleja się przy prawej krawędzi indeksu lub blisko niej. Wstawienia pozostają sekwencyjne, podziały stron niemal znikają, indeks pozostaje zwarty, a skan zakresowy po oknie czasowym odczytuje ciągły bieg stron.

Otrzymujesz generowanie bez koordynacji typowe dla UUID wraz z lokalnością wstawień znaną z liczby całkowitej auto-increment — bez ujawniania możliwego do odgadnięcia licznika sekwencyjnego, bo losowy ogon nadal ukrywa dokładną kolejną wartość.

Wskazówka dotycząca przechowywania: przechowuj 128 bitów jako 16 bajtów binarnych — kolumna uuid w PostgreSQL, BINARY(16) w MySQL — a nie jako 26-znakowe pole tekstowe, które marnuje miejsce i rozdyma indeks. Koduj na ciąg Base32 dopiero na krawędziach, gdzie widzi go człowiek lub URL. Zakładka Convert w generatorze przekonwertuje ULID na UUID dokładnie w tym celu, skoro obie formy to te same 128 bitów.

Monotoniczne ULID-y: ścisły porządek w obrębie milisekundy

Dowód na sortowalność ma jedną uczciwą lukę: w obrębie pojedynczej milisekundy zwykłe ULID-y nie są ściśle uporządkowane. Dzielą ten sam 10-znakowy prefiks czasowy, ale ich 80-bitowe losowe ogony są losowane niezależnie, więc to, który z dwóch ULID-ów z tej samej milisekundy sortuje się pierwszy, jest w istocie rzutem monetą. Do większości zastosowań to wystarczy. Gdy potrzebujesz ścisłego porządku nawet przy tempie poniżej milisekundy, już nie.

Generowanie monotoniczne zamyka tę lukę. Reguła jest prosta: pierwszy ULID w danej milisekundzie dostaje świeżą losowość jak zwykle, a każdy kolejny ULID w tej samej milisekundzie powstaje przez wzięcie poprzedniej 80-bitowej wartości losowej i zwiększenie jej o jeden (traktowanej jako liczba całkowita big-endian, z przeniesieniem do wyższych bitów w razie potrzeby). Każda wartość jest zatem ściśle większa od poprzedniej.

Widać to w partii wygenerowanej w obrębie jednej milisekundy — porusza się tylko ostatni znak:

01KVT0F720ZK9N4T2QX7VR8WMC
01KVT0F720ZK9N4T2QX7VR8WMD
01KVT0F720ZK9N4T2QX7VR8WME

…WMC < …WMD < …WME, z gwarancją. Ma to znaczenie zawsze, gdy wiersze mogą powstawać szybciej, niż tyka zegar milisekundowy: wstawienia o dużej przepustowości, dzienniki zdarzeń, identyfikatory wiadomości w ciasnej pętli. Gdy zegar przechodzi do następnej milisekundy, generowanie wraca do świeżej losowości i cykl się powtarza.

ULID kontra UUID: kiedy którego użyć

Pytanie, z którym większość ludzi faktycznie przychodzi, brzmi ULID kontra UUID. Oto skoncentrowane porównanie — ULID zestawiony z dwiema wersjami UUID, które realnie braliby pod uwagę. (Po pełną pięciokierunkową macierz decyzyjną obejmującą Snowflake i NanoID sięgnij do pełnego porównania ULID, UUID i Snowflake).

WłaściwośćULIDUUIDv4UUIDv7
Długość26 znaków36 znaków36 znaków
KodowanieCrockford Base32Szesnastkowe z łącznikamiSzesnastkowe z łącznikami
Sortowalny według czasu?TakNieTak
Osadza znacznik czasu?Tak (48-bitowy ms)NieTak (48-bitowy ms)
Standaryzowany?Specyfikacja społecznościowaRFC 9562RFC 9562
Najlepszy doKrótkich sortowalnych IDNieprzejrzystych losowych IDSortowalnych ID w formacie UUID

Prozą: sięgnij po ULID, gdy chcesz najkrótszy, bezpieczny dla URL, sortowalny ciąg. Sięgnij po UUIDv4, gdy chcesz nieprzejrzysty, w pełni losowy identyfikator bez osadzonego czasu — na przykład publiczny token, przy którym wolałbyś nie ujawniać, kiedy został utworzony. Sięgnij po UUIDv7, gdy potrzebujesz porządkowania czasowego, ale musisz pozostać w standardowym formacie UUID, z bitami wersji i wariantu na ich stałych pozycjach i natywną kolumną uuid, do której można go włożyć.

Wszystkie trzy mają 128 bitów, więc konwersja ULID ↔ UUID jest bezstratna w obie strony. Związek między ULID a ulid vs uuid v7 jest bliższy, niż się wydaje: UUIDv7 to w istocie ustandaryzowane przez IETF ujęcie tej samej idei prefiksu czasowego, którą ULID utorował drogę. Jeśli dopiero zaczynasz z UUID, zacznij najpierw od podstaw, a potem wróć do tego porównania.

Kompromis dotyczący prywatności: ULID-y ujawniają czas swojego utworzenia

Wbudowany znacznik czasu jest funkcją i wyciekiem — zależnie od tego, kto odczytuje identyfikator. Każdy, kto trzyma ULID, może w jednym kroku zdekodować znacznik czasu i poznać dokładną milisekundę utworzenia rekordu — bez żadnego dostępu do bazy danych.

Wewnątrz własnych systemów to czysta korzyść: natychmiastowy audyt, darmowe porządkowanie, łatwe debugowanie. Na identyfikatorze udostępnianym publicznie to realne ujawnienie. Czas utworzenia sam w sobie może być wrażliwy biznesowo, a garść ULID-ów próbkowanych w czasie ujawnia tempo tworzenia — ile zamówień, kont czy wiadomości generujesz na sekundę — czyli to, co konkurenci i scrapery lubią szacować.

Gwoli ścisłości, to węższy wyciek niż w UUIDv1, który historycznie osadzał adres MAC maszyny generującej; ULID ujawnia tylko czas, nigdy tożsamość sprzętu. Mimo to rozważ to. Prosta mitygacja: trzymaj ULID-y wewnętrznie, a dla identyfikatorów udostępnianych publicznie, gdzie kolejność nie ma znaczenia, wydawaj w pełni losowy UUIDv4.

Częste pułapki przy ULID-ach

Większość kłopotów z ULID-ami to garść możliwych do uniknięcia decyzji inżynierskich, a nie błędy w formacie. Te powracające:

  • Zakładanie, że zwykłe ULID-y z tej samej milisekundy są uporządkowane. Dzielą prefiks czasowy, ale mają niezależne losowe ogony, więc ich kolejność jest niezdefiniowana. Rozwiązanie: używaj trybu monotonicznego, gdy potrzebujesz ścisłego porządku przy tempie poniżej milisekundy.
  • Przechowywanie ULID jako tekstu o 26 znakach. To marnuje miejsce i rozdyma indeks. Rozwiązanie: przechowuj 128 bitów jako 16 bajtów (uuid / BINARY(16)) i koduj na Base32 tylko na krawędziach.
  • Oczekiwanie, że konwersja ULID→UUID zaraportuje się jako v4 lub v7. Konwersja ponownie koduje te same bity; nie ustawia pól wersji i wariantu UUID, więc biblioteka je badająca nie zobaczy oznaczonej wersji. Rozwiązanie: traktuj wynik jako nieprzejrzystą wartość 128-bitową albo wygeneruj prawdziwy UUIDv7, gdy potrzebujesz oznaczenia.
  • Wypełnianie losowości przez Math.random. Jest przewidywalny i może powodować kolizje. Rozwiązanie: zawsze używaj CSPRNG, takiego jak crypto.getRandomValues.
  • Udostępnianie ULID-ów publicznie bez rozważenia wycieku znacznika czasu. Zobacz sekcję o prywatności powyżej. Rozwiązanie: wewnętrzne ULID-y, losowy UUIDv4 dla publicznych ID.
  • Wpisywanie ręcznie I, L, O lub U do ULID. Tych liter nie ma w alfabecie, a przepisywanie zaprasza błędy. Rozwiązanie: kopiuj ULID-y, nie przepisuj ich.

FAQ

Czy ULID to oficjalny standard jak UUID?

Nie. ULID to specyfikacja społecznościowa opublikowana na GitHubie, a nie RFC organizacji IETF. Jest szeroko wdrożona i stabilna, ale nie stoi za nią żadne ciało standaryzacyjne. Jeśli potrzebujesz ustandaryzowanego, uporządkowanego czasowo identyfikatora, UUIDv7 (RFC 9562) stosuje tę samą ideę wewnątrz oficjalnego formatu UUID.

Ile znaków ma ULID i dlaczego jest krótszy od UUID?

26 znaków wobec 36 w UUID. ULID używa Crockford Base32, który pakuje 5 bitów na znak; szesnastkowy zapis UUID pakuje tylko 4 bity i dodaje cztery łączniki. Te same 128 bitów potrzebuje więc mniej znaków w Base32 — i żaden z nich nie wymaga znakowania ucieczki w URL.

Czy dwa ULID-y mogą kiedykolwiek się zderzyć?

Praktycznie nigdy. W obrębie jednej milisekundy ULID ma 80 losowych bitów — około 1,2 × 10²⁴ możliwości — więc nawet generowanie milionów na milisekundę utrzymuje szanse kolizji na znikomo małym poziomie. Jedynym wymogiem jest, by losowość wypełniał kryptograficznie bezpieczny RNG; Math.random unieważnia tę gwarancję.

Czy mogę przechowywać ULID-y w PostgreSQL lub MySQL?

Tak. ULID ma 128 bitów, więc przekonwertuj go do postaci UUID i zapisz w kolumnie uuid (PostgreSQL) lub BINARY(16) (MySQL), a ciąg Base32 renderuj tylko na krawędziach. Nie ma natywnego typu kolumny ULID, ale reprezentacja UUID kosztuje te same 16 bajtów i utrzymuje indeks zwarty.

Czy ULID-y rozróżniają wielkość liter?

Postać kanoniczna jest wielkimi literami, ale Crockford Base32 nie rozróżnia wielkości liter na wejściu: dekoder odczytuje małe litery tak samo i mapuje I/L na 1 oraz O na 0. By uniknąć niespodzianek przy porównaniach na równość i w indeksach, normalizuj do jednej wielkości liter, zanim zapiszesz lub porównasz.

Czy 48-bitowy znacznik czasu kiedykolwiek się wyczerpie?

Nieprędko. 48 bitów milisekund sięga roku 10889, zanim licznik się przepełni, więc komponent znacznika czasu jest w praktyce odporny na upływ czasu w każdym realnym zastosowaniu. Wymienisz system, język i bazę danych długo przedtem, nim formatowi zabraknie miejsca.

Czy mogę generować ULID-y w przeglądarce lub na urządzeniu mobilnym bez serwera?

Tak — to jedna z kluczowych zalet. ULID-y nie potrzebują centralnego koordynatora, więc dowolny węzeł, edge worker, przeglądarka czy urządzenie może wygenerować taki identyfikator ze swojego zegara plus bezpiecznego RNG. Wartości utworzone na różnych maszynach i tak sortują się potem razem według czasu, bo znacznik czasu mieszka w samym identyfikatorze.

Podsumowanie

ULID-y rozwiązują konkretny, realny problem — losowe klucze fragmentujące indeks — nie odbierając zdecentralizowanego generowania. Warto trzymać w głowie ich mechanikę:

  • ULID to 48-bitowy znacznik czasu w milisekundach + 80 bitów losowości, zakodowane jako 26 znaków Crockford Base32.
  • Sortuje się według czasu, bo znacznik czasu jest najbardziej znaczącym komponentem, a alfabet zachowuje porządek — porządek ciągu równa się porządkowi czasu.
  • To porządkowanie daje B-tree lokalność wstawień, której brakuje losowemu UUIDv4, utrzymując szybkie zapisy i zwarty indeks.
  • Używaj trybu monotonicznego, gdy potrzebujesz ścisłego porządku dla ID generowanych w tej samej milisekundzie.
  • Rozważ wyciek znacznika czasu, zanim udostępnisz ULID-y na identyfikatorach publicznych.
  • Wybierz zamiast tego UUIDv7, gdy musisz pozostać w standardowym formacie UUID.

Gdy będziesz gotów wprowadzić to w życie, otwórz generator ULID, by generować, dekodować i konwertować ULID-y w całości w przeglądarce — bez serwera, bez wysyłania, nic nie jest przechowywane.

Tagi: ulid uuid unique-identifier database primary-key

Powiązane artykuły

Zobacz wszystkie artykuły