N+1 Query Problem: Kenapa Aplikasi Anda Lambat Tanpa Anda Sadari
N+1 query problem adalah penyebab performa paling umum yang sulit dideteksi. Satu query untuk list data, lalu N query untuk relasi setiap baris. Pelajari cara mendeteksi dan memperbaikinya di PostgreSQL.
Aplikasi Anda lambat. Query database saja ambil 50ms, tapi load satu halaman butuh 5 detik. Anda cek CPU server, normal. Cek memory, normal. Cek network, normal. Tapi halaman tetap lambat.
Masalahnya bukan di satu query. Masalahnya di jumlah query.
Ini disebut N+1 query problem. Satu query untuk ambil list data, lalu N query tambahan untuk ambil relasi setiap baris. Kalau list Anda punya 100 baris, ada 101 query yang ditembakkan ke database. Artikel ini bahas kenapa ini terjadi, cara mendeteksinya, dan cara memperbaikinya di PostgreSQL.
Apa Itu N+1 Query Problem
N+1 terjadi saat kode Anda mengambil list data dalam satu query, lalu melakukan query tambahan untuk setiap item. Pola ini sering muncul di ORM yang lazy-load relasi secara default.
Contoh klasik: ambil list 10 blog post, lalu untuk setiap post, ambil author-nya. Itu 1 + 10 = 11 query. Ambil 100 post? 101 query. Ambil 1000 post? 1001 query. Jumlah query tumbuh linear dengan jumlah data.
Coba lihat kodenya:
Kode di atas terlihat wajar. Bahkan cukup bersih. Tapi di balik layar, ada 101 round-trip ke database. Setiap round-trip punya network latency, query parsing, dan execution overhead. Untuk 100 baris mungkin masih toleransi. Untuk 10,000 baris, aplikasi Anda akan terasa seperti dial-up.
Kenapa N+1 Berbahaya
Masalahnya bukan cuma soal lambat. N+1 adalah silent killer. Aplikasi Anda jalan normal di development karena datanya sedikit. 10 post, 11 query, selesai dalam 20ms. Production beda cerita.
Tiga alasan kenapa N+1 berbahaya:
- Tumbuh linear dengan data. 10 baris = 11 query, 10,000 baris = 10,001 query. Waktu eksekusi naik konsisten.
- Susah terdeteksi di development. Data dev kecil, jadi semuanya cepat. Baru kelihatan saat traffic naik di production.
- Connection pool habis. Setiap query butuh connection. 101 query simultan bisa menguras pool, bikin query lain menunggu.
Saya pernah lihat kasus API yang response time-nya 3 detik. Setelah di-trace, penyebabnya adalah N+1 query di endpoint yang load 500 record. Diperbaiki jadi JOIN, response time turun ke 80ms. Tanpa mengubah hardware, tanpa menambah server. Cuma mengubah cara query ditulis.
Cara Memperbaiki N+1 di PostgreSQL
Prinsip utamanya sederhana: kurangi jumlah round-trip ke database. Kalau Anda butuh data dari dua tabel, ambil dalam satu query, bukan dua.
Solusi 1: Gunakan JOIN
Alih-alih query per post, JOIN tabel posts dan authors dalam satu query. PostgreSQL akan mengoptimalkan eksekusinya.
Ini versi yang diperbaiki:
Satu query, satu round-trip, hasil yang sama. PostgreSQL optimizer bisa pakai hash join atau merge join tergantung index dan ukuran data. Jauh lebih efisien daripada 101 query terpisah.
Solusi 2: Batch dengan IN Clause
Kalau Anda tidak bisa JOIN, misalnya karena data dari service berbeda, gunakan IN clause untuk batch query. Kumpulkan semua author_id, lalu ambil dalam satu query.
Total: 2 query untuk data berapa pun. 100 post, 1000 post, tetap 2 query. Inilah yang disebut eager loading. Ambil relasi sekaligus, bukan satu per satu.
Solusi 3: LATERAL JOIN untuk Kasus Kompleks
Kadang Anda butuh ambil N baris relasi per parent. Misalnya, 3 komentar terbaru per post. Ini biasanya jadi N+1 karena setiap post ditembakkan query komentar terpisah.
PostgreSQL punya LATERAL JOIN untuk kasus ini:
LATERAL JOIN memungkinkan subquery mengakses kolom dari tabel outer. PostgreSQL akan menjalankan subquery untuk setiap baris posts, tapi tetap dalam satu round-trip. Ini lebih efisien daripada N query terpisah.
Cara Mendeteksi N+1 di Aplikasi Anda
N+1 sering tidak terlihat karena aplikasi tetap berfungsi. Tapi ada tiga cara untuk mendeteksinya sebelum masuk production.
Pertama, nyalakan query logging di PostgreSQL. Lihat berapa banyak query yang ditembakkan untuk satu request.
Kedua, gunakan extension pg_stat_statements. Ini menampilkan jumlah kali setiap query dipanggil. Kalau satu query muncul 100 kali dalam satu request, itu tanda N+1.
Ketiga, tambahkan query counter di aplikasi. Banyak ORM punya opsi untuk log jumlah query per request. Di Django, setelah DEBUG = True, ada connection.queries. Di Go, Anda bisa bungkus database driver untuk counting.
N+1 di ORM: Sumber Masalah Terbesar
ORM adalah penyebab N+1 terbesar. Secara default, ORM lazy-load relasi. Artinya, relasi baru diambil saat diakses, bukan saat query utama dieksekusi. Ini efisien untuk data yang jarang diakses, tapi jadi bencana untuk list data.
Solusinya: gunakan eager loading. Hampir semua ORM mendukung ini.
Di GORM, Preload mengubah N+1 menjadi 2 query. Di Django ORM, ini disebut select_related (untuk foreign key) atau prefetch_related (untuk many-to-many). Di SQLAlchemy, ini disebut joinedload atau selectinload.
Poin penting: tahu ORM Anda. Baca dokumentasinya. Cari bagian tentang eager loading, N+1, atau query optimization. Ini investasi waktu 30 menit yang bisa menghemat jam-jam debugging performa.
Rangkuman
N+1 query problem adalah masalah performa paling umum yang gampang diperbaiki tapi susah dideteksi. Polanya selalu sama: satu query untuk list, lalu N query untuk relasi. Solusinya juga selalu sama: gabungkan menjadi satu atau dua query menggunakan JOIN, IN clause, atau eager loading.
Tiga hal yang perlu Anda lakukan hari ini:
- Nyalakan pg_stat_statements di PostgreSQL production Anda.
- Cek query yang punya calls tinggi per request. Itu kandidat N+1.
- Ubah ke JOIN atau eager loading. Test response time sebelum dan sesudah.
Aplikasi Anda mungkin jauh lebih lambat dari yang seharusnya. Dan kemungkinan besar, bukan karena hardware. Tapi karena cara Anda menulis query.
Referensi
1. PostgreSQL LATERAL JOIN Documentation
2. pg_stat_statements Extension
3. GORM Preload (Eager Loading)
