Skip to content
Kembali ke Blog
Tutorial

Apa Itu ULID? Panduan Pengidentifikasi yang Dapat Diurutkan

Apa itu ULID? Cara kerja pengidentifikasi 128-bit yang dapat diurutkan: struktur timestamp plus keacakan, encoding Crockford Base32, dan kapan memilihnya daripada UUID.

12 menit membaca

Apa Itu ULID? Pengidentifikasi Unik yang Dapat Diurutkan, Dijelaskan

Setiap UUIDv4 acak yang Anda sisipkan sebagai kunci primer (primary key) jatuh di posisi yang tidak terduga dalam indeks database. Lakukan itu beberapa juta kali dan indeks pun terfragmentasi, cache kacau, dan penulisan melambat. ULID memperbaiki hal ini tanpa mengorbankan apa yang Anda sukai dari UUID: Anda tetap bisa membuatnya di mana saja, tanpa koordinator pusat, tetapi ia jatuh dalam urutan waktu alih-alih berserakan.

Jadi, bagaimana sebuah string 26 karakter mengurutkan dirinya sendiri berdasarkan waktu? Itulah seluruh triknya, dan ada baiknya memahaminya sebelum Anda memutuskan untuk menggunakannya.

ULID (Universally Unique Lexicographically Sortable Identifier) adalah pengidentifikasi 128-bit yang ditulis sebagai 26 karakter Crockford Base32. 10 karakter pertama meng-encode timestamp dalam milidetik dan 16 karakter terakhir meng-encode bit acak, sehingga ULID yang dibuat belakangan selalu terurut setelah yang lebih dulu ketika dibandingkan sebagai string biasa. Ia adalah pengidentifikasi unik yang dapat diurutkan (sortable unique identifier) yang bisa Anda hasilkan secara offline.

Panduan ini membongkarnya: anatomi yang diuraikan karakter demi karakter, bukti bahwa ia benar-benar terurut, matematika B-tree di balik keunggulan database, dan pandangan jujur tentang apa yang dibocorkan oleh timestamp yang tertanam. Anda bisa mengikutinya dengan nilai langsung di generator ULID — buat satu, dekode, konversikan ke UUID — sambil membaca.

Apa Itu ULID?

ULID (Universally Unique Lexicographically Sortable Identifier) adalah pengidentifikasi 128-bit yang dirancang sebagai alternatif UUID yang lebih dapat diurutkan dan lebih ringkas. Ia ditulis sebagai 26 karakter Crockford Base32: 10 karakter pertama menampung timestamp 48-bit dalam milidetik sejak Unix epoch, dan 16 karakter sisanya menampung 80 bit keacakan (randomness). Karena waktu datang lebih dulu, string-nya terurut secara kronologis.

Properti terakhir itulah alasan format ini ada. UUIDv4 sepenuhnya acak, yang bagus untuk keunikan tetapi berarti dua ID yang dibuat berselang satu detik tidak punya hubungan satu sama lain. ULID mempertahankan model bebas-koordinasi, buat-di-mana-saja, dan menambahkan pengurutan waktu di atasnya, sehingga sebuah kolom berisi ULID secara alami terurut berdasarkan waktu pembuatan tanpa tambahan apa pun.

Berikut format-nya secara sekilas:

PropertiNilai
Bit128
Encoding26 karakter Crockford Base32
Tata letaktimestamp 48-bit + keacakan 80-bit

Sisa artikel ini menjelaskan cara kerja setiap bagian. Encoding dan kemampuan pengurutan masing-masing mendapat bagiannya sendiri nanti; pertama-tama, tata letaknya.

Anatomi ULID: 48 Bit Waktu + 80 Bit Keacakan

26 karakter ULID terbagi rapi menjadi dua bagian. 10 karakter pertama adalah timestamp; 16 karakter terakhir adalah bagian acak. Bentangkan contoh kanoniknya dan batasnya menjadi jelas:

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

