Kalau query SQL mulai lambat, masalahnya jarang selesai hanya dengan satu saran generik seperti “tambah index” atau “pakai EXPLAIN”. Yang dibutuhkan adalah review yang bisa diverifikasi: lihat rencana eksekusi, cek cardinality, pastikan index benar-benar dipakai, dan evaluasi apakah pagination masih masuk akal saat jumlah data membesar.

Di tengah tren kritik seperti “Your AI slop bores me”, sudut pandang yang sehat untuk engineer adalah ini: AI boleh dipakai untuk membantu membuat hipotesis, tetapi jangan jadikan output AI sebagai keputusan akhir. Untuk query lambat, keputusan teknis harus ditopang oleh bukti dari database: EXPLAIN atau EXPLAIN ANALYZE, pola akses data nyata, ukuran tabel, distribusi nilai, dan perilaku aplikasi saat trafik meningkat.

Mengapa review query lambat tidak boleh berhenti di jawaban AI

AI sering bisa menebak pola umum dengan benar, misalnya:

  • query lambat karena full scan,
  • sorting mahal karena tidak ada index yang cocok,
  • OFFSET besar membuat pagination makin mahal,
  • query list dan query count tidak konsisten.

Masalahnya, saran semacam itu baru berguna setelah diverifikasi. Dua query yang terlihat mirip bisa punya perilaku berbeda karena:

  • urutan kolom pada index berbeda,
  • selectivity filter rendah,
  • jumlah baris hasil jauh lebih besar dari perkiraan optimizer,
  • join order berubah,
  • statistik tabel tidak akurat atau belum mutakhir,
  • aplikasi mengambil kolom terlalu banyak sehingga index saja tidak cukup.

Prinsip kerja praktis: gunakan AI untuk membantu menyusun checklist atau hipotesis optimasi, tetapi pastikan perubahan diputuskan berdasarkan execution plan dan pengukuran, bukan intuisi atau teks yang terdengar meyakinkan.

Langkah audit query lambat yang bisa dipraktikkan

1. Kumpulkan query yang benar-benar bermasalah

Jangan optimasi berdasarkan dugaan. Ambil query dari sumber yang nyata:

  • slow query log,
  • APM atau tracing,
  • query log aplikasi,
  • endpoint yang memang terasa lambat di produksi atau staging.

Catat konteksnya:

  • parameter query,
  • jumlah data saat query dijalankan,
  • apakah berjalan di halaman awal atau halaman jauh,
  • berapa banyak baris yang dikembalikan,
  • apakah query dipanggil berulang dalam satu request.

Ini penting karena query pagination halaman 1 sering terlihat aman, tetapi halaman 500 bisa jauh lebih mahal.

2. Jalankan EXPLAIN atau EXPLAIN ANALYZE

Tujuan utamanya bukan sekadar “apakah pakai index”, melainkan memahami:

  • bagian mana yang membaca banyak row,
  • apakah ada scan besar sebelum filter diterapkan,
  • apakah sorting dilakukan di banyak data,
  • apakah estimasi row masuk akal dibanding hasil aktual.

Contoh query list yang sering muncul pada dashboard admin:

SELECT id, user_id, status, created_at
FROM orders
WHERE tenant_id = 42
  AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20 OFFSET 2000;

Untuk query seperti ini, review minimalnya:

  • apakah filter tenant_id dan status dipakai sebelum sort,
  • apakah database bisa mengambil data sesuai urutan created_at DESC dari index,
  • berapa banyak row yang tetap harus discan sebelum mencapai offset yang diminta.

Jika database harus membaca ribuan atau jutaan row hanya untuk membuang sebagian besar hasil karena OFFSET, maka bottleneck-nya bukan hanya filter, tetapi juga strategi paginasi.

3. Cek cardinality dan selectivity

Cardinality secara praktis membantu menjawab: seberapa “membedakan” suatu kolom untuk kebutuhan pencarian. Kolom dengan nilai sangat sedikit, misalnya status yang hanya berisi paid, pending, failed, sering tidak cukup selektif jika di-index sendirian.

Contoh kesalahan umum:

CREATE INDEX idx_orders_status ON orders(status);

Index ini belum tentu banyak membantu untuk query berikut:

SELECT id, tenant_id, status, created_at
FROM orders
WHERE tenant_id = 42
  AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;

