ULID nedir? Sıralanabilir benzersiz tanımlayıcı, açıklamasıyla
Birincil anahtar olarak eklediğiniz her rastgele UUIDv4, veritabanı indeksinde tahmin edilemez bir noktaya düşer. Bunu birkaç milyon kez yapın; indeks parçalanır, önbellek sürekli yenilenir ve yazma işlemleri yavaşlar. ULID bunu, UUID’lerde sevdiğiniz şeyden vazgeçmeden çözer: yine de merkezî bir koordinatör olmadan, istediğiniz yerde bir tane üretebilirsiniz, ama bu kez dağılmak yerine zaman sırasına göre yerleşir.
Peki 26 karakterlik bir karakter dizisi kendini nasıl zamana göre sıralar? Hüner tam da burada ve bir tanesine uzanmadan önce bunu anlamakta fayda var.
ULID (Universally Unique Lexicographically Sortable Identifier), 26 adet Crockford Base32 karakteriyle yazılan 128 bitlik bir tanımlayıcıdır. İlk 10 karakter milisaniye cinsinden bir zaman damgasını, son 16 karakter ise rastgele bitleri kodlar; böylece daha sonra oluşturulan ULID’ler düz karakter dizileri olarak karşılaştırıldığında daima daha önceki olanlardan sonra sıralanır. Çevrimdışı üretebileceğiniz bir sıralanabilir benzersiz tanımlayıcıdır.
Bu rehber onu parçalarına ayırıyor: karakter karakter çözülmüş yapısı, gerçekten sıraladığının kanıtı, veritabanı kazanımının arkasındaki B-tree matematiği ve gömülü zaman damgasının neyi sızdırdığına dair dürüst bir bakış. Okurken canlı bir değerle ULID üreticisi üzerinden takip edebilirsiniz: bir tane üretin, çözün, bir UUID’ye dönüştürün.
ULID nedir?
ULID (Universally Unique Lexicographically Sortable Identifier), bir UUID’ye göre daha sıralanabilir ve daha kompakt bir alternatif olarak tasarlanmış 128 bitlik bir tanımlayıcıdır. 26 adet Crockford Base32 karakteriyle yazılır: ilk 10 karakter, Unix epoch’tan bu yana milisaniye cinsinden 48 bitlik bir zaman damgası tutar, kalan 16 karakter ise 80 bitlik rastgelelik tutar. Zaman önce geldiği için karakter dizisi kronolojik olarak sıralanır.
Bu son özellik, formatın var olma nedenidir. UUIDv4 tamamen rastgeledir; bu, benzersizlik için harikadır ama bir saniye arayla oluşturulan iki tanımlayıcının birbiriyle hiçbir ilişkisi olmadığı anlamına gelir. ULID’ler koordinasyon gerektirmeyen, her yerde üretilebilen modeli korur ve üstüne zaman sıralamasını ekler; böylece bir sütun dolusu ULID, hiçbir ek şey olmadan doğal olarak oluşturulma zamanına göre sıralanır.
İşte format kısaca:
| Özellik | Değer |
|---|---|
| Bit | 128 |
| Kodlama | 26 Crockford Base32 karakteri |
| Düzen | 48 bitlik zaman damgası + 80 bitlik rastgelelik |
Makalenin geri kalanı her parçanın nasıl çalıştığını dolduruyor. Kodlama ve sıralanabilirlik kendi bölümlerini hak ediyor; Base32’yi ve sıralama kanıtını birazdan ele alacağız. Önce düzen.
Bir ULID’in anatomisi: 48 bit zaman + 80 bit rastgelelik
Bir ULID’in 26 karakteri düzgünce iki yarıya ayrılır. İlk 10 karakter zaman damgasıdır; son 16 karakter rastgele kısımdır. Kanonik örneği serdiğinizde sınır apaçık ortaya çıkar:
01ARYZ6S41 TSV4RRFFQ69G5FAV
└────────┘ └──────────────┘
10 chars 16 chars
48-bit ms 80-bit random
timestamp
İki bileşen, iki iş. Biri ne zaman olduğunu kaydeder; diğeri benzersizliği garanti eder. Her birini çözelim.
48 bitlik zaman damgası (ilk 10 karakter)
Baştaki 10 karakter, 48 bitlik bir tam sayıyı kodlar: ULID oluşturulduğu andaki Unix epoch’tan bu yana geçen milisaniye sayısı. Spesifikasyondan alınan kanonik örneği ele alın:
01ARYZ6S41 -> 1469918176385 ms -> 2016-07-30T22:36:16.385Z
Bu gerçek ve geri döndürülebilir bir çözümdür: 01ARYZ6S41TSV4RRFFQ69G5FAV değerini bir çözücüye yapıştırın, tam olarak 2016-07-30T22:36:16.385Z değerini geri alırsınız. Zaman bileşeni düz veridir, bir hash değil, bu yüzden onu okumak hiçbir maliyet getirmez.
İnsanları takılan küçük bir ayrıntı: bir ULID’in ilk karakteri daima 0 ile 7 arasındadır. Bir Crockford karakteri 5 bit tutar ve 48, 5’in katı değildir: zaman damgası, 10 karakterin taşıyabileceği 50 bitin alt 48’ini işgal eder ve ilk karakterin en üstteki 2 bitini kalıcı olarak sıfır bırakır. İki sıfır bit, o karakterin değerini 7 ile sınırlar. 8 veya daha yükseğiyle başlayan bir ULID görürseniz, o bozuktur.
80 bitlik rastgelelik (son 16 karakter)
Kalan 16 karakter 80 bitlik rastgelelik taşır ve benzersizlik bu yarıdan gelir. Bitler kriptografik olarak güvenli bir kaynaktan gelmelidir: tarayıcıda crypto.getRandomValues, Math.random değil. Bu fark önemlidir: Math.random, bir saldırganın değerleri tahmin edebileceği veya çakıştırabileceği kadar öngörülebilirken, bir CSPRNG değildir.
80 bit ne kadar yer demek? Yaklaşık 1,2 × 10²⁴ olası değer ve bu da milisaniye başına. Tek bir milisaniye içinde milyonlarca ULID üretseniz bile, ikisinin aynı 80 biti çekme ihtimali yok denecek kadar küçük kalır. Zaman damgasının aksine, bu yarı çözülebilir bir anlam taşımaz; yegâne amacı her ULID’i ayırt edilir kılmak olan bir gürültüdür.
Crockford’un Base32’si: ULID’ler neden I, L, O ve U’yu atar
ULID’ler, 32 sembolden oluşan bir alfabe olan Crockford’un Base32’siyle kodlanır: 0–9 rakamları ve dört harfi çıkarılmış A–Z harfleri.
0123456789ABCDEFGHJKMNPQRSTVWXYZ
Eksik harfler I, L, O ve U’dur. Üçü, rakamlara benzedikleri için atılmıştır (I ve L, 1’e benzer, O, 0’a benzer), böylece bir ULID’i ekrandan okuyan bir insan, bir harfi bir rakamla karıştıramaz. Bunun diğer yüzü ise hoşgörülü girdidir: uyumlu bir çözücü, I ve L’yi tekrar 1’e ve O’yu 0’a eşler ve tüm karakter dizisini büyük/küçük harfe duyarsız işler. U, kazara saldırgan kelimeler oluşturmamak için ayrıca dışarıda bırakılır.
Bit matematiği de diğer nedendir. Her Base32 karakteri 5 bit kodlarken, bir onaltılık karakter yalnızca 4 bit kodlar. 128 biti karakter başına 5 bitle paketleyin, 26’ya ihtiyacınız olur; aynı 128 biti, bir UUID’nin yaptığı gibi karakter başına 4 bitle paketleyin, artı dört tire ile birlikte 32’ye, yani 36 karaktere ihtiyacınız olur. Yani bir ULID, bir UUID’den anlamlı ölçüde kısadır ve tiresiz olduğundan, kaçış karakteri gerektirmeden doğrudan bir URL’ye, bir dosya adına veya bir başlığa girer.
Crockford’un Base32’si, karakter başına 5 bit kodlayan 32 sembolden (0 ile 9 arası rakamlar ve I, L, O, U çıkarılmış A–Z harfleri) oluşan bir alfabedir. ULID’ler onu 128 biti 26 büyük/küçük harfe duyarsız, URL güvenli karaktere paketlemek için kullanır ve, kritik biçimde, alfabe artan sıradadır, ki kodlanmış karakter dizisinin ham bitlerle aynı şekilde sıralanmasını sağlayan budur.
ULID’ler neden zamana göre sıralanır
Birçok makale size ULID’lerin zamana göre sıralandığını söyler. Daha azı nedenini gösterir. Gerekçe, halihazırda sahip olduğunuz iki olguya dayanır: zaman damgası, değerin en anlamlı parçasıdır ve Crockford’un alfabesi artan sırada dizilmiştir.
Bunları bir araya getirdiğinizde bir denklikler zinciri elde edersiniz:
string compare == 128-bit integer compare == creation-time compare
Soldan sağa okuyun. İki ULID’i karakter karakter karşılaştırmak (bir karakter dizisi sıralamasının çalışma şekli), altta yatan 128 bitlik tam sayıları karşılaştırmakla aynı sonucu verir, çünkü alfabe sıra koruyucudur: “daha yüksek” bir karakter daima daha yüksek bir değer demektir. 128 bitlik tam sayıları karşılaştırmak, oluşturma zamanlarını karşılaştırmakla aynı sonucu verir, çünkü zaman damgası en anlamlı bitlerde oturur, dolayısıyla karşılaştırmaya hâkim olur; rastgele kuyruk yalnızca aynı milisaniye içindeki eşitlikleri bozar. Karakter dizisi sırası, bit sırası ve zaman sırası aynı sıradır.
Hızlı bir gösterim. Bir milisaniye arayla üretilen iki ULID:
01ARYZ6S41... (created at T)
01ARYZ6S42... (created at T + 1 ms)
Onuncu karakter 1’den 2’ye geçer ve düz bir metin sıralaması ikincisini birincinin arkasına koyar; bunun için ne ayrı bir zaman damgası sütununa ne de özel bir karşılaştırıcıya gerek vardır. Bir sonraki bölümün genişlettiği pratik kazanç tek satırdır: ORDER BY id, satırları hiçbir ek indeks olmadan kronolojik sırada döndürür.
Veritabanı birincil anahtarı olarak ULID’ler: B-tree yerelliği
ULID’lerin asıl hakkını verdiği yer burasıdır. Çoğu ilişkisel veritabanı, bir birincil anahtar indeksini B-tree olarak saklar ve yeni bir anahtarın o ağaçta nereye düştüğü, eklemenin ne kadar pahalı olacağına karar verir.
Rastgele bir UUIDv4, her eklemede tahmin edilemez bir yere düşer:
UUIDv4: her yeni anahtar rastgele bir yaprak sayfasını hedefler. Sayfa çoğunlukla doludur, bu yüzden motor onu böler, satırların yarısını başka bir yere kopyalar ve ağaçta dört bir yandaki sayfaları kirletir. Milyonlarca satır boyunca bu, indeksi parçalar, faydalı sayfaları arabellek önbelleğinden atar ve ekleme verimini aşağı çeker. (Net indeks sayfa-bölme sayıları için, yani yazma yoğun tablolarda tipik olarak 2–10× fark için, karşılaştırma rehberine bakın.)
Zaman önekli bir ULID her seferinde sona düşer:
ULID: yüksek bitler bir zaman damgası olduğundan, her yeni anahtar bir öncekinden büyüktür, bu yüzden indeksin sağ kenarına ya da yakınına eklenir. Eklemeler ardışık kalır, sayfa bölmeleri neredeyse kaybolur, indeks kompakt kalır ve bir zaman penceresi üzerindeki bir aralık taraması, bitişik bir sayfa dizisini okur.
Bir UUID’nin koordinasyon gerektirmeyen üretimini, bir otomatik-artan tam sayının ekleme yerelliğiyle birlikte elde edersiniz. Üstelik bunu, tahmin edilebilir ardışık bir sayaç ifşa etmeden yaparsınız, çünkü rastgele kuyruk hâlâ bir sonraki tam değeri saklar.
Saklama ipucu: 128 biti 16 ikili bayt olarak saklayın (PostgreSQL’de bir uuid sütunu, MySQL’de BINARY(16)), yer israf eden ve indeksi şişiren 26 karakterlik bir metin alanı olarak değil. Base32 karakter dizisine yalnızca bir insanın veya bir URL’nin gördüğü kenarlarda kodlayın. Üreticinin Dönüştür sekmesi tam da bunun için bir ULID’i bir UUID’ye dönüştürür, çünkü iki biçim de aynı 128 bittir.
Monotonik ULID’ler: Bir milisaniye içinde kesin sıra
Sıralanabilirlik kanıtının dürüst bir boşluğu var: tek bir milisaniye içinde, düz ULID’ler kesin olarak sıralı değildir. Aynı 10 karakterlik zaman önekini paylaşırlar, ama 80 bitlik rastgele kuyrukları bağımsız çekilir, bu yüzden aynı milisaniyeden iki ULID’den hangisinin önce sıralanacağı esasen yazı-tura gibidir. Çoğu kullanım için bu sorun değildir. Milisaniye altı oranlarda bile kesin sıraya ihtiyaç duyduğunuzda ise sorundur.
Monotonik üretim bu boşluğu kapatır. Kural basittir: belirli bir milisaniyedeki ilk ULID her zamanki gibi taze rastgelelik alır ve aynı milisaniyedeki sonraki her ULID, önceki 80 bitlik rastgele değer alınıp bir artırılarak üretilir (big-endian bir tam sayı olarak ele alınır, gerektiğinde daha yüksek bitlere elde taşınır). Dolayısıyla her değer, kendisinden öncekinden kesinlikle büyüktür.
Bunu bir milisaniye içinde üretilen bir partide görebilirsiniz; yalnızca son karakter hareket eder:
01KVT0F720ZK9N4T2QX7VR8WMC
01KVT0F720ZK9N4T2QX7VR8WMD
01KVT0F720ZK9N4T2QX7VR8WME
…WMC < …WMD < …WME, garantili. Bu, satırların milisaniye saati ilerlemesinden daha hızlı oluşturulabildiği her durumda önemlidir: yüksek verimli eklemeler, olay günlükleri, dar bir döngüdeki mesaj kimlikleri. Saat bir sonraki milisaniyeye ilerlediğinde, üretim taze rastgeleliğe geri döner ve döngü tekrarlanır.
ULID vs UUID: Hangisini ne zaman kullanmalı
İnsanların aslında çoğunlukla geldiği soru ULID vs UUID’dir. Odaklanmış karşılaştırma şöyle, ULID’in gerçekçi olarak karşısına koyacağınız iki UUID sürümüne karşı. (Snowflake ve NanoID dahil tam beş yönlü karar matrisi için ULID, UUID ve Snowflake’in tam karşılaştırmasına bakın.)
| Özellik | ULID | UUIDv4 | UUIDv7 |
|---|---|---|---|
| Uzunluk | 26 karakter | 36 karakter | 36 karakter |
| Kodlama | Crockford Base32 | Tireli onaltılık | Tireli onaltılık |
| Zamana göre sıralanabilir mi? | Evet | Hayır | Evet |
| Zaman damgası gömer mi? | Evet (48 bit ms) | Hayır | Evet (48 bit ms) |
| Standartlaştırılmış mı? | Topluluk spesifikasyonu | RFC 9562 | RFC 9562 |
| En iyi şu iş için | Kısa sıralanabilir kimlikler | Saydam olmayan rastgele kimlikler | UUID formatında sıralanabilir kimlikler |
Düzyazıyla: en kısa, URL güvenli, sıralanabilir karakter dizisini istediğinizde bir ULID’e uzanın. Saydam olmayan, gömülü zaman içermeyen, tamamen rastgele bir tanımlayıcı istediğinizde bir UUIDv4’e uzanın; örneğin ne zaman oluşturulduğunu açığa vurmak istemediğiniz herkese açık bir token. Zaman sıralamasına ihtiyaç duyduğunuzda ama standart UUID formatının içinde kalmak zorunda olduğunuzda, sürüm ve varyant bitleri sabit konumlarında ve içine bırakacağınız yerel bir uuid sütunuyla UUIDv7’ye uzanın.
Üçü de 128 bittir, bu yüzden ULID ↔ UUID dönüşümü her iki yönde de kayıpsızdır. ULID ile ulid vs uuid v7 arasındaki ilişki göründüğünden daha yakındır: UUIDv7, esasen ULID’in öncülük ettiği aynı zaman önekli fikrin IETF tarafından standartlaştırılmış halidir. UUID’lere tamamen yeniyseniz, önce temellerle başlayın, sonra bu karşılaştırmaya geri dönün.
Gizlilik dengesi: ULID’ler oluşturulma zamanlarını sızdırır
Gömülü zaman damgası, kimliği kimin okuduğuna bağlı olarak hem bir özellik hem bir sızıntıdır. Bir ULID’i elinde tutan herkes zaman damgasını çözebilir ve kaydın oluşturulduğu tam milisaniyeyi tek adımda, veritabanınıza erişmeye gerek kalmadan öğrenebilir.
Kendi sistemlerinizin içinde bu tamamen artıdır: anında denetim, bedava sıralama ve kolay hata ayıklama sağlar. Herkese açık bir tanımlayıcıda ise gerçek bir ifşadır. Oluşturulma zamanı tek başına iş açısından hassas olabilir ve zaman içinde örneklenen bir avuç ULID, oluşturma hızınızı, yani saniyede kaç sipariş, hesap veya mesaj ürettiğinizi sızdırır; bu da rakiplerin ve kazıyıcıların kestirmekten hoşlandığı türden bir şeydir.
Adil olmak gerekirse, bu, tarihsel olarak üreten makinenin MAC adresini gömen UUIDv1’den daha dar bir sızıntıdır; bir ULID yalnızca zamanı açığa çıkarır, asla donanım kimliğini değil. Yine de tartın. Basit önlem: ULID’leri dahili tutun ve sıralamanın önemli olmadığı herkese açık kimlikler için tamamen rastgele bir UUIDv4 verin.
ULID’lerle yaygın tuzaklar
Çoğu ULID sorunu, formattaki hatalar değil, kaçınılabilir bir avuç mühendislik kararıdır. Sıkça tekrarlananlar:
- Aynı milisaniyedeki düz ULID’lerin sıralı olduğunu varsaymak. Bir zaman önekini paylaşırlar ama bağımsız rastgele kuyrukları vardır, bu yüzden sıraları tanımsızdır. Düzeltme: milisaniye altı oranlarda kesin sıralamaya ihtiyaç duyduğunuzda monotonik modu kullanın.
- Bir ULID’i 26 karakterlik metin olarak saklamak. Bu, yer israf eder ve indeksi şişirir. Düzeltme: 128 biti 16 bayt olarak saklayın (
uuid/BINARY(16)) ve Base32’ye yalnızca kenarlarda kodlayın. - Bir ULID→UUID dönüşümünün v4 veya v7 olarak raporlanmasını beklemek. Dönüşüm aynı bitleri yeniden kodlar; UUID sürüm ve varyant alanlarını ayarlamaz, bu yüzden onları inceleyen bir kitaplık etiketlenmiş bir sürüm görmez. Düzeltme: sonucu saydam olmayan bir 128 bitlik değer olarak ele alın ya da etikete ihtiyacınız olduğunda gerçek bir UUIDv7 üretin.
- Rastgeleliği
Math.randomile doldurmak. Öngörülebilirdir ve çakışabilir. Düzeltme: daimacrypto.getRandomValuesgibi bir CSPRNG kullanın. - ULID’leri zaman damgası sızıntısını tartmadan herkese açık biçimde ifşa etmek. Yukarıdaki gizlilik bölümüne bakın. Düzeltme: dahili ULID’ler, herkese açık kimlikler için rastgele UUIDv4.
- Bir ULID’e elle
I,L,OveyaUyazmak. Bu harfler alfabede yoktur ve yeniden yazmak hatalara davet çıkarır. Düzeltme: ULID’leri kopyalayın, yeniden yazmayın.
SSS
ULID, UUID gibi resmî bir standart mı?
Hayır. ULID, bir IETF RFC’si değil, GitHub’da yayımlanan bir topluluk spesifikasyonudur. Yaygın olarak uygulanmış ve kararlıdır, ama arkasında bir standartlar kuruluşu yoktur. Standartlaştırılmış, zaman sıralı bir tanımlayıcıya ihtiyacınız varsa, UUIDv7 (RFC 9562) aynı fikri resmî UUID formatının içinde uygular.
Bir ULID kaç karakterdir ve neden bir UUID’den kısadır?
26 karakter, bir UUID’nin 36 karakterine karşı. ULID, karakter başına 5 bit paketleyen Crockford Base32 kullanır; bir UUID’nin onaltılık kodlaması yalnızca 4 bit paketler ve dört tire ekler. Dolayısıyla aynı 128 bit, Base32’de daha az karaktere ihtiyaç duyar ve hiçbiri URL kaçışı gerektirmez.
İki ULID hiç çakışabilir mi?
Pratikte asla. Bir milisaniye içinde bir ULID’in 80 rastgele biti vardır (yaklaşık 1,2 × 10²⁴ olasılık), bu yüzden milisaniye başına milyonlarca üretmek bile çakışma ihtimalini yok denecek kadar küçük tutar. Tek gereklilik, rastgeleliği kriptografik olarak güvenli bir RNG’nin doldurmasıdır; Math.random bu garantiyi geçersiz kılar.
ULID’leri PostgreSQL veya MySQL’de saklayabilir miyim?
Evet. Bir ULID 128 bittir, bu yüzden onu UUID biçimine dönüştürün ve bir uuid sütununda (PostgreSQL) veya BINARY(16) içinde (MySQL) saklayın, ardından Base32 karakter dizisini yalnızca kenarlarda işleyin. Yerel bir ULID sütun tipi yoktur, ama UUID gösterimi aynı 16 baytın maliyetine sahiptir ve indeksi kompakt tutar.
ULID’ler büyük/küçük harfe duyarlı mı?
Kanonik biçim büyük harftir, ama Crockford Base32 girdide büyük/küçük harfe duyarsızdır: bir çözücü küçük harfleri aynı şekilde okur ve I/L’yi 1’e, O’yu 0’a eşler. Eşitlik kontrollerinde ve indekslerde sürprizlerden kaçınmak için, saklamadan veya karşılaştırmadan önce tek bir harf durumuna normalleştirin.
48 bitlik zaman damgası hiç tükenecek mi?
Çok uzun bir süre tükenmeyecek. 48 bit milisaniye, sayaç taşmadan önce 10889 yılına ulaşır, bu yüzden zaman damgası bileşeni gerçek herhangi bir uygulama için fiilen geleceğe dayanıklıdır. Format yer tükenmeden çok önce sistemi, dili ve veritabanını değiştirmiş olacaksınız.
ULID’leri bir sunucu olmadan tarayıcıda veya mobilde üretebilir miyim?
Evet, bu temel bir avantajdır. ULID’lerin merkezî bir koordinatöre ihtiyacı yoktur, bu yüzden herhangi bir düğüm, edge worker, tarayıcı veya cihaz, saatinden ve güvenli bir RNG’den bir tane üretebilir. Farklı makinelerde oluşturulan değerler sonradan yine zamana göre birlikte sıralanır, çünkü zaman damgası kimliğin kendisinde yaşar.
Sonuç
ULID’ler belirli, gerçek bir sorunu, yani indeksinizi parçalayan rastgele anahtarları, merkezsizleştirilmiş üretimi elinizden almadan çözer. Mekaniği akılda tutmaya değer:
- Bir ULID, 48 bitlik milisaniye zaman damgası + 80 bitlik rastgeleliktir, 26 Crockford Base32 karakteri olarak kodlanır.
- Zamana göre sıralanır çünkü zaman damgası en anlamlı bileşendir ve alfabe sıra koruyucudur: karakter dizisi sırası, zaman sırasına eşittir.
- Bu sıralama, bir B-tree’ye rastgele bir UUIDv4’ün yoksun olduğu ekleme yerelliğini verir, yazma işlemlerini hızlı ve indeksi kompakt tutar.
- Aynı milisaniyede üretilen kimlikler için kesin sıralamaya ihtiyaç duyduğunuzda monotonik modu kullanın.
- ULID’leri herkese açık tanımlayıcılarda ifşa etmeden önce zaman damgası sızıntısını tartın.
- Standart UUID formatının içinde kalmak zorunda olduğunuzda bunun yerine UUIDv7’yi seçin.
Onu iş başında görmeye hazır olduğunuzda, ULID’leri tamamen tarayıcınızda üretmek, çözmek ve dönüştürmek için ULID üreticisini açın. Sunucuya gitmez, hiçbir şey yüklenmez ve hiçbir şey saklanmaz.