Dua komponen, dua tugas. Yang satu mencatat kapan; yang lain menjamin keunikan. Berikut isi masing-masing bagian.

Timestamp 48-bit (10 karakter pertama)

10 karakter awal meng-encode sebuah integer 48-bit: jumlah milidetik sejak Unix epoch pada saat ULID dibuat. Ambil contoh kanonik langsung dari spesifikasinya:

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

Itu adalah dekode yang nyata dan dapat dibalik — tempelkan 01ARYZ6S41TSV4RRFFQ69G5FAV ke dalam decoder dan Anda mendapatkan persis 2016-07-30T22:36:16.385Z kembali. Komponen waktu adalah data biasa, bukan hash, jadi membacanya tidak memakan biaya apa pun.

Satu detail kecil yang sering mengecoh orang: karakter pertama sebuah ULID selalu antara 0 dan 7. Sebuah karakter Crockford menampung 5 bit, dan 48 bit bukan kelipatan 5 — timestamp menempati 48 bit terendah dari 50 bit yang bisa dibawa 10 karakter, menyisakan 2 bit teratas karakter pertama selamanya nol. Dua bit nol membatasi nilai karakter itu maksimal 7. Jika Anda pernah melihat ULID yang dimulai dengan 8 atau lebih tinggi, berarti ia cacat.

80 bit keacakan (16 karakter terakhir)

16 karakter sisanya membawa 80 bit keacakan, dan separuh inilah sumber keunikan. Bit-bit tersebut harus berasal dari sumber yang aman secara kriptografis — crypto.getRandomValues di browser, bukan Math.random. Perbedaannya penting: Math.random cukup mudah ditebak sehingga penyerang bisa menebak atau membenturkan (collide) nilai, sedangkan CSPRNG tidak.

Seberapa luas 80 bit itu? Kira-kira 1.2 × 10²⁴ kemungkinan nilai, dan itu per milidetik. Bahkan jika Anda membuat jutaan ULID dalam satu milidetik, peluang dua di antaranya menarik 80 bit yang sama tetap teramat kecil. Tidak seperti timestamp, separuh ini tidak membawa makna yang bisa didekode — ia adalah derau yang satu-satunya tujuannya adalah membuat setiap ULID berbeda.

Crockford Base32: Mengapa ULID Membuang I, L, O, dan U

ULID di-encode dengan Crockford Base32, sebuah alfabet berisi 32 simbol: angka 09 dan huruf AZ dengan empat huruf dihilangkan.

0123456789ABCDEFGHJKMNPQRSTVWXYZ

Huruf yang hilang adalah I, L, O, dan U. Tiga di antaranya dibuang karena mirip angka — I dan L menyerupai 1, O menyerupai 0 — sehingga manusia yang membaca ULID di layar tidak bisa salah mengira huruf sebagai angka. Sisi lainnya adalah input yang toleran: decoder yang patuh memetakan I dan L kembali ke 1 dan O ke 0, serta memperlakukan seluruh string tanpa membedakan huruf besar/kecil. U dikecualikan secara terpisah, untuk menghindari secara tidak sengaja mengeja kata-kata yang menyinggung.

Matematika bit adalah alasan lainnya. Setiap karakter Base32 meng-encode 5 bit, sementara karakter heksadesimal hanya meng-encode 4 bit. Kemas 128 bit dengan 5 bit per karakter dan Anda butuh 26; kemas 128 bit yang sama dengan 4 bit masing-masing — sebagaimana cara UUID — dan Anda butuh 32, ditambah empat tanda hubung, menjadi 36 karakter. Jadi ULID jauh lebih pendek daripada UUID dan, tanpa tanda hubung, langsung masuk ke dalam URL, nama file, atau header tanpa perlu escaping.

