Utang query dari AI biasanya tidak terasa saat data masih ribuan baris. Masalahnya muncul ketika traffic naik, tabel bertambah besar, lalu query yang tampak rapi dari hasil bantuan AI mulai memicu full scan, sorting mahal, pagination lambat, dan lonjakan latency pada endpoint yang tadinya aman.

Kalau Anda menerima SQL atau ORM query dari AI, anggap itu sebagai draft yang harus diaudit, bukan hasil final. Fokus audit yang paling sering menyelamatkan sistem adalah: apakah OFFSET akan makin lambat, apakah COUNT(*) terlalu mahal, apakah index benar-benar dipakai, apakah pola akses memicu N+1 query, dan apakah kombinasi filter + sort sesuai dengan index yang tersedia.

Mengapa query hasil AI bisa menjadi utang performa

AI cenderung menghasilkan query yang benar secara fungsi, tetapi belum tentu efisien untuk distribusi data, pola traffic, dan kebutuhan pagination di sistem nyata. Di lingkungan pengembangan, query itu sering lolos karena:

  • dataset kecil sehingga full scan belum terasa,
  • developer hanya menguji halaman awal, bukan halaman ke-1000,
  • latency rata-rata masih terlihat normal, padahal ekor distribusi (p95, p99) mulai memburuk,
  • ORM menyamarkan jumlah query yang sebenarnya dieksekusi.

Inilah yang membuat query dari AI mirip utang kecil: mudah diambil, terlihat murah di awal, lalu biayanya membesar saat skala naik.

Gejala nyata yang perlu dicurigai

1. OFFSET makin besar, makin lambat

Pagination berbasis LIMIT ... OFFSET ... mudah dibuat dan sering direkomendasikan AI karena sederhana. Masalahnya, database tetap harus melewati atau menghitung banyak baris sebelum sampai ke halaman yang diminta.

SELECT id, created_at, total
FROM orders
WHERE status = 'paid'
ORDER BY created_at DESC
LIMIT 20 OFFSET 20000;

Query di atas bisa tetap lambat walaupun hasil akhirnya hanya 20 baris. Semakin besar OFFSET, semakin banyak pekerjaan yang dibuang.

2. COUNT(*) mahal pada daftar besar

Banyak endpoint pagination menampilkan total data. AI sering menghasilkan pasangan query seperti ini:

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

SELECT id, created_at, total
FROM orders
WHERE status = 'paid'
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;

Secara fungsi benar, tetapi COUNT(*) pada kondisi tertentu bisa mahal, terutama jika filternya tidak selektif, index tidak cocok, atau query harus membaca bagian tabel yang besar. Pada beban tinggi, query hitung ini sering ikut menjadi bottleneck, meski data list-nya sendiri masih terasa cepat.

3. Index ada, tetapi tidak dipakai

Banyak tim berasumsi bahwa keberadaan index otomatis mempercepat query. Tidak selalu. Query bisa gagal memakai index secara efektif jika:

  • fungsi diterapkan pada kolom yang difilter,
  • urutan kolom pada index komposit tidak cocok dengan pola WHERE dan ORDER BY,
  • kondisi terlalu tidak selektif,
  • sorting tidak sejalan dengan index,
  • tipe data atau casting membuat optimizer kesulitan.

Contoh yang sering muncul:

SELECT id, email
FROM users
WHERE DATE(created_at) = '2025-01-01'
ORDER BY created_at DESC;

Karena ada fungsi DATE(created_at), database sering tidak bisa memakai index pada created_at secara optimal. Versi yang lebih ramah index biasanya berupa rentang waktu:

SELECT id, email
FROM users
WHERE created_at >= '2025-01-01 00:00:00'
  AND created_at <  '2025-01-02 00:00:00'
ORDER BY created_at DESC;

4. N+1 query dari ORM atau loop aplikasi

AI juga sering menghasilkan kode aplikasi yang tampak bersih tetapi menembakkan query tambahan di dalam loop. Misalnya mengambil 50 order, lalu untuk tiap order mengambil user atau item terkait secara terpisah. Hasilnya bisa menjadi 51, 101, atau lebih banyak query untuk satu request.

orders = db.query("SELECT id, user_id, total FROM orders ORDER BY created_at DESC LIMIT 50")
for order in orders:
    user = db.query("SELECT id, name FROM users WHERE id = ?", [order.user_id])

Secara lokal mungkin masih lolos. Di produksi, pola ini memperbesar latency dan membebani pool koneksi database.

