Saat produk masuk maintenance mode, fokus tim biasanya bergeser: bukan lagi mengejar fitur baru, melainkan menjaga sistem tetap stabil dengan sumber daya yang makin terbatas. Dalam fase ini, strategi index dan pagination menjadi area yang layak diaudit lebih awal, karena pola query lama sering tetap dipakai sementara data terus bertambah. Hasilnya mudah ditebak: halaman daftar makin lambat, query admin timeout, dan beban database naik tanpa perubahan fitur yang berarti.

Intinya sederhana: jika aplikasi sudah cenderung read-heavy dan perubahan skema mulai mahal, Anda perlu memastikan dua hal. Pertama, query paling sering dipakai harus punya index yang sesuai dengan pola filter dan sorting nyata. Kedua, pagination berbasis OFFSET besar perlu ditinjau ulang, karena ia cenderung memburuk seiring pertumbuhan data. Konteks ini mirip dengan organisasi atau proyek yang memasuki fase pemeliharaan jangka panjang—seperti beberapa proyek perangkat lunak yang mengumumkan maintenance mode—di mana keputusan teknis harus lebih konservatif, mudah dijaga, dan berdampak langsung pada operasional.

Mengapa audit performa penting saat maintenance mode

Pada fase aktif, query lambat kadang tertutupi oleh pengembangan fitur, cache baru, atau scale-up infrastruktur. Saat produk masuk maintenance mode, toleransi terhadap kompleksitas baru turun. Tim ingin sistem yang:

  • stabil walau traffic dan data bertambah,
  • mudah dipahami oleh tim yang lebih kecil,
  • tidak membutuhkan tuning reaktif terlalu sering,
  • punya biaya operasional database yang terkendali.

Masalah yang umum muncul bukan query yang sepenuhnya salah, melainkan query yang dulu cukup cepat pada jutaan baris lebih sedikit. Beberapa gejala nyata:

  • endpoint daftar produk atau transaksi melambat hanya pada halaman-halaman akhir,
  • CPU database tinggi saat jam kerja meski jumlah request stabil,
  • query dengan filter yang tampak sederhana melakukan sequential scan atau membaca terlalu banyak row,
  • waktu insert/update ikut naik setelah tim menambahkan banyak index secara reaktif,
  • pekerjaan backoffice seperti export atau rekonsiliasi mengunci resource terlalu lama.

Karena itu, audit performa pada fase maintenance mode sebaiknya memprioritaskan query yang paling sering dibaca dan paling kritis untuk operasional.

Cara mengidentifikasi query lambat yang benar-benar penting

Mulai dari query nyata, bukan asumsi

Kesalahan umum adalah menebak bottleneck dari kode, bukan dari database. Audit yang baik dimulai dari sumber berikut:

  • slow query log atau fitur observabilitas database,
  • APM pada endpoint yang melambat,
  • log aplikasi yang mencatat durasi query,
  • daftar endpoint paling sering dipakai pengguna.

Fokuskan pada kombinasi tiga dimensi:

  1. Frekuensi: query dieksekusi sangat sering.
  2. Durasi: query kadang atau sering melampaui ambang yang mengganggu.
  3. Dampak bisnis: query terkait halaman daftar, pencarian, dashboard, atau proses backoffice penting.

Contoh pola query yang sering memburuk seiring pertumbuhan data:

SELECT id, sku, name, status, updated_at
FROM products
WHERE tenant_id = 42
  AND status = 'active'
ORDER BY updated_at DESC
LIMIT 50 OFFSET 50000;

Secara fungsional query ini benar. Masalahnya, database sering tetap harus memproses banyak row sebelum bisa membuang 50.000 hasil awal dan mengembalikan 50 baris berikutnya.

Verifikasi dengan EXPLAIN atau EXPLAIN ANALYZE

Setelah menemukan kandidat query, jangan langsung menambah index. Jalankan EXPLAIN untuk melihat rencana eksekusi, lalu gunakan EXPLAIN ANALYZE di lingkungan aman untuk membandingkan estimasi dan eksekusi nyata.