Crockford Base32 adalah alfabet berisi 32 simbol (09 dan AZ dikurangi I, L, O, U) yang meng-encode 5 bit per karakter. ULID menggunakannya untuk mengemas 128 bit ke dalam 26 karakter yang case-insensitive dan aman-URL, dan — yang krusial — alfabet ini tersusun secara menaik, dan itulah yang membuat string ter-encode terurut sama persis seperti bit mentahnya.

Mengapa ULID Terurut Berdasarkan Waktu

Banyak artikel memberi tahu Anda bahwa ULID terurut berdasarkan waktu. Lebih sedikit yang menunjukkan mengapa. Alasannya bertumpu pada dua fakta yang sudah Anda miliki: timestamp adalah bagian paling signifikan dari nilai itu, dan alfabet Crockford tersusun secara menaik.

Gabungkan keduanya dan Anda mendapatkan rantai kesetaraan:

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

Bacalah dari kiri ke kanan. Membandingkan dua ULID karakter demi karakter (cara kerja pengurutan string) memberi jawaban yang sama dengan membandingkan integer 128-bit di baliknya, karena alfabetnya mempertahankan urutan — karakter yang “lebih tinggi” selalu berarti nilai yang lebih tinggi. Membandingkan integer 128-bit memberi jawaban yang sama dengan membandingkan waktu pembuatan, karena timestamp berada di bit paling signifikan, sehingga ia mendominasi perbandingan; ekor acak hanya memecah seri di dalam milidetik yang sama. Urutan string, urutan bit, dan urutan waktu adalah urutan yang sama.

Sebuah demonstrasi cepat. Dua ULID yang dibuat berselang satu milidetik:

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

Karakter kesepuluh berdetak dari 1 ke 2, dan pengurutan teks biasa menempatkan yang kedua setelah yang pertama — tanpa kolom timestamp, tanpa pembanding khusus. Hasil praktisnya, yang akan diperluas di bagian berikutnya, hanya satu baris: ORDER BY id mengembalikan baris dalam urutan kronologis tanpa indeks tambahan.

ULID sebagai Kunci Primer Database: Lokalitas B-Tree

Di sinilah ULID membuktikan nilainya. Sebagian besar database relasional menyimpan indeks kunci primer sebagai B-tree, dan di mana sebuah kunci baru jatuh dalam pohon itu menentukan seberapa mahal penyisipannya.

UUIDv4 acak jatuh di tempat yang tidak terduga pada setiap penyisipan:

UUIDv4: setiap kunci baru menyasar leaf page acak. Page itu sering penuh, jadi mesin memecahnya, menyalin separuh baris ke tempat lain, dan mengotori page di seantero pohon. Pada jutaan baris hal ini memfragmentasi indeks, menggusur page yang berguna dari buffer cache, dan menurunkan throughput penyisipan. (Untuk angka pemecahan page indeks yang konkret — biasanya selisih 2–10× pada tabel yang padat-tulis — lihat panduan perbandingan.)

ULID berawalan-waktu selalu jatuh di ujung:

ULID: karena bit teratas adalah timestamp, setiap kunci baru lebih besar dari yang terakhir, jadi ia ditambahkan di atau dekat tepi kanan indeks. Penyisipan tetap berurutan, pemecahan page nyaris hilang, indeks tetap ringkas, dan range scan atas suatu rentang waktu membaca rangkaian page yang berdekatan.

Anda mendapatkan pembuatan bebas-koordinasi ala UUID dengan lokalitas penyisipan ala integer auto-increment — tanpa memaparkan penghitung berurutan yang mudah ditebak, karena ekor acak tetap menyembunyikan nilai persis berikutnya.

Tip penyimpanan: simpan 128 bit sebagai 16 byte biner — kolom uuid di PostgreSQL, BINARY(16) di MySQL — bukan sebagai field teks 26 karakter, yang memboroskan ruang dan menggelembungkan indeks. Encode ke string Base32 hanya di tepi tempat manusia atau URL melihatnya. Tab Convert pada generator akan mengonversi ULID menjadi UUID untuk keperluan persis ini, karena kedua bentuk itu adalah 128 bit yang sama.

