N+1 Query Itu Bukan Bug Kecil. Itu Cara Halus Bunuh Latency
N+1 query jarang kelihatan waktu data masih kecil. Tapi begitu traffic dan relasi data naik, latency pelan-pelan rusak tanpa ada error yang dramatis.
0xNN · · 9 min read
Ada bug yang berisik, ada bug yang diem-diem bikin sistem lo makin jelek tiap minggu. N+1 query masuk kategori kedua. Dia gak bikin app langsung crash. Dia bikin endpoint yang tadinya 80ms jadi 250ms, lalu 600ms, lalu 1,2 detik. Semua masih technically hidup. Tapi rasanya udah berat dan semua orang nyalahin hal lain.
Gue pertama kali sadar soal ini pas lihat endpoint daftar order yang keliatannya sederhana banget. Query utama ambil 50 order. Habis itu tiap order manggil relasi user, item, dan payment secara terpisah. Di local aman karena datanya sedikit. Di production, itu berubah jadi pesta query yang gak perlu.
---
N+1 itu apa
Polanya simpel:
• 1 query buat ambil daftar utama
• N query tambahan buat ambil detail tiap item
Misalnya:
const posts = await db.post.findMany()
for (const post of posts) {
post.author = await db.user.findUnique({ where: { id: post.authorId } })
}
Kalau posts ada 100, lo baru bikin 101 query. Kalau tiap query cepat, lo mungkin gak sadar. Tapi network round trip, parsing, lock, planner, dan load DB tetap nambah. Dan yang bikin jahat: masalah ini scale bareng data, bukan bareng niat baik lo.
---
Kenapa ORM sering jadi pintu masuk
ORM itu enak karena bikin query terasa seperti object access biasa. Masalahnya, kadang itu bikin programmer lupa bahwa di bawah layar ada database yang harus bolak-balik kerja.
Akses post.author terlihat natural. Tapi kalau implementasinya lazy load tanpa batching, lo sedang mengubah loop biasa jadi rentetan query. Di code review, ini gampang lolos karena source code-nya kelihatan bersih. Yang kotor justru jejak di database.
Makanya gue gak suka narasi "ORM jelek" atau "ORM bagus". Masalahnya bukan alatnya. Masalahnya adalah ketika abstraksi bikin lo lupa biaya I/O. ORM bisa aman kalau lo sadar kapan harus eager load, preload, atau join. ORM jadi mahal kalau lo memperlakukan relasi kayak properti gratis.
---
Gejala yang sering muncul
Biasanya tim baru sadar N+1 lewat gejala aneh:
• endpoint list lambat, detail page justru cepat
• CPU app aman, tapi database makin sibuk
• latency naik saat page size dinaikkan
• cache hit rate biasa aja, tapi query count per request absurd
Kalau lo punya APM atau query log, lihat metrik paling basic: berapa query per request. Banyak tim monitor response time tapi gak pernah monitor jumlah query. Padahal N+1 sering keliatan lebih cepat dari metrik itu.
---
Cara ngatasin yang realistis
1. Eager loading
Kalau lo tahu data relasi bakal dipakai, ambil dari awal.
const posts = await db.post.findMany({
include: { author: true }
})
2. Batching
Kalau relasinya kompleks, kumpulin semua authorId, lalu ambil sekaligus dengan IN (...). Banyak GraphQL stack mengandalkan DataLoader buat ini.
3. Join yang sadar ukuran data
Kadang 1 query join lebih masuk akal daripada banyak query kecil. Kadang juga join kebanyakan bikin row meledak. Jadi jangan dogmatis. Ukur.
4. Pagination yang waras
Page size 20 beda jauh sama 200 kalau di belakangnya ada N+1. Pagination gak menyelesaikan root cause, tapi membatasi damage.
5. Monitor query count
Ini underrated. Begitu ada guardrail "endpoint ini gak boleh > 10 query", review jadi lebih objektif.
---
Yang sering disalahpahami
"N+1 cuma masalah performa kecil" - salah. Di skala tertentu, ini berubah jadi biaya database, timeout, dan queue backlog.
"Kalau database kuat, gapapa" - itu cuma nunda masalah. Database yang kuat akan tetap dibebani kerja yang gak perlu.
"Join selalu solusi terbaik" - belum tentu. Join besar bisa duplikasi row dan makan memory. Tujuannya bukan memuja join, tapi mengurangi round trip bodoh.
"Caching pasti nutupin semua" - kadang iya, kadang justru bikin orang telat sadar. Cache miss pertama tetap kena dan query jeleknya tetap ada.
---
Referensi yang layak dibaca
• Prisma Docs - Query optimization
• PlanetScale - What is the N+1 query problem?
• Use The Index, Luke!
• PostgreSQL Documentation
---
N+1 itu menarik karena dia jarang datang sebagai drama. Dia datang sebagai rasa "kok makin lama makin berat ya". Dan jujur, bug kayak gini lebih berbahaya dari error yang langsung merah, karena tim bisa hidup lama dalam performa mediocre sambil merasa semuanya masih oke.
Sejak beberapa kali ketipu pola ini, gue jadi makin percaya satu hal: query count itu bagian dari design, bukan detail implementasi. Kalau lo gak sengaja bikin 101 query buat satu halaman, itu bukan cuma masalah database. Itu masalah cara berpikir.
---
*ditulis setelah bongkar endpoint list yang keliatannya rapi di code review tapi ternyata nyeret database kerja lembur buat hal yang sebenernya bisa diselesaikan jauh lebih hemat.*