EXPLAIN ANALYZE
SELECT id, sku, name, status, updated_at
FROM products
WHERE tenant_id = 42
  AND status = 'active'
ORDER BY updated_at DESC
LIMIT 50 OFFSET 50000;

Hal yang perlu diperhatikan:

  • apakah terjadi Seq Scan ketika seharusnya index bisa membantu,
  • apakah ada Sort mahal karena urutan data tidak sesuai index,
  • berapa banyak row yang dibaca dibanding row yang benar-benar dikembalikan,
  • apakah estimasi row jauh meleset dari aktual, yang bisa menandakan statistik kurang akurat,
  • apakah filter dan order by berada pada kolom yang berbeda sehingga database harus bekerja dua kali.

Catatan: Jangan mengandalkan satu query contoh saja. Uji beberapa variasi parameter yang mewakili data nyata, karena planner bisa memilih strategi berbeda untuk nilai yang berbeda.

Memilih index untuk pola read-heavy

Pada maintenance mode, tujuan utama index bukan “menambah sebanyak mungkin”, melainkan mengurangi kerja baca untuk query yang benar-benar penting. Index yang tepat biasanya mengikuti pola:

  • kolom filter yang paling selektif atau paling sering dipakai,
  • kolom pengurutan pada daftar,
  • kombinasi filter + sort yang stabil dari waktu ke waktu.

Mulai dari pola query, bukan dari nama kolom populer

Misalkan query utama aplikasi adalah daftar produk aktif per tenant, diurutkan menurut waktu update terbaru:

SELECT id, sku, name, status, updated_at
FROM products
WHERE tenant_id = ?
  AND status = 'active'
ORDER BY updated_at DESC
LIMIT 50;

Untuk pola seperti ini, index gabungan sering lebih berguna daripada index tunggal yang terpisah-pisah:

CREATE INDEX idx_products_tenant_status_updated_at
ON products (tenant_id, status, updated_at DESC);

Mengapa pendekatan ini bekerja:

  • tenant_id dan status membantu mempersempit kandidat row,
  • updated_at membantu mengembalikan hasil sesuai urutan tanpa sort mahal tambahan,
  • query daftar yang mengambil sedikit row pertama menjadi lebih efisien.

Sebaliknya, jika Anda hanya punya index terpisah pada tenant_id dan updated_at, planner mungkin tetap perlu membaca banyak row dan melakukan sort, tergantung distribusi data.

Kapan index tunggal masih cukup

Index tunggal tetap relevan bila:

  • query hanya memfilter satu kolom dengan selektivitas tinggi,
  • sorting tidak kritis atau jumlah hasil kecil,
  • kolom sering dipakai di banyak query berbeda dengan pola yang tidak konsisten.

Contoh:

CREATE INDEX idx_orders_created_at ON orders (created_at);

Ini berguna untuk laporan rentang waktu atau pembersihan data lama, tetapi belum tentu cukup untuk daftar per tenant, per status, atau per user.

Index tidak selalu menguntungkan

Kesalahan umum saat audit adalah menambahkan index pada setiap kolom yang muncul di WHERE. Ini bisa merugikan karena:

  • insert/update/delete menjadi lebih mahal, karena semua index terkait harus ikut diperbarui,
  • ukuran penyimpanan bertambah,
  • planner punya lebih banyak opsi dan kadang memilih rencana yang tidak ideal,
  • index pada kolom dengan kardinalitas rendah belum tentu efektif, misalnya kolom boolean atau status dengan sedikit nilai.

Contoh kasus yang sering tidak banyak membantu:

CREATE INDEX idx_products_is_deleted ON products (is_deleted);

Jika hampir semua query tetap harus memfilter kolom lain yang lebih menentukan, index semacam ini mungkin jarang dipakai. Ia tetap menambah biaya write tanpa memberi dampak baca yang berarti.

Prinsip praktis memilih index saat tim kecil

  • Prioritaskan 5-10 query paling mahal atau paling sering, bukan semua query.
  • Utamakan index yang melayani lebih dari satu endpoint penting.
  • Hindari index duplikat atau terlalu mirip.
  • Tinjau ulang index lama yang tidak lagi dipakai setelah pola fitur berhenti berubah.