5. Filter dan sort memicu full scan atau filesort

Query sering terlihat aman sampai pengguna diberi filter dinamis: status, rentang tanggal, kategori, pencarian, sort terbaru, sort harga, sort nama. Kombinasi ini bisa dengan cepat menghasilkan rencana eksekusi yang jauh lebih mahal dibanding pengujian awal.

Contohnya:

SELECT id, title, price, created_at
FROM products
WHERE status = 'active' AND category_id = 12
ORDER BY price ASC
LIMIT 20 OFFSET 0;

Kalau index yang ada hanya pada status atau hanya pada created_at, database mungkin tetap harus memindai banyak baris lalu mengurutkannya secara terpisah.

Cara audit query AI-generated sebelum jadi masalah besar

1. Baca EXPLAIN, jangan berhenti di “query berjalan”

Langkah pertama adalah melihat rencana eksekusi dengan EXPLAIN atau fasilitas setara pada database yang Anda pakai. Tujuannya bukan mencari istilah tertentu secara buta, tetapi memahami:

  • apakah query memakai index atau full scan,
  • berapa estimasi baris yang dibaca,
  • di mana sorting dilakukan,
  • apakah join mendorong pembacaan berulang,
  • apakah filter diterapkan sebelum atau sesudah pembacaan besar.

Contoh sederhana:

EXPLAIN
SELECT id, created_at, total
FROM orders
WHERE status = 'paid'
ORDER BY created_at DESC
LIMIT 20 OFFSET 20000;

Kalau rencana menunjukkan pembacaan baris sangat besar untuk mengembalikan hasil kecil, itu sinyal kuat bahwa pagination dan index perlu diubah.

Catatan: EXPLAIN menunjukkan rencana, bukan selalu biaya nyata di produksi. Cocokkan dengan metrik runtime, terutama pada beban dan ukuran data yang mendekati kondisi sebenarnya.

2. Cek cardinality dan selectivity

Index paling berguna ketika membantu database menyaring data secara signifikan. Karena itu, jangan sekadar bertanya “kolom ini sering dipakai di WHERE atau tidak”, tetapi juga “seberapa selektif nilainya”.

  • Cardinality secara praktis mengarah pada seberapa beragam nilai dalam kolom.
  • Selectivity menggambarkan seberapa kecil subset data yang dipilih oleh suatu kondisi.

Contoh:

  • status dengan nilai hanya active, inactive, deleted biasanya kurang selektif jika dipakai sendirian.
  • user_id, created_at rentang sempit, atau kombinasi (status, created_at) bisa jauh lebih membantu.

Ini sebabnya index tunggal pada kolom boolean atau kolom dengan sedikit variasi sering tidak memberi dampak besar untuk query list besar.

3. Ukur p95 dan p99, bukan hanya rata-rata

Rata-rata latency sering menipu. Query pagination halaman awal mungkin cepat, sementara halaman lebih dalam atau kombinasi filter tertentu menyebabkan lonjakan besar di persentil atas.

Saat mengaudit endpoint yang memakai query hasil AI, ukur minimal:

  • latency median atau p50 untuk gambaran umum,
  • p95 untuk beban mayoritas pengguna,
  • p99 untuk kasus buruk yang tetap sering terjadi,
  • jumlah query per request,
  • rows examined atau metrik setara jika tersedia.

Jika p50 bagus tetapi p95/p99 buruk, kemungkinan ada pola akses spesifik seperti offset besar, sort tertentu, atau N+1 yang tidak muncul di jalur paling umum.

4. Uji query dengan data yang realistis

Menguji query pada tabel berisi ribuan baris sering tidak cukup. Anda tidak perlu menunggu produksi penuh, tetapi perlu dataset yang mewakili:

  • rasio data aktif dan nonaktif,
  • sebaran tanggal,
  • kepadatan kategori atau tenant tertentu,
  • hot partition atau user dengan data sangat banyak.

Query yang tampak aman pada distribusi seragam bisa runtuh pada data yang sangat condong.

OFFSET vs keyset pagination: kapan harus pindah

Masalah dasar OFFSET pagination

OFFSET cocok untuk kebutuhan sederhana: implementasi mudah dan UX nomor halaman lebih langsung. Namun secara performa, pendekatan ini memburuk saat pengguna menelusuri halaman dalam, terutama bila dikombinasikan dengan ORDER BY pada dataset besar.

Keyset pagination sebagai alternatif yang lebih stabil

