UUID vs ULID vs ID serial: pelajari bagaimana masing‑masing memengaruhi pengindeksan, pengurutan, sharding, serta ekspor dan impor data yang aman dalam proyek nyata.

Pilihan ID terasa membosankan di minggu pertama. Lalu Anda rilis, data tumbuh, dan keputusan "sederhana" itu muncul di mana-mana: indeks, URL, log, ekspor, dan integrasi.
Pertanyaan sebenarnya bukan "mana yang terbaik?" Melainkan "sakit mana yang ingin Anda hindari nanti?" ID sulit diubah karena mereka disalin ke tabel lain, di-cache oleh klien, dan menjadi dependensi sistem lain.
Saat ID tidak cocok dengan bagaimana produk berkembang, biasanya terlihat di beberapa tempat:
Selalu ada trade-off antara kenyamanan sekarang dan fleksibilitas nanti. Integer serial mudah dibaca dan sering cepat, tapi bisa membocorkan jumlah record dan mempersulit penggabungan dataset. UUID acak bagus untuk keunikan antar sistem, tapi kurang ramah pada indeks dan susah dibaca manusia. ULID bertujuan untuk keunikan global dengan pengurutan "sekitar waktu", namun tetap punya trade-off penyimpanan dan tooling.
Cara yang berguna untuk memikirkannya: untuk siapa ID itu terutama dibuat?
Jika ID terutama untuk manusia (support, debugging, ops), yang lebih pendek dan gampang dipindai cenderung menang. Jika untuk mesin (penulisan terdistribusi, klien offline, sistem multi-region), keunikan global dan penghindaran tabrakan lebih penting.
Saat orang berdebat "UUID vs ULID vs serial IDs," sebenarnya mereka memilih bagaimana setiap baris diberi label unik. Label itu memengaruhi kemudahan insert, sortir, merge, dan pemindahan data nanti.
ID serial adalah penghitung. Basis data memberikan 1, lalu 2, lalu 3, dan seterusnya (sering disimpan sebagai integer atau bigint). Mudah dibaca, murah disimpan, dan biasanya cepat karena baris baru berada di akhir indeks.
UUID adalah identifier 128-bit yang tampak acak, seperti 3f8a.... Dalam banyak pengaturan ia dapat dihasilkan tanpa meminta database untuk nomor berikutnya, sehingga sistem berbeda bisa membuat ID secara independen. Tradeoff-nya adalah insert yang tampak acak dapat membuat indeks bekerja lebih keras dan mengambil lebih banyak ruang dibanding bigint sederhana.
ULID juga 128-bit, tapi dirancang agar kurang lebih berurutan menurut waktu. ULID yang lebih baru biasanya akan mengurut setelah yang lebih lama, sambil tetap unik secara global. Anda sering mendapatkan sebagian manfaat "dapat digenerate di mana saja" seperti UUID dengan perilaku sortir yang lebih ramah.
Ringkasan singkat:
ID serial umum untuk aplikasi satu-database dan alat internal. UUID muncul ketika data dibuat lintas layanan, perangkat, atau region. ULID populer ketika tim ingin generasi ID terdistribusi tapi masih peduli pada urutan, paginasi, atau query "terbaru dulu".
Primary key biasanya didukung oleh sebuah indeks (sering B-tree). Bayangkan indeks itu seperti buku telepon yang terurut: setiap baris baru perlu entri ditempatkan di posisi yang tepat agar lookup tetap cepat.
Dengan ID acak (UUIDv4 klasik), entri baru mendarat di seluruh indeks. Itu berarti database menyentuh banyak halaman indeks, sering melakukan split halaman, dan menulis lebih banyak. Seiring waktu Anda mendapatkan churn indeks: lebih banyak kerja per insert, lebih banyak cache miss, dan indeks yang lebih besar dari yang diharapkan.
Dengan ID yang hampir selalu meningkat (serial/bigint, atau ID yang terurut waktu seperti banyak ULID), database biasanya dapat menambahkan entri baru di akhir indeks. Ini lebih ramah cache karena halaman terbaru tetap "hot", dan insert cenderung lebih mulus pada laju tulis tinggi.
Ukuran kunci penting karena entri indeks tidak gratis:
Kunci yang lebih besar berarti lebih sedikit entri muat per halaman indeks. Itu sering mengarah ke indeks yang lebih dalam, lebih banyak halaman dibaca per query, dan lebih banyak RAM yang dibutuhkan agar tetap cepat.
Jika Anda punya tabel "events" dengan insert konstan, primary key UUID acak bisa mulai terasa lebih lambat lebih cepat daripada kunci bigint, meskipun lookup baris tunggal masih terlihat bagus. Jika Anda mengharapkan penulisan berat, biaya pengindeksan biasanya menjadi perbedaan nyata pertama yang Anda rasakan.
Jika Anda pernah membuat "Load more" atau infinite scroll, Anda sudah merasakan sakitnya ID yang tidak terurut dengan baik. ID "terurut baik" ketika pengurutan berdasarkan ID memberikan urutan yang stabil dan bermakna (seringnya waktu pembuatan) sehingga paginasi dapat diprediksi.
Dengan ID acak (seperti UUIDv4), baris yang lebih baru tersebar. Mengurutkan berdasarkan id tidak sesuai dengan waktu, dan paginasi kursor seperti "berikan item setelah id ini" menjadi tidak dapat diandalkan. Anda biasanya kembali menggunakan created_at, yang baik, tapi harus dilakukan dengan hati-hati.
ULID dirancang agar kurang lebih berurutan menurut waktu. Jika Anda mengurutkan berdasarkan ULID (sebagai string atau dalam bentuk biner), item yang lebih baru cenderung datang belakangan. Itu membuat paginasi kursor lebih sederhana karena kursor bisa berupa ULID terakhir yang terlihat.
ULID membantu dengan pengurutan "sekitar waktu" untuk feed, kursor yang lebih sederhana, dan pengurangan perilaku insert acak dibanding UUIDv4.
Tapi ULID tidak menjamin urutan waktu yang sempurna ketika banyak ID dibuat dalam milidetik yang sama di banyak mesin. Jika Anda butuh pengurutan yang pasti, Anda tetap menginginkan timestamp nyata.
created_at masih lebih baikMengurutkan berdasarkan created_at sering lebih aman ketika Anda backfill data, mengimpor record historis, atau butuh tie-breaking yang jelas.
Polanya praktis adalah mengurutkan dengan (created_at, id), di mana id hanya sebagai pemecah seri.
Sharding berarti membagi satu basis data menjadi beberapa lebih kecil sehingga setiap shard menampung sebagian data. Tim biasanya melakukan ini nanti, ketika satu basis data sulit diskalakan atau menjadi titik kegagalan tunggal yang berisiko.
Pilihan ID Anda bisa membuat sharding mudah atau menyakitkan.
Dengan ID sekuensial (auto-increment serial atau bigint), setiap shard akan dengan senang hati menghasilkan 1, 2, 3.... ID yang sama bisa ada di beberapa shard. Saat Anda perlu menggabungkan data, memindahkan baris, atau membangun fitur lintas-shard, Anda akan menghadapi tabrakan.
Anda bisa menghindari tabrakan dengan koordinasi (layanan ID pusat, atau rentang per shard), tapi itu menambah komponen dan bisa menjadi bottleneck.
UUID dan ULID mengurangi kebutuhan koordinasi karena setiap shard dapat menghasilkan ID secara independen dengan risiko duplikasi yang sangat kecil. Jika Anda berpikir akan memecah data antar basis data, ini salah satu argumen terkuat melawan sequence murni.
Kompromi umum adalah menambahkan prefix shard lalu menggunakan sequence lokal di setiap shard. Anda bisa menyimpannya sebagai dua kolom, atau mengemasnya ke dalam satu nilai.
Itu bekerja, tapi menciptakan format ID kustom. Semua integrasi harus memahaminya, pengurutan berhenti bermakna tanpa logika tambahan, dan memindahkan data antar shard dapat memerlukan penulisan ulang ID (yang memecahkan referensi jika ID tersebut dibagikan).
Tanyakan satu hal lebih awal: apakah Anda akan pernah perlu menggabungkan data dari beberapa basis data dan menjaga referensi tetap stabil? Jika ya, rencanakan ID unik global dari hari pertama, atau siapkan anggaran untuk migrasi nanti.
Ekspor dan impor adalah saat pilihan ID berhenti menjadi teori. Begitu Anda mengkloning prod ke staging, mengembalikan backup, atau menggabungkan data dari dua sistem, Anda akan tahu apakah ID Anda stabil dan portabel.
Dengan ID serial (auto-increment), Anda biasanya tidak bisa menjalankan ulang insert ke basis data lain dan mengharapkan referensi tetap utuh kecuali Anda mempertahankan nomor asli. Jika Anda mengimpor hanya subset baris (misalnya 200 pelanggan dan order mereka), Anda harus memuat tabel dalam urutan yang benar dan menjaga primary key persis sama. Jika ada yang dinomori ulang, foreign key akan rusak.
UUID dan ULID dihasilkan di luar sequence database, jadi lebih mudah dipindahkan antar lingkungan. Anda bisa menyalin baris, menjaga ID, dan hubungan tetap cocok. Ini membantu saat Anda mengembalikan dari backup, melakukan ekspor parsial, atau menggabungkan dataset.
Contoh: ekspor 50 akun dari produksi untuk debug di staging. Dengan primary key UUID/ULID, Anda dapat mengimpor akun tersebut plus baris terkait (project, invoice, log) dan semuanya tetap menunjuk ke induk yang benar. Dengan ID serial, Anda sering membuat tabel translasi (old_id -> new_id) dan menulis ulang foreign key saat impor.
Untuk impor bulk, dasar-dasarnya lebih penting daripada tipe ID:
Anda bisa membuat keputusan solid dengan cepat jika fokus pada apa yang akan menyakiti nanti.
Tuliskan risiko masa depan utama Anda. Peristiwa konkret membantu: memecah ke beberapa basis data, menggabungkan data pelanggan dari sistem lain, penulisan offline, salinan data antar lingkungan.
Putuskan apakah urutan ID harus cocok dengan waktu. Jika Anda ingin "terbaru dulu" tanpa kolom tambahan, ULID (atau ID yang dapat diurutkan menurut waktu) cocok. Jika Anda tidak keberatan mengurutkan dengan created_at, UUID dan serial sama-sama bekerja.
Perkirakan volume tulis dan sensitivitas indeks. Jika Anda mengharapkan banyak insert dan indeks primary key yang paling sering dipukul, serial BIGINT biasanya paling mudah buat B-tree. UUID acak cenderung menyebabkan lebih banyak churn.
Pilih default, lalu dokumentasikan pengecualian. Sederhanakan: satu default untuk sebagian besar tabel, dan aturan jelas kapan menyimpang (sering: ID publik vs ID internal).
Sisakan ruang untuk berubah. Hindari meng-encode makna ke dalam ID, putuskan di mana ID dihasilkan (DB vs aplikasi), dan buat constraint eksplisit.
Kesalahan terbesar adalah memilih ID karena populer, lalu menemukan ia bertabrakan dengan cara Anda query, skala, atau berbagi data. Sebagian besar masalah muncul berbulan-bulan kemudian.
Kegagalan umum:
123, 124, 125, orang bisa menebak record di dekatnya dan memeriksa sistem Anda.Tanda peringatan yang harus Anda tangani sejak awal:
Pilih satu tipe primary key dan konsistenkan di sebagian besar tabel. Mencampur tipe (bigint di satu tempat, UUID di tempat lain) membuat join, API, dan migrasi lebih sulit.
Perkirakan ukuran indeks pada skala yang Anda harapkan. Kunci yang lebih lebar berarti primary index lebih besar dan lebih banyak IO serta memori.
Putuskan bagaimana Anda akan membuat paginasi. Jika Anda paginasi berdasarkan ID, pastikan ID memiliki urutan yang dapat diprediksi (atau terima bahwa tidak). Jika paginasi berdasarkan timestamp, index created_at dan gunakan secara konsisten.
Uji rencana impor Anda pada data yang menyerupai produksi. Verifikasi bahwa Anda dapat merekonstruksi record tanpa merusak foreign key dan bahwa re-import tidak diam-diam menghasilkan ID baru.
Tuliskan strategi tabrakan Anda. Siapa yang menghasilkan ID (DB atau aplikasi), dan apa yang terjadi jika dua sistem membuat record offline lalu sinkronisasi?
Pastikan URL publik dan log Anda tidak membocorkan pola yang Anda pedulikan (jumlah record, laju pembuatan, petunjuk shard internal). Jika Anda menggunakan ID serial, anggap orang bisa menebak ID di dekatnya.
Seorang pendiri solo meluncurkan CRM sederhana: contacts, deals, notes. Satu Postgres, satu web app, dan tujuan utama adalah segera rilis.
Awalnya, primary key bigint serial terasa sempurna. Insert cepat, indeks rapi, dan mudah dibaca di log.
Setahun kemudian, seorang pelanggan meminta ekspor kuartalan untuk audit, dan pendiri mulai mengimpor leads dari alat pemasaran. ID yang sebelumnya hanya internal kini muncul di CSV, email, dan tiket support. Jika dua sistem sama-sama menggunakan 1, 2, 3..., penggabungan menjadi berantakan. Anda berakhir menambahkan kolom sumber, tabel pemetaan, atau menulis ulang ID saat impor.
Pada tahun kedua, ada aplikasi mobile yang perlu membuat record saat offline, lalu sinkronisasi nanti. Sekarang Anda butuh ID yang bisa dihasilkan di klien tanpa bicara ke database, dan risiko tabrakan rendah ketika data masuk dari lingkungan berbeda.
Kompromi yang sering bertahan:
Jika Anda ragu antara UUID, ULID, dan serial IDs, putuskan berdasarkan bagaimana data Anda akan bergerak dan tumbuh.
Pilihan singkat satu kalimat untuk kasus umum:
bigint serial.Mencampur sering jadi jawaban terbaik. Gunakan serial bigint untuk tabel internal yang tidak pernah keluar dari database (join table, job background), dan gunakan UUID/ULID untuk entitas publik seperti users, orgs, invoice, dan apa pun yang mungkin Anda ekspor, sinkronkan, atau referensikan dari layanan lain.
Jika Anda membangun di Koder.ai (koder.ai), ada baiknya menentukan pola ID sebelum menghasilkan banyak tabel dan API. Mode perencanaan platform dan snapshot/rollback memudahkan menerapkan dan memvalidasi perubahan skema lebih awal, sementara sistem masih cukup kecil untuk diubah dengan aman.
Mulailah dengan rasa sakit masa depan yang ingin Anda hindari: insert yang melambat karena penulisan indeks acak, paginasi yang canggung, migrasi berisiko, atau tabrakan ID saat impor dan penggabungan. Jika Anda memperkirakan data akan berpindah antar sistem atau dibuat di banyak tempat, pilih ID yang unik secara global (UUID/ULID) dan pisahkan masalah pengurutan waktu.
Serial bigint adalah pilihan kuat ketika Anda punya satu basis data, tulisannya padat, dan ID hanya dipakai secara internal. Ukurannya kompak, cepat untuk indeks B-tree, dan mudah dibaca di log. Kekurangannya: sulit digabungkan nanti tanpa tabrakan, dan bisa membocorkan jumlah record jika dipublikasikan.
Pilih UUID ketika record bisa dibuat di banyak layanan, region, perangkat, atau klien offline dan Anda ingin risiko tabrakan yang sangat kecil tanpa koordinasi. UUID juga cocok sebagai ID publik karena sulit ditebak. Tradeoff-nya adalah indeks lebih besar dan pola insert lebih acak dibanding kunci sekuensial.
ULID masuk akal ketika Anda mau ID yang bisa dibuat di mana saja dan umumnya terurut menurut waktu pembuatan. Ini mempermudah paginasi kursor dan mengurangi masalah "insert acak" yang sering ditemui pada UUIDv4. Namun ULID bukan pengganti timestamp yang presisi; gunakan created_at jika Anda butuh urutan yang ketat atau keamanan backfill.
Ya, terutama pada tabel dengan banyak penulisan. UUIDv4 yang acak membuat insert tersebar di seluruh indeks primary key, menyebabkan lebih banyak page split, cache churn, dan indeks yang lebih besar seiring waktu. Seringkali terlihat pertama kali sebagai penurunan throughput insert berkelanjutan dan kebutuhan memori/IO yang meningkat, bukan sebagai lookup baris tunggal yang lambat.
Mengurutkan berdasarkan ID acak (seperti UUIDv4) tidak akan mencerminkan waktu pembuatan, jadi kursor "setelah id ini" tidak menghasilkan timeline yang stabil. Perbaikan yang andal adalah paginasi berdasarkan created_at dan menambahkan ID sebagai pemecah seri, misalnya (created_at, id). Jika Anda ingin paginasi hanya berdasarkan ID, ID yang dapat diurutkan berdasarkan waktu seperti ULID biasanya lebih sederhana.
ID sekuensial tabrakan di beberapa shard karena setiap shard akan menghasilkan 1, 2, 3... secara independen. Anda bisa menghindarinya dengan koordinasi (range per shard atau layanan ID pusat), tapi itu menambah kompleksitas operasional dan bisa menjadi bottleneck. UUID/ULID mengurangi kebutuhan koordinasi karena setiap shard bisa menghasilkan ID sendiri dengan aman.
UUID/ULID lebih mudah untuk ekspor dan impor karena Anda dapat menyalin baris dan menjaga ID sehingga relasi tetap utuh tanpa renumerasi. Dengan ID serial, impor parsial sering memerlukan tabel translasi (old_id -> new_id) dan penulisan ulang foreign key, yang rentan kesalahan. Jika Anda sering menyalin lingkungan atau menggabungkan dataset, ID unik global menghemat banyak waktu.
Polanya umum: dua ID. Kunci primer internal yang ringkas (serial bigint) untuk join dan efisiensi penyimpanan, plus ID publik tak berubah (ULID atau UUID) untuk URL, API, ekspor, dan referensi lintas-sistem. Ini menjaga database tetap cepat sambil mempermudah integrasi dan migrasi. Penting: anggap ID publik sebagai stabil dan jangan pernah mendaur ulang atau menginterpretasikannya kembali.
Rencanakan lebih awal dan terapkan konsisten di seluruh tabel dan API. Di Koder.ai (koder.ai), tentukan strategi ID default di mode perencanaan sebelum menghasilkan banyak skema dan endpoint, lalu gunakan snapshot/rollback untuk memvalidasi perubahan saat proyek masih kecil. Bagian tersulit bukan membuat ID baru—melainkan memperbarui foreign key, cache, log, dan integrasi eksternal yang masih merujuk ID lama.