Query yang awalnya cepat bisa melambat drastis ketika volume data tumbuh tanpa batas. Solusi langsungnya adalah menerapkan indexing responsif, pagination bertahap, dan batching yang selaras dengan fitur JavaScript modern agar helper database tetap ringkas. Artikel ini menunjukkan langkah praktis mulai dari analisis EXPLAIN hingga pengukuran peningkatan performa.

Anda akan mendapat pendekatan konkret untuk mengenali kolom yang perlu diindeks, membangun pagination adaptif dengan cursor, dan menghitung statistik perbaikan tanpa mengorbankan keterbacaan kode.

Menentukan Bottleneck Lewat EXPLAIN dan Data Profiling

Sebelum menambah indeks atau mengubah pagination, pahami eksekusi query melalui EXPLAIN. Gunakan ANALYZE agar planner menjalankan query dan menunjukkan waktu nyata.

EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
SELECT id, user_id, status
FROM orders
WHERE status = 'confirmed'
ORDER BY created_at DESC
LIMIT 100;

Perhatikan kolom yang menjadi sequential scan atau menyebabkan sorting besar. Jika planner tidak memakai indeks untuk kondisi WHERE atau ORDER BY, tambahkan indeks komposit yang mencakup kolom filter + sort. Contohnya dengan PostgreSQL:

CREATE INDEX CONCURRENTLY idx_orders_status_created
ON orders (status, created_at DESC);

Indeks ini membantu query yang memfilter status lalu mengurutkan menurut created_at. Tambahkan CONCURRENTLY agar tidak mengunci tabel produksi.

Perhatikan trade-off: indeks komposit memperpanjang waktu insert/update karena harus menjaga struktur tambahan. Lakukan analisis frekuensi operasi tulis dan pertimbangkan indeks partial jika hanya subset data yang selalu di-query (misalnya WHERE status = 'confirmed').

Indexing Responsif saat Data Bertambah

Indexing responsif berarti menjaga indeks tetap relevan seiring pola query berubah. Beberapa langkah praktis:

  • Identifikasi kolom paling sering muncul di WHERE + ORDER BY dari slow query log atau observability tool.
  • Buat indeks partial untuk filter dominan, misalnya CREATE INDEX ON orders (created_at DESC) WHERE status = 'confirmed';.
  • Gunakan multi-column index jika query memanfaatkan kombinasi kolom, tapi hindari indeks yang menyertakan terlalu banyak kolom karena ukuran bertambah.
  • Periksa statistik dengan ANALYZE agar planner tahu distribusi data terbaru.

Selalu jalankan EXPLAIN ulang setelah menambahkan indeks untuk memastikan Index Scan digunakan. Jika tidak, mungkin statistik tidak cukup akurat atau indeks tidak sesuai urutan kolom.

Pagination Bertahap dan Batching dengan Helper Node Ringkas

Pagination offset tradisional (misalnya OFFSET 10000) menjadi lambat saat data besar karena database tetap memindai baris sebelumnya. Gunakan pagination dengan cursor untuk melanjutkan dari posisi terakhir tanpa memindai ulang.

const client = await getDbClient();

const fetchOrders = async ({ cursor, limit = 150 } = {}) => {
  cursor ??= { created_at: new Date(0), id: 0 };

  const { rows } = await client.query(
    `SELECT id, created_at, status
     FROM orders
     WHERE (created_at, id) > ($1, $2)
     ORDER BY created_at ASC, id ASC
     LIMIT $3`,
    [cursor.created_at, cursor.id, limit]
  );

  const lastRow = rows.at(-1);
  const nextCursor = lastRow
    ? { created_at: lastRow.created_at, id: lastRow.id }
    : null;

  return { rows, nextCursor };
};

Kombinasikan cursor dengan batching agar Anda bisa mengorkestrasi pengambilan data lebih besar tanpa membebani memori aplikasi. Contoh orchestrator:

const processBatches = async () => {
  let cursor;
  do {
    const { rows, nextCursor } = await fetchOrders({ cursor });
    await doWork(rows);
    cursor = nextCursor;
  } while (cursor);
};

await processBatches();

Perhatikan penggunaan fitur modern JavaScript: cursor ??= ... menjaga helper tetap ringkas, rows.at(-1) melejitkan akses baris terakhir, dan optional chaining (nextCursor?.id) membantu meminimalkan err handling ketika daftar kosong.

Pagination cursor juga memudahkan pengukuran throughput; jika pengambilan batch dipercepat dengan indeks baru, Anda melihat jumlah batch per detik naik karena setiap query hanya mengembalikan baris yang diperlukan.

Batching Lebih Besar dengan Level Kontrol

Untuk pemrosesan background atau API batch, atur ukuran batch sesuai kemampuan database. Gunakan metrik latency database untuk menentukan batas atas. Contoh orchestrator di atas bisa diperluas dengan delay adaptif:

const backoff = (err) => {
  if (err.code === '53300') {
    return new Promise((resolve) => setTimeout(resolve, 200));
  }
  throw err;
};

try {
  await processBatches();
} catch (err) {
  await backoff(err);
  await processBatches();
}

Jangan lupa batasi jumlah connection pooling saat batch berjalan agar tidak mengganggu traffic API lain.

Mengukur dan Memantau Performa Setelah Optimasi

Setelah indeks atau pagination diubah, ukur hasilnya.

  • Jalankan EXPLAIN (ANALYZE) setelah perubahan indeks dan bandingkan total_time atau planning_time.
  • Gunakan metrik aplikasi (misalnya histogram latency) untuk melihat perubahan distribusi respon.
  • Catat throughput batch di log (rows.length / waktu) sehingga Anda dapat membandingkan sebelum dan sesudah optimasi.
  • Aktifkan slow query log untuk memastikan query tidak lagi muncul di threshold lama.

Jika perbaikan tidak cukup, pastikan tidak ada parameter query yang berubah (NULL vs nilai), dan cek apakah aplikasi terkadang mengambil data dari wilayah waktu yang sangat tua; hal ini bisa membuat indeks tidak efektif.

Dalam kasus penulisan data tinggi, pertimbangkan replikasi baca sehingga read-heavy query dengan pagination tidak menghambat cluster utama.

Kesimpulan

Indexing adaptif dan pagination bertahap dengan batching modern membuat query lambat di Node tetap terkendali saat data tumbuh. Analisis EXPLAIN memastikan indeks dipakai dengan benar, sementara helper pagination yang memanfaatkan optional chaining dan logical assignment menjaga kode tetap rapi. Pengukuran yang konsisten—baik dari EXPLAIN maupun metrik throughput—menjamin setiap perubahan memberikan dampak nyata.