Untuk daftar yang diurutkan berdasarkan kolom stabil seperti created_at atau id, keyset pagination biasanya lebih efisien. Alih-alih berkata “lompat ke baris ke-20001”, aplikasi berkata “ambil 20 baris setelah nilai terakhir yang sudah saya lihat”.

Sebelum:

SELECT id, created_at, total
FROM orders
WHERE status = 'paid'
ORDER BY created_at DESC, id DESC
LIMIT 20 OFFSET 20000;

Sesudah, dengan cursor dari item terakhir sebelumnya:

SELECT id, created_at, total
FROM orders
WHERE status = 'paid'
  AND (created_at, id) < ('2025-01-15 10:30:00', 845120)
ORDER BY created_at DESC, id DESC
LIMIT 20;

Kenapa ini lebih baik? Karena database bisa melanjutkan pencarian dari posisi tertentu pada index, bukan membuang ribuan baris lebih dulu.

Trade-off keyset pagination

  • Lebih efisien untuk scroll atau next/previous.
  • Kurang cocok jika UX sangat bergantung pada nomor halaman absolut.
  • Perlu kolom pengurutan yang stabil dan deterministik.
  • Sering perlu tie-breaker tambahan, misalnya id, agar urutan konsisten saat ada nilai created_at yang sama.

Kalau sistem Anda menampilkan feed, riwayat transaksi, audit log, atau daftar event, keyset biasanya pilihan yang lebih aman dibanding offset.

Menentukan index komposit dari pola WHERE + ORDER BY

Salah satu sumber utang query dari AI adalah saran index yang terlalu generik, misalnya “tambahkan index di semua kolom filter”. Itu tidak cukup. Yang penting adalah pola query aktual.

Contoh audit query dan perbaikan index

Misalkan query utama aplikasi adalah:

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

Jika pola ini dominan, index komposit yang masuk akal biasanya mengikuti kolom filter lalu kolom sort yang dipakai terus-menerus. Contoh pendekatan:

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

Kenapa pendekatan ini sering bekerja:

  • tenant_id dan status membantu mempersempit subset data lebih dulu,
  • created_at, id mendukung urutan yang konsisten untuk pagination,
  • database bisa menghindari sorting besar jika urutan index cocok dengan query.

Namun, ini bukan aturan kaku untuk semua kasus. Bila status sangat tidak selektif dan hampir semua query selalu per tenant, bisa saja bobot utamanya ada pada tenant_id lalu created_at. Karena itu, desain akhir harus dikonfirmasi dengan EXPLAIN dan metrik nyata.

Kesalahan umum saat membuat index komposit

  • Membuat banyak index tumpang tindih tanpa menghapus yang tidak lagi berguna.
  • Menaruh kolom dalam urutan yang tidak mencerminkan query dominan.
  • Menganggap satu index bisa optimal untuk semua kombinasi filter dan semua sort.
  • Melupakan bahwa query dengan ORDER BY price dan query dengan ORDER BY created_at sering butuh strategi berbeda.

Contoh sebelum/sesudah untuk filter yang memicu full scan

Sebelum:

SELECT id, title, price
FROM products
WHERE status = 'active'
  AND category_id = 12
ORDER BY price ASC
LIMIT 20 OFFSET 0;

Jika pola ini sering dipakai, pendekatan sesudah bisa berupa:

CREATE INDEX idx_products_status_category_price_id
ON products (status, category_id, price, id);

SELECT id, title, price
FROM products
WHERE status = 'active'
  AND category_id = 12
ORDER BY price ASC, id ASC
LIMIT 20;

Menambahkan id sebagai tie-breaker membantu urutan yang stabil, terutama bila banyak produk memiliki harga sama.

Kapan index baru justru merugikan

Menambah index bukan gratis. Setiap index tambahan menambah biaya pada operasi tulis karena database juga harus memperbarui struktur index saat INSERT, UPDATE, atau DELETE.

Index baru bisa merugikan jika:

  • tabel sangat write-heavy,
  • index jarang dipakai oleh query nyata,
  • ada banyak index mirip dengan perbedaan kecil,
  • kolom yang sering berubah dimasukkan ke banyak index,
  • ukuran index memperbesar penggunaan storage dan pressure pada cache.

Tanda praktis bahwa index perlu dipertanyakan:

  • throughput tulis turun setelah penambahan index,
  • lock, contention, atau waktu commit memburuk,
  • query lambat yang ingin dibantu ternyata tetap tidak memakai index baru,
  • terdapat beberapa index yang saling tumpang tindih untuk pola query serupa.

