Mengatasi N+1 Query Problem: Musuh Tersembunyi Performa Backend

Pernah bikin endpoint yang cuma nampilin 50 postingan beserta nama penulisnya, tapi response-nya lambat banget? Bisa jadi kalian kena N+1 Query. Yuk, kita bedah penyebabnya dan cara memberantasnya!

Mengatasi N+1 Query Problem: Musuh Tersembunyi Performa Backend

Pernah nggak kalian bikin endpoint API yang kelihatannya simpel banget—cuma nampilin daftar 50 postingan blog beserta nama penulisnya masing-masing—tapi pas dites, response-nya makan waktu sampai 2-3 detik?

Setelah dicek log database-nya, ternyata server database kalian dibombardir 51 query terpisah cuma buat ngelayanin satu request itu. Padahal datanya cuma segitu-gitu aja, lho.

Fenomena ini dikenal luas sebagai N+1 Query Problem. Ini salah satu performance anti-pattern paling klasik, dan sering banget menjebak developer backend yang pakai ORM.

Poin Penting

  • N+1 Query menjalankan 1 query awal buat ngambil daftar utama, lalu N query tambahan satu per satu di dalam perulangan.
  • Akar masalahnya adalah perilaku Lazy Loading yang jadi bawaan hampir semua ORM.
  • Solusinya Eager Loading alias ngambil relasi sekaligus, lewat joinedload atau selectinload di SQLAlchemy, serta select_related atau prefetch_related di Django.
  • Efeknya besar: puluhan round-trip ke database bisa menyusut jadi satu atau dua query saja.

Apa Sih N+1 Query Problem Itu?

Secara sederhana, N+1 Query terjadi ketika aplikasi kalian melakukan dua hal:

  1. 1 query awal buat ngambil daftar data utama sebanyak $N$ baris, misalnya SELECT * FROM posts LIMIT 50;.
  2. N query tambahan buat ngambil data relasinya satu per satu di dalam perulangan, misalnya SELECT * FROM authors WHERE id = 1;, lalu id = 2, dan seterusnya sampai 50 kali.

Total query yang dieksekusi ke database jadi 1 + N = 51 query. Bayangkan kalau ada 1.000 orang mengakses halaman itu bersamaan. Database kalian bakal kebanjiran puluhan ribu round-trip yang sebenernya bisa diselesaikan cuma dalam satu atau dua query.

Biar kebayang borosnya, coba pakai analogi belanja ini deh. Kalian disuruh beli 10 barang berbeda di minimarket.

  • Cara N+1: beli susu, pulang ke rumah. Balik ke minimarket, beli roti, pulang lagi. Ulangi 10 kali. Waktunya habis di jalan, bukan di belanjanya.
  • Cara efisien: bawa daftar belanjaan lengkap, ambil 10 barang sekaligus, bayar di kasir, terus pulang sekali jalan.

Nah, "jalan bolak-balik" itulah network latency. Semakin sering kalian bolak-balik ngeksekusi query kecil, semakin lambat response API kalian.

Kenapa ORM Sering Menjebak Kita?

Hampir semua ORM modern pakai mekanisme Lazy Loading secara bawaan.

Artinya, data relasi nggak bakal dimuat ke memori sampai atributnya diakses secara eksplisit di dalam kode. Sekilas ini praktis dan hemat memori, ya. Tapi begitu kalian nge-loop for ke objek relasinya, ORM bakal otomatis ngeksekusi query baru di setiap iterasi tanpa kalian sadari. Nggak ada error, nggak ada warning—cuma lambat. 😅

Cara Mengatasinya: Eager Loading vs Lazy Loading

Solusi utamanya adalah menerapkan Eager Loading, atau sering disebut juga pre-fetching.

Alih-alih nunggu relasinya diakses satu per satu, kita kasih tahu ORM sejak awal: "Tolong sekalian ambilkan data relasinya dalam satu query JOIN atau query IN (...) ya." Bedanya bisa drastis, lho—dari 51 query jadi 1 query.

Contoh Kode: Cara Memperbaiki di Python

1. Di SQLAlchemy (FastAPI / Flask)

PYTHON
# Masalah N+1 (Lazy Loading)
posts = db.query(Post).all()
for post in posts:
    print(post.author.name)  # Memicu query baru tiap iterasi!

# Solusi: Eager Loading dengan joinedload atau selectinload
from sqlalchemy.orm import joinedload

posts = db.query(Post).options(joinedload(Post.author)).all()
for post in posts:
    print(post.author.name)  # Data author sudah dimuat sekaligus via JOIN!

2. Di Django ORM

PYTHON
# Masalah N+1
posts = Post.objects.all()
for post in posts:
    print(post.author.name)  # Query berulang ke tabel Author

# Solusi untuk ForeignKey / OneToOne: select_related
posts = Post.objects.select_related('author').all()

# Solusi untuk ManyToMany / Reverse ForeignKey: prefetch_related
posts = Post.objects.prefetch_related('tags').all()

Kapan Lazy Loading Masih Wajar Dipakai?

Eager loading bukan berarti kalian harus ngehapus semua lazy loading, ya. Ada kalanya lazy loading justru lebih hemat, kok.

  • Kalau relasinya cuma diakses di sebagian kecil baris, misalnya cuma halaman detail satu postingan, lazy loading itu nggak masalah.
  • Kalau kalian nge-prefetch tabel relasi yang gede padahal cuma butuh beberapa kolomnya, memori aplikasi kalian justru bisa membengkak.
  • Yang paling penting, biasakan cek query log atau pakai profiling tool di fase development. Tools kayak Django Debug Toolbar atau opsi echo=True di SQLAlchemy bakal langsung nunjukin kalau query kalian tiba-tiba menumpuk.

N+1 Query itu jebakan klasik yang nggak kerasa di localhost dengan segelintir data dummy, tapi langsung bikin server timeout begitu datanya menyentuh angka ribuan. Jadi, biasakan inspeksi query kalian sejak awal ya, dan manfaatkan eager loading kayak joinedload di SQLAlchemy atau select_related di Django tiap kali berurusan dengan relasi data dalam jumlah banyak.

Selamat ber-database ria, dan semoga query kalian selalu cukup satu! 👋

Biar makin jago ngoding.

Yuk gabung bareng temen-temen developer lainnya buat dapet update teknologi, tips, dan tutorial santai tiap minggu.

Biar Nggak Kudet

Update teknologi, tips ngoding, dan insight santai langsung ke email kamu tiap minggu.

Santai, anti spam. Bisa berhenti langganan kapan aja.

Baca Selanjutnya

Lihat semua

© 2026 Inva.dev.