ULID Monotonik: Urutan Ketat dalam Satu Milidetik

Bukti kemampuan pengurutan punya satu celah jujur: dalam satu milidetik, ULID biasa tidak terurut secara ketat. Mereka berbagi awalan waktu 10 karakter yang sama, tetapi ekor acak 80-bit mereka ditarik secara independen, jadi ULID semilidetik mana yang terurut lebih dulu pada dasarnya seperti lemparan koin. Untuk sebagian besar penggunaan, itu tidak masalah. Saat Anda butuh urutan ketat bahkan pada laju sub-milidetik, itu menjadi masalah.

Pembuatan monotonik (monotonic) menutup celah itu. Aturannya sederhana: ULID pertama dalam suatu milidetik mendapat keacakan segar seperti biasa, dan setiap ULID berikutnya dalam milidetik yang sama dihasilkan dengan mengambil nilai acak 80-bit sebelumnya dan menambahkannya satu (diperlakukan sebagai integer big-endian, membawa ke bit yang lebih tinggi sesuai kebutuhan). Setiap nilai karena itu secara ketat lebih besar dari sebelumnya.

Anda bisa melihatnya pada sekumpulan yang dihasilkan dalam satu milidetik — hanya karakter terakhir yang bergerak:

01KVT0F720ZK9N4T2QX7VR8WMC
01KVT0F720ZK9N4T2QX7VR8WMD
01KVT0F720ZK9N4T2QX7VR8WME

…WMC < …WMD < …WME, dijamin. Hal ini penting setiap kali baris bisa dibuat lebih cepat daripada detak jam milidetik: penyisipan throughput tinggi, log peristiwa, ID pesan dalam loop yang rapat. Ketika jam maju ke milidetik berikutnya, pembuatan kembali ke keacakan segar dan siklusnya berulang.

ULID vs UUID: Kapan Memakai yang Mana

Pertanyaan yang dibawa kebanyakan orang adalah ULID vs UUID. Berikut perbandingan terfokusnya: ULID melawan dua versi UUID yang realistis Anda timbang. (Untuk matriks keputusan lima-arah lengkap termasuk Snowflake dan NanoID, lihat perbandingan lengkap ULID, UUID, dan Snowflake.)

PropertiULIDUUIDv4UUIDv7
Panjang26 karakter36 karakter36 karakter
EncodingCrockford Base32Heksa dengan tanda hubungHeksa dengan tanda hubung
Dapat diurutkan berdasarkan waktu?YaTidakYa
Menanamkan timestamp?Ya (48-bit ms)TidakYa (48-bit ms)
Distandardisasi?Spesifikasi komunitasRFC 9562RFC 9562
Paling cocok untukID singkat yang dapat diurutkanID acak yang opakID dapat diurutkan dalam format UUID

Dalam prosa: pilih ULID ketika Anda ingin string yang paling pendek, aman-URL, dan dapat diurutkan. Pilih UUIDv4 ketika Anda ingin pengidentifikasi yang opak dan sepenuhnya acak tanpa waktu tertanam — misalnya token publik yang lebih baik tidak mengungkap kapan ia dibuat. Pilih UUIDv7 ketika Anda butuh pengurutan waktu tetapi harus tetap di dalam format UUID standar, dengan bit version dan variant pada posisi tetapnya serta kolom uuid native untuk menampungnya.

Ketiganya berukuran 128 bit, jadi konversi ULID ↔ UUID tidak menghilangkan apa pun ke arah mana pun. Hubungan antara ULID dan ulid vs uuid v7 lebih dekat daripada yang terlihat: UUIDv7 pada dasarnya adalah versi yang distandardisasi IETF dari gagasan berawalan-waktu yang sama yang dipelopori ULID. Jika Anda baru mengenal UUID sama sekali, mulailah dari dasar-dasarnya dulu, lalu kembali ke perbandingan ini.

