tutorial

77 N+1 Problem dan Solusinya dengan DataLoader

77 N+1 Problem dan Solusinya dengan DataLoader

N+1 problem adalah istilah klasik dalam pengembangan perangkat lunak ketika kita berurusan dengan akses ke data yang memiliki relasi, terutama di lingkungan ORM (Object Relational Mapping) atau query berbasis GraphQL/REST. Fenomena ini kerap menimbulkan masalah efisiensi yang berdampak pada performa aplikasi, dan seringkali baru tersadari setelah aplikasi live dan adanya keluhan performa dari end-user atau pemilik bisnis. Salah satu solusi yang naik daun dalam beberapa tahun terakhir adalah pendekatan DataLoader.

Artikel ini membedah 77 N+1 Problem (bukan hanya jumlah total kasus ya, melainkan ilustrasi dari betapa masifnya masalah ini!) dan menyoroti solusi efektif nan elegan menggunakan DataLoader. Kita akan mengupas secara rinci dengan simulasi kode, serta visualisasi arsitektur bagaimana DataLoader menyelamatkan kita dari jebakan kueri redundant.


Daftar Isi

  1. Apa itu N+1 Problem?
  2. Contoh Kasus dalam Aplikasi
  3. Dampak N+1 Problem
  4. Solusi: Eager Loading & DataLoader
  5. Implementasi DataLoader di Node.js
  6. Eksperimen dan Simulasi Kode
  7. Tabel Perbandingan
  8. Diagram Alur DataLoader
  9. Best Practices
  10. Kesimpulan

Apa itu N+1 Problem?

N+1 problem secara sederhana terjadi ketika pada saat mengambil resource/objek induk (N objek), lalu untuk tiap item juga melakukan query tambahan 1x ke resource relasi-nya. Alhasil, total query adalah N+1. Pada data yang besar, masalah ini menjadi membahayakan performa sistem.

Ilustrasi:

Misalkan kita ingin menampilkan daftar 10 artikel beserta penulisnya.

  1. Query 1: SELECT * FROM articles LIMIT 10;
  2. Query 2-11: Satu per satu, untuk setiap artikel, ambil data penulis
    SELECT * FROM authors WHERE id = ?;

Total ada 1+10 = 11 query hanya untuk data yang seharusnya bisa diambil jauh lebih efisien.


Contoh Kasus dalam Aplikasi

Katakan kita punya model Article & Author:

// Pseudo-code Model SQL
Article { id, title, author_id }
Author  { id, name }

Kode tanpa optimasi (N+1 occuring):

const articles = await db.query('SELECT * FROM articles LIMIT 10');
for (const article of articles) {
  article.author = await db.query('SELECT * FROM authors WHERE id = ?', [article.author_id]);
}

Jika data article ada 10, total query ke database = 11 kali.


Dampak N+1 Problem

  • Overhead Database: Meningkat pesat, apalagi jika traffic tinggi.
  • Latency meningkat: Tiap query ke DB butuh waktu (I/O), multiply N+1 bisa sangat lama.
  • Scalability Problem: Membuat scaling menjadi mahal dan ribet.
  • Cost: Koneksi database biasanya dibatasi, mengurangi performa seluruh aplikasi.

Tabel Dampak N+1

Jumlah DataQuery NormalQuery N+1Query Naif (tanpa batch)
1122
1011111
1001101101
1000110011001
1000011000110001

Solusi: Eager Loading & DataLoader

Eager Loading

Eager loading adalah teknik ORM/SQL untuk mengambil data beserta relasinya sekali waktu, biasanya dengan JOIN atau sub-query.
Contoh:

SELECT articles.*, authors.*
FROM articles
JOIN authors ON authors.id = articles.author_id

DataLoader

DataLoader adalah library pattern populer (diciptakan oleh tim Facebook, sering dipakai di GraphQL API) untuk batching dan caching permintaan data yang sama atau sejenis dalam satu request/iterasi eventloop.

Fungsi utamanya:

  • Batching: Mengumpulkan permintaan akses ke resource yang sama ke dalam satu batch query
  • Caching: Menghindari query berulang untuk key yang sama pada satu request

