💡 Key Takeaways
- The "I'll Just Use UUIDs Everywhere" Disaster
- Premature Normalization: When Third Normal Form Becomes Your Enemy
- The NULL Nightmare: When Optional Becomes Impossible
- Index Overload: When More Isn't Better
Tiga tahun yang lalu, saya melihat basis data startup kami berhenti berfungsi pada pukul 2:47 pagi di Black Friday. Kami memiliki 50.000 pengguna bersamaan, $2 juta dalam transaksi yang tertunda, dan kueri yang membutuhkan waktu 45 detik untuk mengembalikan ketersediaan produk. CTO kami berteriak di Slack. Para investor menelepon. Dan saya hanya bisa menatap skema yang saya desain enam bulan sebelumnya, menyadari bahwa setiap keputusan "cerdas" yang saya buat sekarang costing kami sekitar $8.000 per menit dalam pendapatan yang hilang.
💡 Poin Penting
- Bencana "Saya Akan Menggunakan UUID di Mana-mana"
- Normalisasi Dini: Ketika Bentuk Normal Ketiga Menjadi Musuhmu
- Mimpi Buruk NULL: Ketika Opsional Menjadi Mustahil
- Beban Indeks: Ketika Lebih Banyak Tidak Selalu Lebih Baik
Saya Marcus Chen, dan saya telah menghabiskan dua belas tahun terakhir sebagai arsitek basis data, bekerja dengan semua orang mulai dari startup SaaS yang gigih hingga perusahaan Fortune 500. Saya telah merancang skema untuk sistem yang menangani 500 juta transaksi harian, mengoptimalkan kueri yang menghemat 200ms dari jalur kritis, dan ya—saya telah membuat hampir setiap kesalahan desain basis data yang ada. Insiden Black Friday itu? Itu mengajarkan saya lebih banyak tentang desain basis data daripada seluruh gelar ilmu komputer saya.
Saat ini, saya adalah Arsitek Basis Data Utama di TXT1.ai, di mana kami memproses lebih dari 3 miliar pesan teks setiap tahun di platform komunikasi bertenaga AI kami. Tetapi saya sampai di sini dengan gagal maju, dan saya ingin berbagi pelajaran mahal yang saya pelajari agar Anda bisa melewatkan serangan panik pukul 2 pagi dan panggilan marah dari investor.
Bencana "Saya Akan Menggunakan UUID di Mana-mana"
Izinkan saya memulai dengan apa yang saya sebut kesalahan $40.000 saya. Pada tahun 2019, saya sedang merancang sistem manajemen hubungan pelanggan untuk sebuah perusahaan e-commerce menengah. Saya baru saja membaca sebuah artikel blog tentang bagaimana UUID adalah cara "modern" untuk menangani kunci utama—tidak ada lagi integer auto-increment, tidak ada lagi eksposur ID berurutan, sempurna untuk sistem terdistribusi. Saya terpesona.
Jadi saya menjadikan setiap kunci utama dalam sistem sebagai UUID. Tabel pengguna? UUID. Tabel pesanan? UUID. Item lini pesanan? Anda sudah menebaknya—UUID. Saya merasa seperti seorang jenius. Skema terlihat bersih, tidak ada kerentanan ID berurutan, dan saya bisa menghasilkan ID di sisi klien jika perlu. Apa yang bisa salah?
Semuanya. Benar-benar semuanya berjalan salah.
Dalam enam bulan, ukuran basis data kami membengkak menjadi 340GB padahal seharusnya hanya sekitar 180GB. Kinerja kueri menurun setiap minggu. Ukuran indeks kami sangat besar—indeks tabel pesanan saja 12GB. Penggabungan antara pesanan dan item lini yang seharusnya memerlukan 50ms kini memakan waktu 800ms. Basis data menghabiskan waktu yang sangat besar untuk I/O disk, dan biaya AWS RDS kami hampir dua kali lipat.
Inilah yang saya pelajari dengan cara yang sulit: UUID adalah 128 bit (16 byte) dibandingkan dengan integer 4 byte atau bigint 8 byte. Itu berarti 4x penyimpanan untuk setiap kunci utama. Tapi yang benar-benar meriuhkan adalah fragmentasi indeks. UUID bersifat acak, yang berarti setiap penyisipan menyebabkan penulisan acak pada indeks B-tree Anda. Dengan integer berurutan, baris baru ditambahkan di akhir indeks. Dengan UUID, basis data terus-menerus menyeimbangkan seluruh struktur indeks.
Kami mengukur dampaknya: menyisipkan 100.000 baris dengan ID integer membutuhkan waktu 8 detik. Operasi yang sama dengan UUID memakan waktu 34 detik. Itu adalah penalti kinerja 4,25x hanya dari pemilihan kunci utama. Ketika Anda memproses 50.000 pesanan per hari, angka itu cepat terakumulasi.
Perbaikannya menghabiskan waktu pengembangan selama tiga minggu dan memerlukan migrasi yang diatur dengan hati-hati selama jendela pemeliharaan. Kami beralih ke kunci utama bigint untuk tabel dengan volume tinggi dan menyimpan UUID hanya untuk tabel di mana kami benar-benar memerlukan pengidentifikasi unik secara global di sistem terdistribusi—yang ternyata hanya dua tabel dari empat puluh tujuh.
Aturan saya sekarang: Gunakan integer atau bigint yang auto-increment untuk kunci utama kecuali Anda memiliki alasan tertentu yang terdokumentasi untuk menggunakan UUID. Dan "sepertinya lebih modern" bukanlah alasan yang terdaftar.
Normalisasi Dini: Ketika Bentuk Normal Ketiga Menjadi Musuhmu
Baru keluar dari universitas, saya sangat terobsesi dengan normalisasi. Saya telah menghafal semua bentuk normal, bisa mengulangi aturan Codd dalam tidur saya, dan percaya bahwa basis data yang dinormalisasi dengan baik adalah puncak keunggulan desain. Jadi ketika saya merancang sistem produksi pertama saya—sebuah platform manajemen konten—saya menormalisasi segalanya hingga bentuk normal ketiga dan lebih.
"Setiap keputusan basis data 'cerdas' yang Anda buat hari ini adalah potensi krisis pukul 2 pagi enam bulan dari sekarang. Rancang untuk sistem yang Anda miliki, bukan untuk sistem yang Anda inginkan."
Saya memiliki tabel pengguna, tabel user_addresses (karena pengguna mungkin memiliki beberapa alamat), tabel user_phone_numbers (beberapa telepon!), tabel user_preferences, tabel user_settings, dan tabel user_metadata. Memuat profil seorang pengguna membutuhkan penggabungan enam tabel. Saya sangat bangga dengan betapa "bersihnya" semua itu terlihat.
Kemudian kami meluncurkan. Halaman profil pengguna—halaman yang paling sering diakses di seluruh aplikasi—memerlukan waktu 1,2 detik untuk memuat. Kami melakukan enam penggabungan untuk setiap tampilan halaman, dan dengan 10.000 pengguna aktif harian, itu berarti 60.000 penggabungan per hari hanya untuk tampilan profil. CPU basis data terus-menerus di atas 70%.
Panggilan bangun datang ketika pengembang utama kami menarik saya ke samping dan menunjukkan rencana eksekusi kueri. "Marcus," katanya, "kami menggabungkan enam tabel untuk menampilkan nama, email, dan nomor telepon pengguna. Ini gila." Dia benar. Saya telah mengoptimalkan untuk kemurnian teoretis alih-alih kinerja praktis.
Kami melakukan denormalisasi secara strategis. Alamat utama pengguna kembali ke tabel pengguna. Nomor telepon utama mereka? Begitu juga. Kami menyimpan tabel terpisah untuk alamat tambahan dan nomor telepon, tetapi 94% pengguna kami hanya memiliki satu dari masing-masing. Perubahan tunggal itu mengurangi waktu kueri halaman profil rata-rata kami dari 1,2 detik menjadi 180ms—peningkatan 85%.
Inilah yang saya pelajari: Normalisasi adalah alat, bukan agama. Bentuk normal ketiga adalah titik awal yang hebat, tetapi kinerja dunia nyata sering kali memerlukan denormalisasi strategis. Sekarang saya mengikuti apa yang saya sebut "aturan denormalisasi 80/20"—jika 80% kueri memerlukan data dari beberapa tabel, kemungkinan data tersebut seharusnya berada di satu tabel. Saya mengukur pola kueri di lingkungan produksi dan melakukan denormalisasi berdasarkan penggunaan aktual, bukan kemurnian teoretis.
Kuncinya adalah mengetahui kapan harus melakukan denormalisasi. Tabel dengan banyak baca dan sedikit tulis adalah kandidat yang sempurna. Profil pengguna, katalog produk, data konfigurasi—semua ini adalah tempat yang bagus untuk melakukan denormalisasi. Tabel transaksi dengan volume tulis yang tinggi? Simpan yang itu dalam keadaan normal untuk menghindari anomali pembaruan.
Mimpi Buruk NULL: Ketika Opsional Menjadi Mustahil
Dahulu saya menyukai kolom nullable. Mereka terlihat sangat fleksibel, sangat akomodatif. Seorang pengguna mungkin tidak memiliki nama tengah? Buat saja nullable. Sebuah pesanan mungkin tidak memiliki kode diskon? Nullable. Sebuah produk mungkin tidak memiliki berat? Anda pasti paham maksudnya.
| Tipe Kunci Utama | Ukuran Penyimpanan | Kinerja Indeks | Kasus Penggunaan Terbaik |
|---|---|---|---|
| Auto-increment INT | 4 byte | Istimewa (berurutan) | Sistem server tunggal, tabel dengan volume tinggi |
| Auto-increment BIGINT | 8 byte | Istimewa (berurutan) | Sistem server tunggal berskala besar |
| UUID (v4) | 16 byte | Miskin (acak) | Sistem terdistribusi, ID yang sensitif terhadap keamanan |
| ULID/UUID (v7) | 16 byte | Baik (berurutan waktu) | Sistem terdistribusi yang memerlukan kemampuan urut |
| Kunci Komposit | Beragam | Adil hingga Baik | Hubungan natural, sistem multi-penyewa |
Pada suatu proyek yang sangat buruk, saya merancang sistem manajemen inventaris di mana sekitar 60% kolom di semua tabel adalah nullable. Ini tampaknya masuk akal—tidak setiap field selalu memiliki data, bukan? Mengapa memaksakan nilai default ketika NULL jelas mengkomunikasikan "tidak ada nilai"?
Masalah mulai muncul dengan segera. Kueri menjadi ladang ranjau dari pemeriksaan NULL. Ingin menemukan semua produk tanpa berat? Anda mungkin berpikir "DIMANA berat IS NULL" akan berhasil, tetapi bagaimana dengan produk di mana beratnya secara eksplisit diatur ke nol? Sekarang Anda membutuhkan "DIMANA berat IS NULL ATAU berat = 0". Ingin menjumlahkan total pesanan? Lebih baik menggunakan COALESCE atau jumlah Anda mungkin mengembalikan NULL jika ada nilai individu yang NULL.
🛠 Jelajahi Alat Kami
B