Pertukaran Privasi: ULID Membocorkan Waktu Pembuatannya

Timestamp yang tertanam adalah fitur sekaligus kebocoran, tergantung siapa yang membaca ID-nya. Siapa pun yang memegang ULID bisa mendekode timestamp dalam satu langkah dan mengetahui milidetik persis saat record dibuat — tanpa perlu akses ke database Anda.

Di dalam sistem Anda sendiri itu murni keuntungan: audit instan, pengurutan gratis, debugging mudah. Pada pengidentifikasi yang menghadap publik, itu adalah pengungkapan yang nyata. Waktu pembuatan bisa sensitif secara bisnis dengan sendirinya, dan segenggam ULID yang diambil sampel dari waktu ke waktu membocorkan laju pembuatan Anda — berapa banyak pesanan, akun, atau pesan yang Anda buat per detik — hal semacam yang gemar diperkirakan pesaing dan scraper.

Untuk adilnya, ini kebocoran yang lebih sempit daripada UUIDv1, yang secara historis menanamkan alamat MAC mesin yang menghasilkan; sebuah ULID hanya memaparkan waktu, tidak pernah identitas perangkat keras. Tetap saja, timbanglah. Mitigasi sederhananya: jaga ULID tetap internal dan berikan UUIDv4 yang sepenuhnya acak untuk ID yang menghadap publik di mana pengurutan tidak penting.

Jebakan Umum dengan ULID

Sebagian besar masalah ULID adalah segelintir keputusan rekayasa yang bisa dihindari, bukan bug dalam format. Yang sering berulang:

  • Mengasumsikan ULID biasa semilidetik sudah terurut. Mereka berbagi awalan waktu tetapi memiliki ekor acak yang independen, jadi urutannya tidak terdefinisi. Perbaikan: gunakan mode monotonik ketika Anda butuh pengurutan ketat pada laju sub-milidetik.
  • Menyimpan ULID sebagai teks 26 karakter. Itu memboroskan ruang dan menggelembungkan indeks. Perbaikan: simpan 128 bit sebagai 16 byte (uuid / BINARY(16)) dan encode ke Base32 hanya di tepi.
  • Mengharapkan konversi ULID→UUID dilaporkan sebagai v4 atau v7. Konversi meng-encode ulang bit yang sama; ia tidak menyetel field version dan variant UUID, jadi library yang memeriksanya tidak akan melihat versi yang bertanda. Perbaikan: perlakukan hasilnya sebagai nilai 128-bit yang opak, atau hasilkan UUIDv7 sungguhan ketika Anda butuh tanda itu.
  • Mengisi keacakan dengan Math.random. Ia mudah ditebak dan bisa bertabrakan (collide). Perbaikan: selalu gunakan CSPRNG seperti crypto.getRandomValues.
  • Memaparkan ULID secara publik tanpa menimbang kebocoran timestamp. Lihat bagian privasi di atas. Perbaikan: ULID internal, UUIDv4 acak untuk ID publik.
  • Mengetik manual I, L, O, atau U ke dalam ULID. Huruf-huruf itu tidak ada dalam alfabet, dan mengetik ulang mengundang kesalahan. Perbaikan: salin ULID, jangan ketik ulang.

FAQ

Apakah ULID standar resmi seperti UUID?

Tidak. ULID adalah spesifikasi komunitas yang dipublikasikan di GitHub, bukan RFC IETF. Ia diimplementasikan secara luas dan stabil, tetapi tidak ada badan standar di belakangnya. Jika Anda butuh pengidentifikasi terurut-waktu yang distandardisasi, UUIDv7 (RFC 9562) menerapkan gagasan yang sama di dalam format UUID resmi.

Berapa karakter sebuah ULID, dan mengapa lebih pendek dari UUID?