Karena filter nyata bukan hanya status, melainkan kombinasi tenant_id + status, lalu hasilnya perlu diurutkan berdasarkan created_at. Pada kasus seperti ini, composite index biasanya lebih relevan daripada index tunggal yang terpisah-pisah.

Masalah nyata #1: filter + sort tanpa composite index

Salah satu sumber query lambat yang paling sering adalah pola ini:

  • ada filter di beberapa kolom,
  • ada ORDER BY,
  • tetapi index hanya menutupi sebagian kondisi.

Contoh query:

SELECT id, user_id, total_amount, created_at
FROM orders
WHERE tenant_id = 42
  AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;

Index yang kurang tepat:

CREATE INDEX idx_orders_tenant ON orders(tenant_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at ON orders(created_at);

Di atas kertas terlihat lengkap, tetapi optimizer belum tentu bisa menggabungkan semuanya secara efisien untuk pola filter + sort ini. Dalam banyak kasus, database tetap harus:

  • mengambil sekumpulan row berdasarkan filter,
  • lalu melakukan sort tambahan,
  • atau membaca terlalu banyak row sebelum menemukan 20 hasil teratas.

Index yang lebih sesuai sering berbentuk:

CREATE INDEX idx_orders_tenant_status_created_at
ON orders(tenant_id, status, created_at);

Mengapa ini lebih baik?

  • tenant_id dan status membantu mempersempit kandidat row,
  • created_at membantu menjaga urutan untuk ORDER BY,
  • query LIMIT 20 bisa berhenti lebih cepat setelah menemukan hasil yang cukup.

Kapan composite index benar-benar masuk akal

Pilih composite index jika:

  • pola query sering sama,
  • kolom filter dan sort konsisten,
  • query tersebut penting untuk latensi endpoint utama,
  • penghematan baca lebih besar daripada biaya tambahan saat insert/update.

Kesalahan umum saat menambah index

  • Menambah index per kolom tanpa melihat pola query. Query biasanya butuh kombinasi kolom, bukan koleksi index acak.
  • Mengabaikan urutan kolom pada composite index. Urutan memengaruhi apakah filter dan sort bisa dipakai efektif.
  • Terlalu banyak index. Setiap index menambah biaya write, storage, dan maintenance.
  • Menganggap semua query akan membaik. Satu index yang bagus untuk query list bisa tidak relevan untuk query agregasi lain.
  • Tidak menghapus index redundan. Setelah menambah composite index, beberapa index lama mungkin jadi tidak perlu.

Catatan: jangan menambah index hanya karena AI mengatakan “kemungkinan besar perlu index di kolom X”. Pastikan index itu cocok dengan where clause, order by, dan pola akses yang benar-benar ada.

Masalah nyata #2: OFFSET makin mahal saat data tumbuh

Pola pagination klasik:

SELECT id, user_id, total_amount, created_at
FROM orders
WHERE tenant_id = 42
ORDER BY created_at DESC
LIMIT 20 OFFSET 10000;

Masalah utamanya sederhana: untuk mengambil 20 row pada halaman jauh, database sering tetap harus melewati banyak row sebelumnya. Semakin besar OFFSET, semakin besar kerja yang dibuang.

Gejala yang sering terlihat:

  • halaman awal cepat, halaman belakang lambat,
  • CPU atau I/O naik saat user menelusuri data lama,
  • latensi endpoint list tidak stabil,
  • query tampak “sama”, tetapi waktu eksekusinya membesar seiring pertumbuhan tabel.

Kapan OFFSET masih bisa dipakai

OFFSET masih wajar jika:

  • dataset kecil,
  • halaman dalam tidak diakses sering,
  • kebutuhan UX memang meminta navigasi nomor halaman tradisional,
  • biaya query sudah terbukti masih aman.

Namun, jika data terus bertambah dan user sering membuka halaman jauh, Anda sebaiknya mempertimbangkan keyset pagination.

Memilih keyset pagination

Keyset pagination menggunakan penanda dari item terakhir yang sudah terlihat, bukan menghitung berapa baris yang harus dilewati.

Contoh, untuk urutan created_at DESC, id DESC:

SELECT id, user_id, total_amount, created_at
FROM orders
WHERE tenant_id = 42
  AND (created_at, id) < ('2025-01-10 09:15:00', 987654)
ORDER BY created_at DESC, id DESC
LIMIT 20;

Pendekatan ini umumnya lebih stabil untuk data besar karena database tidak perlu membuang ribuan row demi mencapai halaman berikutnya. Ia langsung melanjutkan dari posisi terakhir.

Kapan keyset pagination lebih tepat

  • timeline, feed, audit log, order history, activity log,
  • data terus bertambah,
  • urutan jelas dan stabil,
  • halaman “next/previous” lebih penting daripada lompat ke halaman 537.

Trade-off keyset pagination

  • Tidak nyaman untuk fitur “lompat ke halaman N”.
  • Perlu kolom pengurutan yang stabil dan deterministik.
  • Biasanya lebih aman jika memakai tie-breaker, misalnya ORDER BY created_at DESC, id DESC agar urutan tidak ambigu.

Jika banyak row punya created_at yang sama dan Anda tidak menambahkan tie-breaker seperti id, hasil pagination bisa lompat atau duplikat antar halaman.

Masalah nyata #3: query count dan list tidak konsisten

Kesalahan ini sangat sering terjadi di endpoint daftar data. Developer membuat dua query:

  • satu query untuk mengambil data list,
  • satu query untuk menghitung total baris.

Masalah muncul ketika filter, join, atau kondisi keduanya tidak sama persis.

Contoh:

-- query list
SELECT id, user_id, status, created_at
FROM orders
WHERE tenant_id = 42
  AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;

-- query count yang tidak konsisten
SELECT COUNT(*)
FROM orders
WHERE tenant_id = 42;

Hasilnya, API bisa mengembalikan total yang tidak sesuai dengan jumlah item yang benar-benar difilter. Ini bukan hanya bug UX; ia juga mengacaukan analisis performa karena Anda menghitung satu populasi tetapi menampilkan populasi lain.

Cara audit query count/list

  • Pastikan WHERE konsisten.
  • Jika query list memakai join yang memengaruhi hasil, pastikan query count menghitung populasi yang sama.
  • Hindari menulis dua versi filter secara manual jika bisa membangun kondisi dari sumber yang sama di layer aplikasi.
  • Jika count terlalu mahal, pertimbangkan apakah total absolut memang diperlukan untuk setiap request.

Kapan COUNT(*) bisa jadi bottleneck

Pada tabel besar dengan filter kompleks, query count bisa lebih mahal daripada query list itu sendiri, terutama jika UI selalu meminta total lengkap padahal pengguna hanya butuh tombol “next”. Dalam kasus seperti itu:

  • gunakan keyset pagination tanpa total halaman,
  • atau hitung total secara terpisah dan tidak di setiap request,
  • atau tampilkan estimasi bila memang sesuai kebutuhan produk dan dapat diterima.

Pilih berdasarkan kebutuhan produk, bukan kebiasaan default.

Contoh audit end-to-end: dari query lambat ke perbaikan

Misalkan endpoint GET /orders terasa lambat untuk tenant besar.

Query awal

SELECT id, user_id, status, total_amount, created_at
FROM orders
WHERE tenant_id = 42
  AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20 OFFSET 5000;

Temuan audit

  1. EXPLAIN menunjukkan pembacaan row tinggi sebelum hasil 20 row ditemukan.
  2. Sort mahal atau database tidak bisa memanfaatkan urutan dari index secara efektif.
  3. OFFSET 5000 menambah kerja yang sebenarnya tidak menghasilkan nilai bisnis.
  4. Index yang ada hanya di tenant_id dan created_at, belum cocok dengan pola filter + sort.

Perbaikan tahap 1: sesuaikan index dengan pola query

CREATE INDEX idx_orders_tenant_status_created_at_id
ON orders(tenant_id, status, created_at, id);

Penambahan id dapat membantu jika Anda ingin membuat urutan yang lebih stabil untuk pagination lanjutan.

Perbaikan tahap 2: ubah pagination jika halaman dalam sering diakses

SELECT id, user_id, status, total_amount, created_at
FROM orders
WHERE tenant_id = 42
  AND status = 'paid'
  AND (created_at, id) < ('2025-01-10 09:15:00', 987654)
ORDER BY created_at DESC, id DESC
LIMIT 20;

Perbaikan tahap 3: audit query count

SELECT COUNT(*)
FROM orders
WHERE tenant_id = 42
  AND status = 'paid';

Setelah itu, ukur ulang. Jangan berhenti di “secara teori lebih baik”. Cek lagi plan, jumlah row yang dibaca, dan latensi endpoint pada parameter yang mewakili kondisi nyata.

Checklist review hasil AI untuk query SQL lambat

Kalau Anda tetap memakai AI untuk draft analisis, gunakan checklist ini sebelum mengubah schema atau query di produksi:

  • Apakah AI membaca query yang benar? Banyak saran salah karena konteks query, parameter, atau join tidak lengkap.
  • Apakah bottleneck dibuktikan dengan EXPLAIN/ANALYZE? Bukan asumsi.
  • Apakah index yang disarankan mengikuti pola WHERE + ORDER BY? Bukan sekadar menebak kolom populer.
  • Apakah urutan kolom pada composite index masuk akal?
  • Apakah query memakai OFFSET besar? Jika ya, evaluasi keyset pagination.
  • Apakah query count dan query list konsisten?
  • Apakah perbaikannya aman untuk beban write? Setiap index menambah biaya insert/update/delete.
  • Apakah ada tie-breaker pada sorting? Penting untuk pagination yang stabil.
  • Apakah statistik database cukup mutakhir untuk optimizer? Jika tidak, plan bisa menyesatkan.
  • Apakah hasil diuji pada data yang representatif? Query yang cepat di data kecil belum tentu aman di produksi.

Aturan praktis: jika saran AI tidak bisa Anda hubungkan ke execution plan, cardinality, atau pola akses data, anggap itu baru hipotesis, bukan solusi.

Debugging tips saat optimasi belum berhasil

Jangan hanya melihat “pakai index atau tidak”

Sebuah query bisa tetap lambat meskipun “pakai index” jika index tersebut membaca terlalu banyak row. Fokus pada berapa banyak data yang tetap harus disentuh, bukan sekadar keberadaan index.

Perhatikan kolom yang diambil

Jika Anda mengambil terlalu banyak kolom besar atau tidak diperlukan, biaya I/O tetap tinggi. Untuk endpoint list, pilih kolom yang benar-benar ditampilkan.

Uji halaman awal dan halaman dalam

Pagination sering menipu. Halaman 1 bisa cepat, tetapi halaman 300 sangat mahal. Uji keduanya.

Waspadai data skew

Filter yang terlihat bagus di tenant kecil bisa buruk di tenant besar. Distribusi data nyata sangat memengaruhi efektivitas index.

Jangan lupa dampak write

Setelah menambah beberapa index, baca memang bisa lebih cepat, tetapi write bisa melambat. Untuk tabel dengan volume insert tinggi, setiap index tambahan harus punya alasan kuat.

Kapan memilih solusi yang mana

Pilih optimasi index jika

  • query lambat karena filter dan sort tidak cocok dengan index,
  • pola query stabil,
  • halaman awal pun sudah lambat,
  • biaya write tambahan masih bisa diterima.

Pilih keyset pagination jika

  • masalah utama ada pada OFFSET besar,
  • data terus bertambah,
  • alur pengguna lebih sering “lihat berikutnya” daripada lompat ke halaman tertentu,
  • Anda bisa menyediakan urutan yang stabil.

Pilih redesign API/UI jika

  • query count terlalu mahal tetapi tidak terlalu penting untuk pengguna,
  • fitur pagination numerik justru memaksa desain query yang buruk,
  • list terlalu besar dan seharusnya dipersempit dengan filter yang lebih spesifik.

Penutup

Review query lambat tanpa AI slop berarti menolak jawaban yang terdengar pintar tetapi tidak dibuktikan. Untuk kasus yang paling sering muncul di aplikasi bisnis, fokus Anda seharusnya jelas: verifikasi dengan EXPLAIN/ANALYZE, cek cardinality, pastikan index sesuai pola filter dan sort, dan jangan mengabaikan bahwa OFFSET akan makin mahal saat data tumbuh.

Kalau harus merangkum jadi satu aturan kerja: gunakan AI untuk membantu berpikir, bukan untuk menggantikan proses audit database. Query lambat diperbaiki dengan bukti, bukan dengan teks yang terdengar yakin.