N+1 Query Problem di PHP dan MySQL: Deteksi dan Solusi
Aplikasi PHP lambat tapi query database normal? Mungkin N+1 query problem. Pelajari cara deteksi dan fix dengan PDO, JOIN, IN clause, dan Laravel eager loading.
Aplikasi PHP Anda lambat. Query database saja 50ms, tapi load satu halaman butuh 5 detik. Anda cek Apache, normal. Cek MySQL, normal. Tapi halaman tetap lambat.
Masalahnya bukan di satu query. Masalahnya di jumlah query. Ini disebut N+1 query problem, dan ini adalah penyebab performa paling umum yang sulit dideteksi.
Artikel ini bahas N+1 khusus untuk PHP dan MySQL: kenapa terjadi, cara deteksi, dan cara fix dengan PDO maupun Laravel Eloquent. Prinsipnya sama dengan bahasa lain, tapi solusinya spesifik untuk ekosistem PHP.
N+1 di PHP: Pola yang Sama, Bahasa Berbeda
Pola N+1 di PHP biasanya muncul saat Anda loop hasil query, lalu di dalam loop eksekusi query lain. Ini sangat umum, terutama di Laravel saat eager loading tidak dipakai.
Contoh kasus: ambil 100 blog post, lalu untuk setiap post ambil nama author. Kodenya:
101 query ke MySQL. Setiap query punya overhead: parsing, network round-trip, execution plan. Untuk 100 baris mungkin masih cepat. Untuk 10,000 baris, halaman Anda akan lambat.
Yang tricky, masalah ini tidak kelihatan di development. Data dev sedikit, semuanya cepat. Baru kelihatan saat traffic naik di production.
Solusi 1: JOIN di MySQL
Cara paling langsung: gabungkan posts dan authors dalam satu query. MySQL optimizer akan pakai index join atau hash join tergantung ukuran tabel.
Versi PHP-nya:
Satu query, selesai. Tidak ada loop, tidak ada query tambahan. Ini perbaikan paling sederhana dan paling efektif.
Solusi 2: Batch dengan IN Clause
Kalau Anda tidak bisa JOIN, misalnya data dari service berbeda atau query terlalu kompleks, gunakan IN clause. Kumpulkan semua author_id, ambil dalam satu query.
Total: 2 query untuk data berapa pun. 100 post, 1000 post, tetap 2 query. PDO::FETCH_KEY_PAIR langsung bikin array asosiatif id ke name, jadi lookup di step 4 jadi O(1).
Solusi 3: Window Function untuk Kasus Kompleks
Kadang Anda butuh ambil N baris relasi per parent. Misalnya, 3 komentar terbaru per post. Di MySQL 8+, Anda bisa pakai window function.
ROW_NUMBER() OVER (PARTITION BY ...) membagi komentar per post, lalu urutkan berdasarkan created_at. Ambil 3 teratas per grup. Semua dalam satu query, satu round-trip.
N+1 di Laravel: Eloquent Eager Loading
Laravel Eloquent adalah sumber N+1 terbesar di ekosistem PHP. Secara default, Eloquent lazy-load relasi. Saat Anda akses property relasi, Eloquent ditembakkan query baru.
Satu kata: with(). Itu saja yang membedakan 101 query dari 2 query. Laravel juga punya withCount() untuk ambil jumlah relasi tanpa query tambahan.
Untuk relasi bertingkat, pakai dot notation:
Cara Deteksi N+1 di PHP
Laravel punya tool bawaan: Telescope dan Debugbar. Aktifkan di development, lihat jumlah query per request. Kalau satu request mengeksekusi 50+ query, itu tanda N+1.
Untuk MySQL, nyalakan slow query log:
Atau pakai MySQL performance_schema untuk lihat query dengan calls tinggi. Kalau satu query muncul 100 kali dalam satu request, itu N+1.
Rangkuman
N+1 query problem tidak peduli bahasa. PHP, Go, Python, semua bisa kena. Solusinya juga tidak peduli bahasa: gabungkan query, jangan ditembakkan satu per satu.
Tiga hal untuk PHP + MySQL:
- Pakai JOIN, bukan query di dalam loop.
- Kalau tidak bisa JOIN, batch pakai IN clause.
- Di Laravel, selalu pakai with() untuk eager loading.
Satu kata kunci di Laravel: with(). Satu JOIN di raw SQL. Itu yang membedakan aplikasi lambat dari aplikasi cepat.