26 karakter, dibandingkan 36 milik UUID. ULID menggunakan Crockford Base32, yang mengemas 5 bit per karakter; heksadesimal UUID hanya mengemas 4 bit dan menambahkan empat tanda hubung. 128 bit yang sama karena itu membutuhkan lebih sedikit karakter dalam Base32 — dan tak satu pun di antaranya perlu URL escaping.

Mungkinkah dua ULID bertabrakan?

Praktis tidak pernah. Dalam satu milidetik sebuah ULID memiliki 80 bit acak — sekitar 1.2 × 10²⁴ kemungkinan — jadi bahkan menghasilkan jutaan per milidetik tetap menjaga peluang tabrakan (collision) teramat kecil. Satu syaratnya adalah RNG yang aman secara kriptografis mengisi keacakannya; Math.random membatalkan jaminan itu.

Bisakah saya menyimpan ULID di PostgreSQL atau MySQL?

Bisa. Sebuah ULID berukuran 128 bit, jadi konversikan ke bentuk UUID dan simpan di kolom uuid (PostgreSQL) atau BINARY(16) (MySQL), lalu render string Base32 hanya di tepi. Tidak ada tipe kolom ULID native, tetapi representasi UUID memakan 16 byte yang sama dan menjaga indeks tetap ringkas.

Apakah ULID peka huruf besar/kecil (case-sensitive)?

Bentuk kanoniknya adalah huruf besar, tetapi Crockford Base32 tidak peka huruf besar/kecil pada input: decoder membaca huruf kecil dengan cara yang sama, dan memetakan I/L ke 1 dan O ke 0. Untuk menghindari kejutan dalam pemeriksaan kesetaraan dan indeks, normalkan ke satu format huruf sebelum Anda menyimpan atau membandingkan.

Apakah timestamp 48-bit akan pernah habis?

Tidak untuk waktu yang sangat lama. 48 bit milidetik mencapai tahun 10889 sebelum penghitung meluap (overflow), jadi komponen timestamp efektif tahan-masa-depan untuk aplikasi nyata apa pun. Anda akan mengganti sistem, bahasa, dan database jauh sebelum format ini kehabisan ruang.

Bisakah saya menghasilkan ULID di browser atau perangkat seluler tanpa server?

Bisa — itu manfaat intinya. ULID tidak butuh koordinator pusat, jadi node, edge worker, browser, atau perangkat apa pun bisa membuatnya dari jamnya plus RNG yang aman. Nilai yang dibuat pada mesin berbeda tetap terurut bersama berdasarkan waktu setelahnya, karena timestamp hidup di dalam ID itu sendiri.

Kesimpulan

ULID memecahkan masalah spesifik dan nyata — kunci acak memfragmentasi indeks Anda — tanpa menghilangkan pembuatan yang terdesentralisasi. Mekanismenya layak diingat:

  • ULID adalah timestamp milidetik 48-bit + 80 bit keacakan, di-encode sebagai 26 karakter Crockford Base32.
  • Ia terurut berdasarkan waktu karena timestamp adalah komponen paling signifikan dan alfabetnya mempertahankan urutan — urutan string sama dengan urutan waktu.
  • Pengurutan itu memberi B-tree lokalitas penyisipan yang tidak dimiliki UUIDv4 acak, menjaga penulisan tetap cepat dan indeks tetap ringkas.
  • Gunakan mode monotonik ketika Anda butuh pengurutan ketat untuk ID yang dibuat dalam milidetik yang sama.
  • Timbang kebocoran timestamp sebelum memaparkan ULID pada pengidentifikasi yang menghadap publik.
  • Pilih UUIDv7 sebagai gantinya ketika Anda harus tetap di dalam format UUID standar.

Saat Anda siap mempraktikkannya, buka generator ULID untuk menghasilkan, mendekode, dan mengonversi ULID sepenuhnya di browser Anda — tanpa server, tanpa unggahan, tidak ada yang disimpan.

Tag: ulid uuid unique-identifier database primary-key

Artikel Terkait

Lihat semua artikel