Strategi index dan pagination: mengapa OFFSET besar harus dicurigai

Bagian ini sering menjadi sumber penurunan performa yang paling terasa pengguna. Pagination berbasis LIMIT ... OFFSET ... mudah diimplementasikan, tetapi performanya memburuk saat offset membesar.

Masalah OFFSET besar

Contoh query klasik:

SELECT id, name, updated_at
FROM products
WHERE tenant_id = 42
ORDER BY updated_at DESC, id DESC
LIMIT 50 OFFSET 100000;

Walau hasil akhirnya hanya 50 row, database sering harus melangkahi atau memproses banyak row sebelum sampai ke posisi tersebut. Ini menimbulkan beberapa gejala:

  • halaman 1 cepat, halaman 2000 lambat sekali,
  • CPU dan I/O naik pada daftar yang sering diakses admin,
  • hasil pagination bisa tidak stabil bila ada data baru masuk di tengah navigasi.

Ganti dengan keyset pagination

Untuk daftar yang diurutkan secara stabil, keyset pagination biasanya lebih cocok daripada offset besar. Alih-alih meminta “halaman ke-2000”, klien meminta “50 item setelah baris terakhir yang tadi diterima”.

Contoh halaman pertama:

SELECT id, name, updated_at
FROM products
WHERE tenant_id = 42
ORDER BY updated_at DESC, id DESC
LIMIT 50;

Jika baris terakhir dari halaman pertama memiliki updated_at = '2026-01-10 09:00:00' dan id = 12345, halaman berikutnya dapat diambil dengan:

SELECT id, name, updated_at
FROM products
WHERE tenant_id = 42
  AND (
    updated_at < '2026-01-10 09:00:00'
    OR (updated_at = '2026-01-10 09:00:00' AND id < 12345)
  )
ORDER BY updated_at DESC, id DESC
LIMIT 50;

Index yang mendukung pola ini perlu mengikuti urutan filter dan sort:

CREATE INDEX idx_products_tenant_updated_id
ON products (tenant_id, updated_at DESC, id DESC);

Mengapa ini lebih baik:

  • database bisa langsung melanjutkan dari posisi terakhir yang relevan,
  • jumlah row yang dipindai tetap kecil walau data total besar,
  • hasil lebih stabil untuk alur “next page”.

Trade-off keyset pagination

Keyset pagination bukan solusi untuk semua kebutuhan. Kekurangannya:

  • tidak cocok jika pengguna harus lompat langsung ke halaman arbitrer seperti “halaman 347”,
  • implementasi API dan UI sedikit lebih rumit karena memakai cursor atau kombinasi nilai terakhir,
  • sorting harus jelas dan stabil; bila urutan ambigu, hasil bisa duplikat atau terlewat.

Karena itu, pilihan praktis biasanya seperti ini:

  • gunakan offset untuk dataset kecil atau kebutuhan lompat halaman yang benar-benar penting,
  • gunakan keyset pagination untuk feed, daftar transaksi, daftar pesan, audit log, dan daftar operasional besar yang dominan next/previous.

Verifikasi perbaikan: jangan berhenti setelah membuat index

Menambah index atau mengganti pagination belum tentu otomatis memperbaiki produksi. Anda perlu membuktikannya.

Bandingkan execution plan sebelum dan sesudah

Sebelum perubahan, simpan hasil EXPLAIN atau EXPLAIN ANALYZE. Setelah perubahan, bandingkan:

  • jenis scan yang dipakai,
  • jumlah row yang dibaca,
  • adanya operasi sort besar,
  • waktu total eksekusi,
  • konsistensi hasil saat parameter berubah.

Jika planner tetap tidak memakai index baru, beberapa kemungkinan penyebabnya:

  • query tidak cocok dengan urutan kolom dalam index,
  • selektivitas filter terlalu rendah,
  • statistik belum merepresentasikan distribusi data terbaru,
  • query ORM menghasilkan kondisi tambahan yang mengubah rencana eksekusi.

Uji dari sisi aplikasi, bukan database saja

