N+1 Query: Cara Menemukan dan Membereskannya

N+1 query adalah salah satu penyebab paling umum aplikasi Laravel terasa lambat, dan ironisnya sering tidak terlihat sampai data produksi membesar. Waktu development, tabel masih berisi belasan baris, jadi query berulang-ulang tetap terasa instan. Begitu masuk produksi dengan ribuan baris, satu halaman yang tadinya kirim 3 query bisa diam-diam mengirim ratusan query ke database. Bagi saya yang sehari-hari kerja di sistem klinik/rumah sakit dengan volume data pasien dan transaksi yang terus tumbuh, isu ini bukan sekadar teori — ini yang paling sering bikin halaman listing tiba-tiba terasa berat.

Apa itu N+1 Query

Pola ini muncul ketika kita mengambil satu koleksi data (1 query), lalu di dalam loop mengakses relasinya satu per satu, sehingga Laravel diam-diam menembakkan query tambahan untuk tiap item (N query). Contoh klasik:

$appointments = Appointment::all();

foreach ($appointments as $appointment) {
    echo $appointment->patient->name;
}

Kode ini terlihat sederhana dan tidak salah secara logika, tapi sebagai ilustrasi: kalau $appointments berisi 500 baris, Eloquent akan menjalankan 1 query untuk mengambil semua appointment, lalu 500 query tambahan untuk mengambil relasi patient satu per satu — total 501 query untuk satu halaman.

Cara Mendeteksinya

Masalah N+1 jarang terasa saat menulis kode, jadi kita perlu alat bantu untuk melihatnya. Dua cara yang paling saya andalkan:

  • Laravel Telescope — cukup pasang di environment development/staging, buka tab Queries, lalu perhatikan request yang menjalankan puluhan atau ratusan query serupa dengan pola WHERE id = ? berulang. Itu ciri khas N+1.
  • Query log manual — kalau tidak pakai Telescope, bisa dengarkan event query langsung, misalnya di AppServiceProvider:
DB::listen(function ($query) {
    Log::info($query->sql, $query->bindings);
});

Setelah itu, buka satu halaman dan hitung berapa baris log yang muncul. Sebagai ilustrasi, kalau jumlahnya sebanding dengan jumlah baris data yang ditampilkan (misalnya 50 item menghasilkan sekitar 50-100 query), itu tanda kuat ada N+1.

Cara lain yang lebih cepat untuk audit rutin adalah mengaktifkan Model::preventLazyLoading() di environment lokal — Laravel akan melempar exception setiap kali ada relasi diakses tanpa eager loading, sehingga N+1 ketahuan sejak development, bukan setelah production lambat.

Membereskan dengan Eager Loading yang Tepat

Solusi standarnya adalah with(), yang memberitahu Eloquent untuk mengambil relasi dalam query terpisah tapi jumlahnya tetap konstan, bukan per baris:

$appointments = Appointment::with('patient')->get();

Sekarang hanya ada 2 query total, berapa pun jumlah appointment-nya. Tapi eager loading juga bisa berlebihan kalau tidak hati-hati. Beberapa pola yang saya pakai supaya tidak over-fetching:

  • Pilih kolom yang perlu saja, jangan tarik seluruh relasi kalau hanya butuh satu-dua field: with('patient:id,name').
  • Nested relation ditulis eksplisit, misalnya with('patient.address'), daripada memuat semua relasi turunan lewat load() di banyak tempat berbeda.
  • Eager load kondisional pakai with(['visits' => fn ($q) => $q->latest()->limit(5)]) kalau hanya butuh sebagian data relasi, bukan seluruhnya.
  • Gunakan loadCount() ketimbang eager load seluruh koleksi hanya untuk menghitung jumlah baris relasi, misalnya jumlah kunjungan per pasien.

Prinsip umumnya: eager loading itu trade-off antara jumlah query dan ukuran payload. Terlalu sedikit relasi di-load, kita kena N+1; terlalu banyak relasi (atau kolom) ditarik sekaligus, kita justru menghabiskan memori dan bandwidth untuk data yang tidak dipakai di halaman itu.

Ringkasnya

N+1 query paling efektif ditangkap lewat kebiasaan, bukan sekali audit: aktifkan query log atau Telescope di staging, nyalakan preventLazyLoading() di lokal, dan biasakan menulis with() sejak awal setiap kali menampilkan koleksi beserta relasinya. Sedikit disiplin di titik ini biasanya menyelamatkan banyak waktu debugging performa di kemudian hari.