Prinsip aman: tambahkan index untuk pola query yang memang penting, sering, dan terukur. Jangan membuat index hanya karena AI menyebut kolom itu “sering difilter”.

Mengurangi N+1 query sebelum masuk produksi

Untuk query AI-generated di level aplikasi, audit tidak berhenti pada SQL tunggal. Periksa juga jumlah round-trip ke database.

Sebelum: query dalam loop

orders = db.query("SELECT id, user_id, total FROM orders ORDER BY created_at DESC LIMIT 50")
for order in orders:
    user = db.query("SELECT id, name FROM users WHERE id = ?", [order.user_id])

Sesudah: join atau eager loading yang terkontrol

SELECT o.id, o.total, o.created_at, u.id AS user_id, u.name
FROM orders o
JOIN users u ON u.id = o.user_id
ORDER BY o.created_at DESC
LIMIT 50;

Atau bila memakai ORM, gunakan mekanisme eager loading yang memang dirancang untuk mencegah query per item. Setelah itu, ukur lagi jumlah query per request. Ini salah satu audit paling sederhana dan paling berdampak.

Checklist review PR untuk query AI-generated

Gunakan checklist ini saat meninjau PR yang berisi SQL baru, perubahan endpoint list, atau kode ORM hasil bantuan AI.

  1. Apakah query diuji pada data yang realistis?
    Jangan hanya memakai database lokal kecil.
  2. Apakah ada EXPLAIN untuk query utama?
    Simpan hasil atau ringkasan temuannya di PR jika query kritikal.
  3. Apakah pagination masih memakai OFFSET besar?
    Jika ya, pertimbangkan keyset pagination.
  4. Apakah endpoint selalu menjalankan COUNT(*)?
    Pastikan total count memang dibutuhkan dan biayanya dipahami.
  5. Apakah filter dan ORDER BY sesuai dengan index yang ada?
    Cek urutan kolom dalam index komposit.
  6. Apakah query menerapkan fungsi pada kolom yang diindex?
    Hindari pola yang mempersulit penggunaan index.
  7. Apakah ada risiko N+1?
    Hitung query per request, bukan hanya melihat satu query utama.
  8. Apakah sorting stabil?
    Tambahkan tie-breaker seperti id bila perlu.
  9. Apakah index baru punya biaya write yang dapat diterima?
    Tinjau dampaknya pada insert/update/delete.
  10. Apakah metrik p95/p99 tersedia setelah perubahan?
    Minimal pada staging atau observability produksi setelah deploy bertahap.

Strategi praktis agar utang query dari AI tidak menumpuk

1. Jadikan AI sebagai pembuat draft, bukan penentu final

AI berguna untuk mempercepat eksplorasi query, tetapi keputusan final harus berbasis pola akses data Anda sendiri. Selalu validasi dengan EXPLAIN, metrik, dan ukuran data yang masuk akal.

2. Mulai dari query dominan, bukan semua query

Fokus pada endpoint yang paling sering dipakai, paling sensitif terhadap latency, atau paling mahal di database. Biasanya 20% query menyumbang 80% beban.

3. Simpan kontrak query untuk endpoint penting

Untuk daftar besar, dokumentasikan:

  • pola filter yang didukung,
  • sort yang diizinkan,
  • strategi pagination,
  • index yang menopang query tersebut.

Ini membantu reviewer membedakan query yang masih sejalan dengan desain dari query baru yang diam-diam merusak performa.

4. Audit ulang saat fitur filter bertambah

Endpoint yang awalnya aman bisa memburuk setelah ditambah pencarian teks, sort baru, atau filter opsional. Setiap kombinasi baru berpotensi mengubah rencana eksekusi.

Penutup

Utang query dari AI bukan berarti AI buruk untuk produktivitas. Masalahnya muncul ketika query yang benar secara fungsi diterima tanpa audit performa. Pada skala kecil, semuanya tampak baik-baik saja. Saat data tumbuh, gejalanya muncul jelas: OFFSET makin lambat, COUNT(*) mahal, index tidak terpakai, N+1 lolos, dan filter/sort memicu full scan.

Audit yang paling berguna biasanya sederhana tetapi disiplin: baca EXPLAIN, pahami selectivity, ukur p95/p99, pertimbangkan keyset pagination, dan rancang index komposit dari pola WHERE + ORDER BY yang benar-benar dominan. Jika itu dilakukan sejak review PR, utang kecil dari query AI tidak sempat meledak menjadi insiden produksi.