Implementasi DataLoader di Node.js

Install library dataloader:

npm install dataloader

Setup DataLoader

const DataLoader = require('dataloader');

// Inisialisasi DataLoader
const authorLoader = new DataLoader(async (authorIds) => {
  // Batch query
  const rows = await db.query('SELECT * FROM authors WHERE id IN (?)', [authorIds]);
  // Mapping hasil ke urutan authorIds
  return authorIds.map(id => rows.find(row => row.id === id));
});

Menggunakan DataLoader

const articles = await db.query('SELECT * FROM articles LIMIT 10');
for (const article of articles) {
  article.author = await authorLoader.load(article.author_id);
}

Jadi, meskipun ada 10 artikel, hanya ada 2 query:

  • 1 query ambil articles
  • 1 query (batch) ambil semua authors yang terkait

Eksperimen dan Simulasi Kode

Mari bandingkan dua pendekatan:
Simulasi sederhana dengan pseudo-kode dan console log.

// Without DataLoader (N+1 Problem)
const articles = await db.query('SELECT * FROM articles LIMIT 5');
// Query Counter
let queryCounter = 1;
for (const article of articles) {
  queryCounter++;
  article.author = await db.query('SELECT * FROM authors WHERE id = ?', [article.author_id]);
}
console.log('Total queries (tanpa DataLoader):', queryCounter);

// With DataLoader
const authorLoader = new DataLoader(async (authorIds) => {
  queryCounter++;
  const rows = await db.query('SELECT * FROM authors WHERE id IN (?)', [authorIds]);
  return authorIds.map(id => rows.find(row => row.id === id));
});

for (const article of articles) {
  article.author = await authorLoader.load(article.author_id);
}
console.log('Total queries (pakai DataLoader):', queryCounter);

Output:

Total queries (tanpa DataLoader): 6
Total queries (pakai DataLoader): 2

Tabel Perbandingan

MetodeJumlah QueryLatencyI/O DatabasePenjelasan
Naive/N+1N+1TinggiTinggiQuery child dipanggil per-item
Eager Loading1RendahRendahQuery join
DataLoader2RendahRendahBatch + cache per request

Diagram Alur DataLoader

Mari visualisasikan dengan diagram Mermaid.

flowchart TD
    A[Minta daftar articles] --> B["SELECT * FROM articles"]
    B --> C{Loop setiap article}
    C --> D["DataLoader.load(article.author_id)"]
    D --> E[[Batch collect author_ids]]
    E --> F["SELECT * FROM authors WHERE id IN (author_ids)"]
    F --> G[Map hasil ke setiap article]
    G --> H[Tampilkan hasil]

DataLoader secara otomatis mendeteksi permintaan load multiple key, dan baru menjalankan batch query, lalu memetakan hasil ke request caller asinkron-nya.


Best Practices

  • DataLoader per request: Inisialisasi DataLoader tiap request, bukan global singleton, agar cache tidak tercampur antar user/request.
  • Batch size: Jika child data sangat banyak, bisa atur batch max size untuk mencegah query IN terlalu besar.
  • Combining with Eager Loading: Untuk kasus tertentu join lebih efisien, pilih sesuai query pattern.
  • Monitoring: Selalu profiling dan monitor query database.

Kesimpulan

N+1 problem adalah anti-pattern yang sering tidak disadari oleh developer, namun dampaknya signifikan pada performa aplikasi, utamanya pada sistem database relasional dan API GraphQL.

DataLoader adalah solusi pattern yang sederhana tapi powerful, dengan prinsip batching dan caching per request, sangat cocok untuk API modern yang memerlukan fleksibilitas dalam resolve data (misal GraphQL resolver). Dengan mengimplementasikan DataLoader, kita bisa memangkas ratusan atau bahkan ribuan query menjadi 2-3 query SQL saja!

Selalu review pola fetching data saat membangun aplikasi skalabel–pahami N+1 problem, dan gunakan pattern DataLoader setiap kali query per-relasi mulai terasa redundant.


Referensi


Selamat menulis kode yang lebih efisien dan scalable! 🚀

comments powered by Disqus