PostgreSQL vs MariaDB untuk Aplikasi CRUD Transaksional
PostgreSQL vs MariaDB: tinjauan praktis tentang pengindeksan, migrasi, JSON, dan fitur query yang mulai penting ketika aplikasi CRUD berkembang melewati tahap prototipe.

Saat aplikasi CRUD berkembang melewati prototipe
Aplikasi CRUD prototipe biasanya terasa cepat karena datanya masih kecil, tim kecil, dan lalu lintas bisa diprediksi. Anda bisa memakai query sederhana, beberapa indeks, dan tweak skema manual. Lalu aplikasi mendapat pengguna nyata, alur kerja nyata, dan tenggat nyata.
Pertumbuhan mengubah beban kerja. Daftar dan dashboard dibuka seharian. Lebih banyak orang mengedit catatan yang sama. Job latar mulai menulis secara batch. Saat itulah “kemarin berjalan baik” berubah jadi halaman lambat, timeout acak, dan tunggu lock saat jam puncak.
Anda mungkin sudah melewati batas ketika melihat hal seperti halaman daftar yang melambat setelah halaman ke-20, rilis yang mencakup backfill data (bukan hanya kolom baru), semakin banyak “flex fields” untuk metadata dan payload integrasi, atau tiket dukungan yang mengatakan “simpan butuh waktu lama” saat sibuk.
Di saat itu, membandingkan PostgreSQL dan MariaDB berhenti menjadi preferensi merek dan menjadi pertanyaan praktis. Untuk beban kerja CRUD transaksional, detail yang biasanya menentukan hasil adalah opsi pengindeksan saat query menjadi lebih kompleks, keamanan migrasi saat tabel besar, penyimpanan dan query JSON, serta fitur query yang mengurangi kerja di sisi aplikasi.
Tulisan ini fokus pada perilaku basis data tersebut. Tidak membahas mendalam soal sizing server, harga cloud, atau kontrak vendor. Itu penting, tapi seringkali lebih mudah diubah nanti dibandingkan gaya skema dan query yang jadi dasar produk Anda.
Mulai dari kebutuhan aplikasi, bukan merek database
Titik awal yang lebih baik bukanlah “PostgreSQL vs MariaDB.” Melainkan perilaku sehari-hari aplikasi Anda: membuat catatan, memperbarui beberapa field, menampilkan daftar ber-filter, dan tetap benar saat banyak orang klik bersamaan.
Tuliskan apa yang dilakukan layar tersibuk Anda. Berapa banyak baca yang terjadi untuk setiap tulis? Kapan puncaknya terjadi (login pagi, laporan akhir bulan, impor besar)? Tangkap filter dan pengurutan tepat yang Anda andalkan, karena itu yang nanti mengarahkan desain indeks dan pola query.
Lalu definisikan hal yang tak bisa ditawar. Untuk banyak tim itu berarti konsistensi ketat untuk uang atau inventaris, jejak audit untuk “siapa mengubah apa,” dan query laporan yang tidak runtuh setiap kali skema berkembang.
Realitas operasional sama pentingnya dengan fitur. Putuskan apakah Anda akan menjalankan database terkelola atau self-host, seberapa cepat Anda harus memulihkan dari backup, dan toleransi Anda terhadap jendela pemeliharaan.
Akhirnya, definisikan “cukup cepat” dalam beberapa target jelas. Misalnya: latensi API p95 di beban normal (200–400 ms), p95 di puncak konkuren (mungkin 2x normal), tunggu lock maksimal saat update (di bawah 100 ms), dan batas waktu backup serta restore.
Dasar pengindeksan yang menentukan kecepatan CRUD
Kebanyakan aplikasi CRUD terasa cepat sampai tabel mencapai jutaan baris dan setiap layar menjadi “daftar berfilter dengan pengurutan.” Pada titik itu, pengindeksan membedakan antara query 50 ms dan timeout 5 detik.
B-tree adalah indeks default di PostgreSQL dan MariaDB. Mereka membantu saat Anda memfilter kolom, join pada kunci, dan saat ORDER BY cocok dengan urutan indeks. Perbedaan nyata biasanya soal selektivitas (berapa banyak baris yang cocok) dan apakah indeks dapat memenuhi filter dan pengurutan tanpa memindai baris ekstra.
Seiring aplikasi matang, indeks komposit lebih penting daripada indeks satu kolom. Pola umum adalah filter multi-tenant + status + pengurutan waktu, seperti (tenant_id, status, created_at). Letakkan filter yang paling konsisten dulu (sering tenant_id), lalu filter berikutnya, lalu kolom yang Anda urutkan. Ini cenderung lebih baik daripada indeks terpisah yang optimizer tidak bisa gabungkan secara efisien.
Perbedaan muncul dengan indeks “lebih cerdas”. PostgreSQL mendukung partial dan expression index, yang bagus untuk layar fokus (mis. mengindeks hanya tiket “open”). Mereka kuat, tapi bisa mengejutkan tim jika query tidak cocok persis dengan predikatnya.
Indeks tidak gratis. Setiap insert dan update juga harus memperbarui setiap indeks, jadi mudah untuk mempercepat satu layar namun diam-diam memperlambat semua operasi tulis.
Cara sederhana untuk disiplin:
- Tambahkan indeks hanya untuk jalur query nyata (layar atau panggilan API yang bisa Anda sebut namanya).
- Lebih suka satu indeks komposit yang baik daripada banyak indeks yang saling tumpang tindih.
- Periksa kembali indeks setelah perubahan fitur dan hapus beban mati.
- Rencanakan pemeliharaan: PostgreSQL perlu vacuum/analyze rutin untuk menghindari bloat; MariaDB juga bergantung pada statistik yang bagus dan pembersihan sesekali.
- Ukur sebelum dan sesudah, jangan cuma percaya intuisi.
Pengindeksan untuk layar nyata: daftar, pencarian, dan paginasi
Sebagian besar aplikasi CRUD menghabiskan waktu pada beberapa layar: daftar dengan filter, kotak pencarian, dan halaman detail. Pilihan database Anda kurang penting dibandingkan apakah indeks cocok dengan layar tersebut, tapi kedua engine memberi alat berbeda saat tabel tumbuh.
Untuk halaman daftar, pikirkan urutannya: filter dulu, lalu sort, lalu paginate. Pola umum: “semua tiket untuk akun X, status di (open, pending), terbaru dulu.” Indeks komposit yang dimulai dengan kolom filter dan diakhiri kolom pengurutan biasanya menang.
Paginasi perlu perhatian khusus. Offset pagination (halaman 20 dengan OFFSET 380) melambat saat Anda menggulir karena database masih harus melewati baris awal. Keyset pagination lebih stabil: Anda mengoper nilai terakhir yang dilihat (mis. created_at dan id) dan minta “20 berikutnya yang lebih tua dari itu.” Ini juga mengurangi duplikat dan celah ketika baris baru datang saat pengguna menggulir.
PostgreSQL punya opsi berguna untuk layar daftar: indeks “covering” menggunakan INCLUDE, yang bisa memungkinkan index-only scans saat visibility map memungkinkannya. MariaDB juga bisa melakukan covering reads, tapi biasanya dicapai dengan meletakkan kolom yang diperlukan langsung ke definisi indeks. Itu bisa membuat indeks lebih lebar dan lebih mahal untuk dipelihara.
Anda mungkin butuh indeks lebih baik jika endpoint daftar melambat saat tabel tumbuh meskipun hanya mengembalikan 20–50 baris, pengurutan jadi lambat kecuali Anda menghapus ORDER BY, atau I/O melonjak saat filter sederhana. Query yang lebih panjang juga cenderung meningkatkan tunggu lock saat periode sibuk.
Contoh: layar order yang memfilter customer_id dan status dan mengurutkan menurut created_at biasanya mendapat manfaat dari indeks yang dimulai dengan (customer_id, status, created_at). Jika nanti Anda menambah “cari berdasarkan nomor order”, itu biasanya indeks terpisah, bukan sesuatu yang Anda tambahkan ke indeks daftar.
Migrasi: menjaga rilis aman saat data bertumbuh
Migrasi cepat berubah maknanya dari “ubah tabel” menjadi lebih kompleks. Setelah ada pengguna nyata dan riwayat nyata, Anda juga perlu menangani backfill data, memperketat constraint, dan membersihkan bentuk data lama tanpa merusak aplikasi.
Default aman adalah expand, backfill, contract. Tambahkan apa yang Anda butuhkan dengan cara yang tidak mengganggu kode yang ada, salin atau hitung data dalam langkah kecil, lalu hapus jalur lama hanya setelah Anda yakin.
Dalam praktik artinya biasanya menambahkan kolom nullable atau tabel baru, melakukan backfill dalam batch sambil menjaga consistency tulis, memvalidasi kemudian dengan constraint seperti NOT NULL, foreign key, dan aturan unique, dan baru kemudian menghapus kolom, indeks, dan jalur kode lama.
Tidak semua perubahan skema sama risikonya. Menambah kolom sering rendah risiko. Menambah indeks bisa mahal pada tabel besar, jadi rencanakan saat lalu lintas rendah dan ukur. Mengubah tipe kolom sering paling berisiko karena mungkin menulis ulang data atau memblokir tulis. Pola yang lebih aman: buat kolom baru dengan tipe baru, backfill, lalu alihkan baca dan tulis.
Rollback juga berubah makna pada skala besar. Rollback skema kadang mudah; rollback data seringkali tidak. Jelaskan apa yang bisa Anda batalkan, terutama jika migrasi mencakup hapus destruktif atau transformasi yang hilang datanya.
Dukungan JSON: field fleksibel tanpa masalah di masa depan
Field JSON menggoda karena memungkinkan Anda rilis lebih cepat: field tambahan pada form, payload integrasi, preferensi pengguna, dan catatan dari sistem eksternal bisa masuk tanpa ubahan skema. Triknya adalah menentukan apa yang masuk ke JSON dan apa yang pantas jadi kolom nyata.
Di PostgreSQL dan MariaDB, JSON biasanya bekerja paling baik saat jarang difilter dan lebih sering ditampilkan, disimpan untuk debugging, dipakai sebagai “settings” per user atau tenant, atau untuk atribut opsional kecil yang tidak mendorong reporting.
Pengindeksan JSON sering mengejutkan tim. Query satu kunci JSON mudah. Memfilter dan mengurutk berdasarkan kunci itu di seluruh tabel besar bisa membuat performa runtuh. PostgreSQL punya opsi kuat untuk mengindeks path JSON, tetapi Anda tetap perlu disiplin: pilih beberapa kunci yang benar-benar Anda filter dan indeks, biarkan sisanya sebagai payload tanpa indeks. MariaDB juga bisa query JSON, tapi pola “cari di dalam JSON” yang kompleks sering menjadi rapuh dan sulit dipertahankan cepat.
JSON juga melemahkan constraint. Lebih sulit memastikan “harus salah satu dari nilai ini” atau “selalu hadir” dalam blob tak terstruktur, dan alat reporting umumnya lebih suka kolom bertipe.
Aturan yang skala: mulai dengan JSON untuk hal yang belum pasti, tapi normalisasi ke kolom atau tabel anak ketika Anda (1) memfilter atau mengurutk berdasarkan itu, (2) butuh constraint, atau (3) melihatnya muncul di dashboard setiap minggu. Menyimpan seluruh respons API pengiriman untuk sebuah order sebagai JSON sering tidak masalah. Field seperti delivery_status dan carrier biasanya layak jadi kolom nyata ketika dukungan dan reporting mengandalkannya.
Fitur query yang muncul pada aplikasi matang
Di awal, sebagian besar aplikasi CRUD berjalan dengan SELECT, INSERT, UPDATE, dan DELETE sederhana. Nanti, Anda menambah feed aktivitas, view audit, laporan admin, dan pencarian yang harus terasa instan. Di sinilah pilihan mulai terlihat sebagai tradeoff fitur.
CTE dan subquery membantu menjaga query kompleks tetap terbaca. Mereka berguna saat Anda membangun hasil bertahap (filter orders, join payments, hitung total). Tapi keterbacaan bisa menyembunyikan biaya. Saat query jadi lambat, Anda mungkin perlu menulis ulang CTE sebagai subquery atau join lalu cek rencana eksekusi.
Window function jadi penting saat seseorang meminta “peringkat pelanggan berdasarkan pengeluaran,” “tunjukkan total berjalan,” atau “status terbaru per tiket.” Mereka sering menggantikan loop di aplikasi dan mengurangi jumlah query.
Tulis idempoten adalah kebutuhan dewasa. Saat retry terjadi (jaringan mobile, job latar), upsert memungkinkan Anda menulis aman tanpa membuat duplikat:
- PostgreSQL:
INSERT ... ON CONFLICT - MariaDB:
INSERT ... ON DUPLICATE KEY UPDATE
Pencarian adalah fitur yang tiba-tiba menjadi penting. Full-text search bawaan dapat menangani katalog produk, basis pengetahuan, dan catatan dukungan. Pencarian mirip trigram berguna untuk type-ahead dan toleransi typo. Jika pencarian menjadi inti (pemberian peringkat kompleks, banyak filter, lalu lintas berat), alat pencarian eksternal bisa layak meski menambah kompleksitas.
Contoh: portal order mulai dengan “daftar order.” Setahun kemudian perlu “tunjukkan order terbaru setiap pelanggan, urutkan berdasarkan pengeluaran bulanan, dan cari nama yang salah eja.” Itu adalah kemampuan database, bukan hanya pekerjaan UI.
Transaksi, lock, dan konkurensi di bawah beban
Saat lalu lintas rendah, kebanyakan database terasa baik. Di bawah beban, perbedaan sering soal seberapa baik Anda menangani perubahan bersamaan pada data yang sama, bukan kecepatan mentah. PostgreSQL dan MariaDB bisa menjalankan beban kerja CRUD transaksional, tapi Anda tetap harus mendesain untuk kontensi.
Isolasi dengan bahasa sederhana
Transaksi adalah kelompok langkah yang harus berhasil bersama. Isolasi mengontrol apa yang bisa dilihat sesi lain sementara langkah-langkah itu berjalan. Isolasi lebih tinggi menghindari pembacaan mengejutkan, tapi bisa menambah tunggu. Banyak aplikasi mulai dengan default dan mengetatkan isolasi hanya untuk alur yang benar-benar perlu (seperti mengenakan biaya kartu dan memperbarui order).
Apa yang sebenarnya menyebabkan masalah lock
Masalah locking di aplikasi CRUD biasanya datang dari beberapa pelaku berulang: baris panas yang banyak orang perbarui, counter yang berubah pada setiap aksi, antrean job di mana banyak worker mencoba klaim “job berikutnya” yang sama, dan transaksi panjang yang menahan lock sementara pekerjaan lain (atau waktu pengguna) berlalu.
Untuk mengurangi kontensi, jaga transaksi pendek, perbarui hanya kolom yang diperlukan, dan hindari panggilan jaringan di dalam transaksi.
Kebiasaan yang membantu adalah retry saat konflik. Jika dua agen dukungan menyimpan edit pada tiket yang sama bersamaan, jangan gagal diam-diam. Deteksi konflik, muat ulang baris terbaru, dan minta pengguna menerapkan ulang perubahan.
Untuk mendeteksi masalah lebih awal, pantau deadlock, transaksi berjalan lama, dan query yang menghabiskan waktu menunggu alih-alih berjalan. Jadikan slow query log bagian dari rutinitas, terutama setelah rilis yang menambah layar atau job latar.
Operasi yang menjadi penting setelah peluncuran
Setelah peluncuran, Anda tidak hanya mengoptimalkan kecepatan query. Anda mengoptimalkan pemulihan, perubahan aman, dan performa yang dapat diprediksi.
Langkah umum berikutnya adalah menambahkan replica. Primary menangani tulis, dan replica dapat melayani halaman baca-berat seperti dashboard atau laporan. Ini mengubah cara Anda memikirkan kesegaran data: beberapa pembacaan bisa tertinggal beberapa detik, jadi aplikasi harus tahu layar mana yang harus baca dari primary (mis. “order baru saja dibuat”) dan mana yang bisa mentolerir data sedikit lebih tua (mis. ringkasan mingguan).
Backup hanyalah setengah pekerjaan. Yang penting adalah apakah Anda bisa restore dengan cepat dan benar. Jadwalkan uji restore berkala ke environment terpisah, lalu validasi dasar: aplikasi bisa konek, tabel kunci ada, dan query penting mengembalikan hasil yang diharapkan. Tim sering baru sadar terlambat bahwa mereka membackup hal yang salah, atau waktu restore jauh melebihi batas downtime mereka.
Upgrade juga tidak lagi sekadar “klik dan berharap.” Rencanakan jendela pemeliharaan, baca catatan kompatibilitas, dan uji jalur upgrade dengan salinan data produksi. Bahkan bump versi minor bisa mengubah rencana query atau perilaku indeks dan fungsi JSON.
Observability sederhana memberi nilai lebih awal. Mulailah dengan slow query log dan query teratas berdasarkan total waktu, saturasi koneksi, replication lag (jika pakai replica), rasio hit cache dan tekanan I/O, serta tunggu lock dan kejadian deadlock.
Cara memilih: proses evaluasi praktis
Jika buntu, berhentilah membaca daftar fitur dan jalankan uji kecil dengan beban kerja Anda sendiri. Tujuannya bukan benchmark sempurna. Tujuannya menghindari kejutan ketika tabel mencapai jutaan baris dan siklus rilis Anda mempercepat.
1) Bangun tes mini yang menyerupai produksi
Pilih irisan aplikasi yang merepresentasikan masalah nyata: satu atau dua tabel kunci, beberapa layar, dan jalur penulisan di belakangnya. Kumpulkan query teratas Anda (yang di balik halaman daftar, detail, dan job latar). Muat jumlah baris realistis (setidaknya 100x data prototipe Anda, dengan bentuk data serupa). Tambahkan indeks yang Anda kira perlu, lalu jalankan query yang sama dengan filter dan pengurutan yang sama dan rekam waktu. Ulangi saat penulisan berlangsung (skrip sederhana memasukkan dan memperbarui baris sudah cukup).
Contoh cepat: daftar "Customers" yang memfilter berdasarkan status, mencari berdasarkan nama, mengurutkan berdasarkan aktivitas terakhir, dan melakukan paginasi. Layar ini sering mengungkap apakah pengindeksan dan perilaku planner Anda akan tahan lama.
2) Latih migrasi seperti rilis nyata
Buat salinan staging dataset dan latih perubahan yang Anda tahu akan datang: menambah kolom, mengubah tipe, backfill data, menambah indeks. Ukur berapa lama, apakah memblokir tulis, dan apa arti rollback sebenarnya ketika data sudah berubah.
3) Gunakan scorecard sederhana
Setelah pengujian, beri skor tiap opsi pada performa untuk query nyata Anda, kebenaran dan keamanan (constraint, transaksi, edge case), risiko migrasi (locking, downtime, opsi recovery), usaha ops (backup/restore, replikasi, monitoring), dan kenyamanan tim.
Pilih database yang mengurangi risiko untuk 12 bulan ke depan, bukan yang menang dalam satu mikro-tes.
Kesalahan umum dan jebakan
Masalah database paling mahal sering dimulai sebagai “jalan pintas.” Kedua database bisa menjalankan aplikasi CRUD transaksional, tapi kebiasaan buruk akan merugikan salah satu pun saat lalu lintas dan data tumbuh.
Jebakan umum adalah menganggap JSON solusi untuk segala hal. Field "extras" yang fleksibel baik untuk data benar-benar opsional, tetapi field inti seperti status, timestamp, dan foreign key harus tetap sebagai kolom nyata. Kalau tidak, Anda akan berakhir dengan filter lambat, validasi canggung, dan refactor menyakitkan saat reporting jadi prioritas.
Pengindeksan punya jebakan sendiri: menambah indeks untuk setiap filter yang Anda lihat di layar. Indeks mempercepat baca, tapi memperlambat tulis dan membuat migrasi lebih berat. Indekslah apa yang benar-benar digunakan pengguna, lalu validasi dengan pengukuran beban.
Migrasi bisa menggigit ketika mengunci tabel. Perubahan big-bang seperti menulis ulang kolom besar, menambah NOT NULL dengan default, atau membuat indeks besar bisa memblokir tulis selama menit. Pecah perubahan berisiko ke langkah kecil dan jadwalkan saat aplikasinya sepi.
Juga, jangan bergantung pada default ORM selamanya. Saat daftar Anda tumbuh dari 1.000 baris menjadi 10 juta, Anda perlu membaca rencana query, menemukan indeks yang hilang, dan memperbaiki join yang lambat.
Tanda peringatan cepat: field JSON dipakai untuk filter dan pengurutan utama, jumlah indeks bertambah tanpa mengukur performa tulis, migrasi yang menulis ulang tabel besar dalam satu deploy, dan paginasi tanpa pengurutan stabil (yang menyebabkan baris hilang dan duplikat).
Daftar periksa cepat sebelum Anda berkomitmen
Sebelum memilih sisi, lakukan pemeriksaan realitas cepat berdasarkan layar tersibuk dan proses rilis Anda.
- Bisakah layar teratas Anda tetap cepat pada beban puncak? Uji halaman daftar paling lambat dengan filter nyata, pengurutan, dan paginasi, dan pastikan indeks cocok dengan query tersebut.
- Bisakah Anda mengirim perubahan skema dengan aman? Tuliskan rencana expand-backfill-contract untuk perubahan besar berikutnya.
- Apakah Anda punya aturan jelas untuk JSON vs kolom? Tentukan kunci JSON mana yang harus dapat dicari atau diurutkan dan mana yang benar-benar fleksibel.
- Apakah Anda bergantung pada fitur query spesifik? Periksa perilaku upsert, window function, CTE, dan apakah Anda perlu indeks fungsional atau partial.
- Bisakah Anda mengoperasikannya setelah peluncuran? Buktikan Anda bisa restore dari backup, mengukur slow query, dan baseline latensi serta tunggu lock.
Contoh: dari pelacakan order sederhana ke portal pelanggan sibuk
Bayangkan portal pelanggan yang dimulai sederhana: pelanggan login, melihat order, mengunduh faktur, dan membuka tiket dukungan. Minggu pertama, hampir semua database transaksional terasa baik. Halaman cepat, dan skema kecil.
Beberapa bulan kemudian, momen pertumbuhan muncul. Pelanggan minta filter seperti "order dikirim 30 hari terakhir, dibayar kartu, dengan pengembalian sebagian." Tim dukungan ingin ekspor cepat ke CSV untuk tinjauan mingguan. Finance mau jejak audit: siapa mengubah status faktur, kapan, dan dari apa ke apa. Pola query menjadi lebih luas dan bervariasi dibanding layar awal.
Di sinilah keputusan tentang fitur spesifik dan perilakunya di bawah beban nyata menjadi penting.
Jika Anda menambah field fleksibel (instruksi pengiriman, atribut kustom, metadata tiket), dukungan JSON penting karena kelak Anda mungkin ingin query di dalam field itu. Jujurlah apakah tim Anda akan mengindeks path JSON, memvalidasi bentuknya, dan menjaga performa tetap dapat diprediksi saat JSON tumbuh.
Reporting adalah titik tekanan lain. Saat Anda join orders, invoices, payments, dan tickets dengan banyak filter, Anda akan peduli tentang indeks komposit, perencanaan query, dan seberapa mudah mengubah indeks tanpa downtime. Migrasi juga berhenti menjadi "jalankan skrip hari Jumat" dan menjadi bagian dari setiap rilis, karena perubahan kecil bisa menyentuh jutaan baris.
Jalan praktis ke depan: tuliskan lima layar nyata dan ekspor yang Anda harapkan dalam enam bulan, sertakan tabel histori audit lebih awal, benchmark dengan ukuran data realistis menggunakan query paling lambat Anda (bukan hello-world CRUD), dan dokumentasikan aturan tim untuk penggunaan JSON, pengindeksan, dan migrasi.
Jika Anda ingin bergerak cepat tanpa membangun setiap lapisan sendiri, AppMaster (appmaster.io) dapat menghasilkan backend siap produksi, aplikasi web, dan aplikasi mobile native dari model visual. Itu juga mendorong Anda memperlakukan layar, filter, dan proses bisnis sebagai beban query nyata sejak awal, sehingga membantu menangkap risiko pengindeksan dan migrasi sebelum mencapai produksi.
FAQ
Mulailah dengan menuliskan beban kerja nyata Anda: layar daftar tersibuk, filter, pengurutan, dan jalur penulisan pada puncak beban. Keduanya bisa menjalankan CRUD dengan baik, tetapi pilihan yang lebih aman adalah yang sesuai dengan cara Anda mengindeks, melakukan migrasi, dan menjalankan query selama 12 bulan ke depan — bukan nama yang terasa akrab.
Jika halaman daftar melambat saat Anda melanjutkan ke halaman yang lebih jauh, besar kemungkinan Anda terkena biaya pemindaian OFFSET. Jika penyimpanan kadang tersendat saat jam sibuk, mungkin ada kontensi lock atau transaksi panjang. Jika rilis sekarang termasuk backfill data dan indeks besar, migrasi telah menjadi problem reliabilitas, bukan sekadar perubahan skema.
Default ke satu indeks komposit untuk setiap query layar penting, susun berdasarkan filter yang paling konsisten terlebih dulu dan kolom pengurutan di akhir. Contoh: daftar multi-tenant sering bekerja baik dengan (tenant_id, status, created_at) karena mendukung filter dan pengurutan tanpa pemindaian ekstra.
Offset pagination melambat karena database tetap harus melewati baris-baris awal. Gunakan pagination keyset (menggunakan nilai terakhir yang dilihat seperti created_at dan id) untuk kinerja yang lebih stabil dan mengurangi duplikat atau celah ketika baris baru masuk saat pengguna menggulir.
Tambahkan indeks hanya ketika Anda bisa menyebutkan layar atau panggilan API yang tepat yang membutuhkannya, dan periksa kembali setelah setiap rilis fitur. Terlalu banyak indeks yang saling tumpang tindih bisa memperlambat setiap INSERT dan UPDATE, sehingga aplikasi terasa lambat saat beban tulis puncak.
Gunakan pendekatan expand, backfill, contract: tambahkan struktur baru dengan cara yang kompatibel, backfill dalam batch kecil, validasi dengan constraint kemudian, lalu hapus jalur lama setelah reads dan writes sudah beralih. Ini membuat rilis lebih aman ketika tabel besar dan lalu lintas konstan.
Simpan JSON untuk data seperti payload yang sebagian besar ditampilkan atau disimpan untuk debugging, dan promosikan field menjadi kolom nyata ketika Anda secara rutin memfilter, mengurutkan, atau membuat laporan darinya. Ini menghindari query JSON yang lambat dan memudahkan penerapan constraint seperti nilai wajib atau status yang valid.
Upsert menjadi penting ketika retry sering terjadi (jaringan mobile, job latar belakang, timeout). PostgreSQL: INSERT ... ON CONFLICT. MariaDB: INSERT ... ON DUPLICATE KEY UPDATE. Di kedua kasus, definisikan kunci unik dengan hati-hati agar retry tidak membuat duplikat.
Jaga transaksi tetap pendek, hindari panggilan jaringan di dalam transaksi, dan kurangi “hot rows” yang sering diperbarui (seperti counter bersama). Saat konflik terjadi, lakukan retry atau tampilkan konflik dengan jelas kepada pengguna sehingga edit tidak hilang tanpa disadari.
Ya, jika Anda bisa mentolerir sedikit lag pada halaman baca-berat seperti dashboard dan laporan. Tetap jaga agar pembacaan kritis sesaat setelah perubahan tetap diarahkan ke primary (mis. tepat setelah melakukan pemesanan), dan pantau replication lag sehingga Anda tidak menampilkan data yang membingungkan karena kadaluwarsa.