Query yang cepat di database belum tentu memberi dampak besar pada endpoint bila bottleneck lain lebih dominan, misalnya:

  • serialisasi response terlalu berat,
  • N+1 query pada relasi tambahan,
  • network latency ke database,
  • cache invalidation yang buruk.

Karena itu, verifikasi akhir sebaiknya meliputi:

  • waktu endpoint end-to-end,
  • jumlah query per request,
  • pemakaian CPU/I/O database saat beban normal,
  • dampak pada operasi write setelah index ditambah.

Kapan index justru merugikan

Dalam konteks maintenance mode, index yang salah bisa memperburuk operasional jangka panjang. Waspadai kondisi berikut:

Tabel dengan write tinggi

Jika tabel menerima insert atau update sangat sering, setiap index tambahan punya biaya. Pada sistem transaksi atau event log, terlalu banyak index dapat membuat:

  • latensi write naik,
  • replikasi tertinggal,
  • maintenance seperti vacuum, analyze, atau backup menjadi lebih berat.

Index dibuat untuk query yang jarang dipakai

Jangan mengoptimalkan query bulanan jika beban utama datang dari daftar harian yang diakses ribuan kali. Pada tim kecil, setiap index harus punya alasan operasional yang jelas.

Index tumpang tindih

Contoh pola yang perlu ditinjau:

CREATE INDEX idx_a ON products (tenant_id);
CREATE INDEX idx_b ON products (tenant_id, status);
CREATE INDEX idx_c ON products (tenant_id, status, updated_at DESC);

Tidak selalu berarti semua salah, tetapi kombinasi seperti ini harus diperiksa apakah benar semuanya dipakai. Bisa jadi index yang paling lengkap sudah cukup untuk query penting, sementara index lain hanya menambah overhead.

Checklist audit sebelum tim diperkecil

Jika maintenance mode berarti tim inti akan lebih kecil, buat audit yang bisa diwariskan. Checklist berikut bisa dipakai sebagai baseline:

  1. Inventaris query kritis
    Catat query atau endpoint paling sering, paling lambat, dan paling penting secara bisnis.
  2. Dokumentasikan pola filter dan sorting
    Misalnya per-tenant, per-status, urut updated_at desc, atau per-user dan rentang tanggal.
  3. Petakan index yang ada
    Cari index duplikat, tumpang tindih, atau index lama yang sudah tidak relevan.
  4. Evaluasi pagination
    Identifikasi endpoint yang masih memakai OFFSET besar dan tentukan apakah bisa dipindah ke keyset pagination.
  5. Verifikasi dengan EXPLAIN/ANALYZE
    Simpan contoh plan sebelum dan sesudah perubahan untuk query utama.
  6. Ukur dampak write
    Pastikan penambahan index tidak menimbulkan regresi pada insert/update/delete yang penting.
  7. Audit query dari ORM
    Pastikan query nyata sama dengan asumsi tim; banyak masalah berasal dari generated SQL yang tidak disadari.
  8. Pastikan sorting stabil
    Untuk keyset pagination, gunakan tie-breaker seperti id agar urutan konsisten.
  9. Dokumentasikan keputusan
    Tulis mengapa index dibuat, query apa yang dilayani, dan risiko jika dihapus.
  10. Jadwalkan review berkala
    Walau produk maintenance mode, data tetap tumbuh. Review ringan per kuartal lebih murah daripada incident besar.

Penutup

Saat produk masuk maintenance mode, performa database sering justru menjadi lebih penting karena tim ingin mengurangi kejutan operasional. Audit yang paling bernilai biasanya tidak rumit: identifikasi query lambat nyata, pastikan index mengikuti pola read-heavy yang benar, dan ganti pagination berbasis OFFSET besar dengan keyset pagination bila alurnya memungkinkan.

Prinsip utamanya adalah konservatif tetapi terukur. Tambahkan index hanya jika ada query penting yang terbukti terbantu, verifikasi dengan EXPLAIN/ANALYZE, dan selalu timbang biaya write serta kompleksitas jangka panjang. Pada fase maintenance, keputusan yang paling baik bukan yang paling canggih, melainkan yang paling stabil, terdokumentasi, dan mudah dirawat oleh tim yang lebih kecil.