Laravel N+1 Query: Penyebab, Cara Mendeteksi, dan Solusinya

Laravel N+1 Query: Apa Itu dan Bagaimana Cara Mengatasinya?

Laravel N+1 Query terjadi ketika aplikasi menjalankan 1 query untuk mengambil data utama, kemudian N query tambahan untuk mengambil relasi dari setiap data tersebut.

Contoh sederhananya:

$posts = Post::all();

foreach ($posts as $post) {
    echo $post->user->name;
}

Kode tersebut dapat menghasilkan:

1 query  → mengambil semua posts
N query  → mengambil user untuk setiap post

Jika terdapat 100 post, aplikasi berpotensi menjalankan 101 query.

Solusi paling umum adalah menggunakan eager loading:

$posts = Post::with('user')->get();

Dengan begitu, Laravel dapat mengambil data posts dan relasi users secara lebih efisien.

Apa Itu N+1 Query?

Istilah N+1 berasal dari pola jumlah query yang terjadi:

1 + N

Di mana:

  • 1 adalah query untuk mengambil data utama.
  • N adalah query tambahan untuk mengambil relasi masing-masing record.

Misalnya aplikasi memiliki 50 artikel dan setiap artikel memiliki seorang author.

Kode:

$posts = Post::all();

foreach ($posts as $post) {
    echo $post->user->name;
}

Secara konseptual dapat menghasilkan:

SELECT * FROM posts;

SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE id = 2;
SELECT * FROM users WHERE id = 3;
...

Semakin banyak data, semakin banyak query yang harus diproses.

Inilah yang membuat N+1 Query dapat menyebabkan aplikasi Laravel menjadi lambat, terutama ketika data sudah besar.

Contoh Laravel N+1 Query

Misalnya terdapat model Post:

class Post extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Kemudian controller mengambil data:

public function index()
{
    $posts = Post::all();

    return view('posts.index', compact('posts'));
}

Di Blade:

@foreach ($posts as $post)
    <h2>{{ $post->title }}</h2>
    <p>Author: {{ $post->user->name }}</p>
@endforeach

Sekilas kode tersebut terlihat normal.

Masalahnya adalah $post->user merupakan relationship yang dapat memicu query ketika data relasi belum dimuat.

Jika terdapat 100 post, pola query dapat menjadi:

1 query posts
+
100 query users
=
101 query

Itulah N+1 Query.

Mengapa N+1 Query Berbahaya?

N+1 Query tidak selalu langsung terlihat sebagai error.

Aplikasi mungkin tetap menghasilkan response yang benar, tetapi performanya menurun.

Dampaknya antara lain:

  • response halaman menjadi lebih lambat,
  • database menerima terlalu banyak query,
  • penggunaan CPU database meningkat,
  • penggunaan koneksi database meningkat,
  • waktu response API bertambah,
  • performa memburuk ketika jumlah data bertambah.

Masalah ini sering baru terasa ketika aplikasi sudah memiliki banyak data.

Contohnya, query yang masih terasa cepat dengan 10 record dapat menjadi sangat lambat ketika endpoint mengembalikan 1.000 record.

Karena itu, N+1 Query lebih tepat dipandang sebagai masalah efisiensi akses database, bukan sekadar masalah jumlah query.

Cara Mendeteksi Laravel N+1 Query

Ada beberapa cara untuk menemukan N+1 Query di aplikasi Laravel.

1. Periksa Query yang Dijalankan

Laravel menyediakan DB::listen() yang dapat digunakan untuk melihat query yang dieksekusi.

Contoh sederhana:

use Illuminate\Support\Facades\DB;

DB::listen(function ($query) {
    logger($query->sql);
});

Kemudian jalankan endpoint yang dicurigai.

Jika log menunjukkan query relationship yang sama dieksekusi berkali-kali, kemungkinan terdapat N+1 Query.

Contohnya:

select * from users where id = ?
select * from users where id = ?
select * from users where id = ?
select * from users where id = ?

Jika query tersebut muncul berulang untuk setiap record, periksa apakah relationship belum menggunakan eager loading.

2. Gunakan Laravel Debugging Tool

Untuk development, developer dapat menggunakan tool seperti Laravel Debugbar untuk melihat:

  • jumlah query,
  • query SQL,
  • waktu eksekusi,
  • duplicate query,
  • timeline request.

Jika sebuah halaman hanya membutuhkan beberapa data tetapi menjalankan puluhan atau ratusan query, itu menjadi sinyal kuat untuk melakukan pemeriksaan lebih lanjut.

Catatan: tool debugging sebaiknya digunakan pada environment development, bukan dibuka kepada pengguna di production.

3. Perhatikan Query Berulang

Salah satu indikator paling mudah adalah menemukan query yang sama berulang kali.

Misalnya:

select * from users where users.id = 5
select * from users where users.id = 5
select * from users where users.id = 5
select * from users where users.id = 5

Jika query tersebut muncul akibat:

$post->user

di dalam loop, kemungkinan besar relationship perlu di-eager-load.

4. Gunakan Query Count Saat Testing

N+1 Query juga dapat dicegah dengan menguji jumlah query pada test.

Contohnya:

DB::enableQueryLog();

$posts = Post::with('user')->get();

$queries = DB::getQueryLog();

$this->assertLessThan(5, count($queries));

Pendekatan ini dapat membantu memastikan perubahan kode tidak menyebabkan jumlah query meningkat secara tidak sengaja.

Untuk aplikasi besar, pengujian performa seperti ini dapat menjadi bagian dari quality control.

Solusi Laravel N+1 Query: Gunakan Eager Loading

Solusi paling umum untuk N+1 Query adalah eager loading.

Daripada:

$posts = Post::all();

gunakan:

$posts = Post::with('user')->get();

Kemudian:

@foreach ($posts as $post)
    <h2>{{ $post->title }}</h2>
    <p>{{ $post->user->name }}</p>
@endforeach

Laravel akan memuat relationship user sejak awal sehingga aplikasi tidak perlu melakukan query terpisah untuk setiap post.

Secara sederhana:

Tanpa eager loading:

posts → user → query
      → user → query
      → user → query
      → user → query


Dengan eager loading:

posts ─┐
       ├── users
       └── ...

Eager Loading dengan with()

Untuk satu relationship:

$posts = Post::with('user')->get();

Untuk beberapa relationship:

$posts = Post::with(['user', 'category'])->get();

Untuk nested relationship:

$posts = Post::with('comments.user')->get();

Contohnya:

@foreach ($posts as $post)
    <h2>{{ $post->title }}</h2>

    @foreach ($post->comments as $comment)
        <p>{{ $comment->user->name }}</p>
    @endforeach
@endforeach

Jika comments.user tidak di-eager-load, loop bertingkat seperti ini juga dapat menghasilkan N+1 Query.

h2>Eager Loading Setelah Query dengan load()

Terkadang relationship baru diketahui setelah model berhasil diambil.

Dalam kondisi tersebut, Anda dapat menggunakan load():

$posts = Post::latest()->get();

$posts->load('user');

Untuk beberapa relationship:

$posts->load([
    'user',
    'category',
]);

Perbedaan sederhananya:

Post::with('user')->get();

Relationship dimuat sebagai bagian dari query retrieval.

Sedangkan:

$posts = Post::get();
$posts->load('user');

Relationship dimuat setelah collection berhasil diperoleh.

Eager Loading dengan Kondisi

Anda tidak selalu perlu mengambil seluruh data relationship.

Contohnya:

$posts = Post::with([
    'comments' => function ($query) {
        $query->where('approved', true);
    }
])->get();

Dengan cara ini, hanya komentar yang memenuhi kondisi yang dimuat.

Pada Laravel versi modern, Anda juga dapat menggunakan gaya query yang lebih ringkas:

$posts = Post::with([
    'comments' => fn ($query) => $query->where('approved', true),
])->get();

Gunakan withCount() untuk Menghindari Query Tidak Perlu

Misalnya Anda hanya membutuhkan jumlah komentar.

Cara yang kurang efisien adalah mengambil seluruh comments:

$posts = Post::with('comments')->get();

kemudian menghitungnya:

{{ $post->comments->count() }}

Jika Anda hanya membutuhkan jumlahnya, gunakan:

$posts = Post::withCount('comments')->get();

Kemudian:

{{ $post->comments_count }}

Dengan pendekatan tersebut, database dapat menghitung jumlah relationship tanpa harus mengambil seluruh data komentar ke memory aplikasi.

Untuk beberapa relationship:

$posts = Post::withCount([
    'comments',
    'likes',
]);

Jangan Hanya Memperbaiki N+1 di Controller

N+1 Query dapat muncul di berbagai tempat.

Contohnya di Blade:

@foreach ($posts as $post)
    {{ $post->user->name }}
@endforeach

Di API Resource:

return [
    'title' => $this->title,
    'author' => $this->user->name,
];

Di accessor:

public function getAuthorNameAttribute()
{
    return $this->user->name;
}

Atau bahkan di service dan job.

Karena itu, ketika menemukan N+1, jangan hanya mencari loop di controller. Periksa seluruh jalur data dari query sampai response.

N+1 Query pada API Resource

Masalah N+1 sering muncul ketika membuat REST API.

Contoh:

public function toArray($request)
{
    return [
        'id' => $this->id,
        'title' => $this->title,
        'author' => $this->user->name,
    ];
}

Kemudian controller:

$posts = Post::paginate(20);

return PostResource::collection($posts);

Jika user belum di-load, resource dapat memicu query tambahan ketika setiap item diproses.

Solusinya:

$posts = Post::with('user')->paginate(20);

return PostResource::collection($posts);

Dengan demikian relationship sudah tersedia ketika resource diproses.

N+1 Query pada Relationship Bertingkat

Masalah dapat menjadi lebih kompleks jika terdapat beberapa relationship.

Misalnya:

Post
 └── User
      └── Company

Kode:

@foreach ($posts as $post)
    {{ $post->user->company->name }}
@endforeach

Jika hanya melakukan:

Post::with('user')->get();

relationship company belum tentu ikut dimuat.

Gunakan nested eager loading:

Post::with('user.company')->get();

Atau:

Post::with([
    'user',
    'user.company',
])->get();

Pilih bentuk yang paling mudah dipahami sesuai struktur aplikasi.

Lazy Loading dan Eager Loading: Apa Bedanya?

Perbedaan paling sederhana:

Lazy Loading

Relationship baru diambil ketika relationship tersebut diakses.

$posts = Post::all();

foreach ($posts as $post) {
    $post->user;
}

Jika dilakukan di dalam loop, ini dapat menghasilkan N+1 Query.

Eager Loading

Relationship diminta sejak awal:

$posts = Post::with('user')->get();

Dengan demikian Laravel dapat memuat relationship secara lebih efisien.

Bukan berarti lazy loading selalu buruk. Lazy loading dapat berguna ketika relationship memang tidak selalu diperlukan. Masalahnya muncul ketika lazy loading dilakukan berulang dalam loop atau pola akses yang menghasilkan banyak query.

Mencegah N+1 Secara Proaktif

Selain memperbaiki N+1 setelah terjadi, Laravel juga dapat dikonfigurasi agar developer mengetahui ketika lazy loading dilakukan secara tidak sengaja.

Pada environment development, Anda dapat menggunakan:

use Illuminate\Database\Eloquent\Model;

Model::preventLazyLoading(! app()->isProduction());

Dengan konfigurasi tersebut, aplikasi dapat membantu mendeteksi akses lazy loading yang tidak diinginkan ketika development.

Pendekatan ini sangat berguna untuk project yang dikerjakan oleh banyak developer karena masalah N+1 dapat ditemukan lebih awal.

Jangan Berlebihan Menggunakan with()

Meskipun eager loading merupakan solusi N+1, bukan berarti semua relationship harus selalu dimuat.

Contoh yang kurang ideal:

Post::with([
    'user',
    'comments',
    'comments.user',
    'category',
    'tags',
    'likes',
])->get();

Jika halaman hanya membutuhkan:

  • title,
  • author,
  • category,

maka memuat semua relationship tersebut justru dapat mengambil terlalu banyak data.

Lebih baik gunakan eager loading berdasarkan kebutuhan:

Post::with([
    'user',
    'category',
])->get();

Prinsipnya:

Load relationship yang memang dibutuhkan, bukan semua relationship yang tersedia.

Gunakan Pagination untuk Data yang Banyak

Eager loading dapat mengurangi jumlah query, tetapi tidak menyelesaikan masalah jika aplikasi mengambil terlalu banyak record sekaligus.

Hindari:

$posts = Post::with('user')->get();

jika tabel memiliki ratusan ribu record dan halaman hanya membutuhkan 20 data.

Gunakan pagination:

$posts = Post::with('user')
    ->latest()
    ->paginate(20);

Dengan demikian aplikasi hanya mengambil data yang diperlukan untuk halaman tersebut.

Optimasi Kolom yang Diambil

Anda juga dapat membatasi kolom yang diambil dari relationship.

Misalnya:

$posts = Post::with('user:id,name')->get();

Pastikan foreign key yang dibutuhkan relationship tetap tersedia.

Untuk relationship belongsTo, contoh tersebut meminta Laravel mengambil id dan name dari tabel users.

Pendekatan ini berguna ketika relationship memiliki banyak kolom tetapi halaman hanya membutuhkan beberapa field.

Contoh Sebelum dan Sesudah Memperbaiki N+1

Sebelum

public function index()
{
    $posts = Post::latest()->get();

    return view('posts.index', compact('posts'));
}

Blade:

@foreach ($posts as $post)
    <h2>{{ $post->title }}</h2>
    <span>{{ $post->user->name }}</span>
@endforeach

Potensi query:

1 query posts
+
N query users

Sesudah

Controller:

public function index()
{
    $posts = Post::with('user')
        ->latest()
        ->paginate(20);

    return view('posts.index', compact('posts'));
}

Blade tetap:

@foreach ($posts as $post)
    <h2>{{ $post->title }}</h2>
    <span>{{ $post->user->name }}</span>
@endforeach

Perbedaannya adalah relationship user sudah dimuat sebelum Blade melakukan iterasi.

Checklist Mendeteksi Laravel N+1 Query

Jika aplikasi Laravel terasa lambat ketika menampilkan banyak data, gunakan checklist berikut:

  • Periksa jumlah query pada endpoint atau halaman.
  • Cari query relationship yang berulang.
  • Periksa relationship yang dipanggil di dalam foreach.
  • Periksa Blade template.
  • Periksa API Resource.
  • Periksa accessor dan service.
  • Gunakan with() untuk eager loading.
  • Gunakan load() jika relationship dimuat setelah query utama.
  • Gunakan withCount() jika hanya membutuhkan jumlah data.
  • Gunakan pagination untuk collection besar.
  • Batasi kolom yang diambil jika diperlukan.
  • Aktifkan deteksi lazy loading pada development.
  • Ukur ulang jumlah query setelah optimasi.

FAQ Laravel N+1 Query

Apa itu N+1 Query di Laravel?

N+1 Query adalah kondisi ketika Laravel menjalankan satu query untuk mengambil data utama dan N query tambahan untuk mengambil relationship setiap record. Contohnya mengambil 100 post lalu menjalankan query user secara terpisah untuk setiap post.

Bagaimana cara memperbaiki N+1 Query di Laravel?

Solusi paling umum adalah eager loading menggunakan with():

$posts = Post::with('user')->get();

Jika relationship baru perlu dimuat setelah query dijalankan, gunakan load().

Apakah with() selalu menghilangkan N+1 Query?

Tidak selalu. with() hanya eager-load relationship yang Anda tentukan. Jika aplikasi kemudian mengakses relationship lain yang belum dimuat, N+1 masih dapat terjadi.

Contohnya:

Post::with('user')->get();

tetapi kode kemudian mengakses:

$post->user->company

Dalam kondisi tersebut, company juga mungkin perlu di-eager-load:

Post::with('user.company')->get();

Apakah N+1 Query selalu menyebabkan aplikasi lambat?

Tidak selalu terasa pada dataset kecil. Namun, jumlah query yang meningkat seiring jumlah record membuat masalah ini semakin signifikan ketika volume data bertambah.

Bagaimana cara mengetahui Laravel mengalami N+1 Query?

Periksa jumlah dan pola query menggunakan Laravel Debugbar, query logging, atau DB::listen(). Jika query relationship yang sama muncul berulang kali untuk setiap record, periksa kemungkinan N+1.

Apakah lazy loading harus selalu dihindari?

Tidak. Lazy loading bukan sesuatu yang otomatis salah. Masalahnya adalah ketika relationship diakses berulang, terutama di dalam loop, sehingga menghasilkan banyak query. Gunakan eager loading ketika Anda sudah mengetahui relationship tersebut memang dibutuhkan.

Kesimpulan

Laravel N+1 Query terjadi ketika aplikasi menjalankan query relationship berulang kali untuk setiap record yang diambil. Masalah ini sering muncul ketika relationship Laravel diakses di dalam loop tanpa eager loading.

Pola yang perlu diwaspadai:

$posts = Post::all();

foreach ($posts as $post) {
    $post->user->name;
}

Solusi umumnya:

$posts = Post::with('user')->get();

Untuk kebutuhan lain:

// Nested relationship
Post::with('user.company')->get();

// Load setelah query
$posts->load('user');

// Hanya membutuhkan jumlah relationship
Post::withCount('comments')->get();

// Dataset besar
Post::with('user')->paginate(20);

Intinya, jangan hanya mengejar jumlah query sekecil mungkin. Fokus pada query yang sesuai kebutuhan, data yang benar-benar digunakan, dan cara mengambil data yang tetap efisien ketika jumlah record bertambah.

Dengan mendeteksi N+1 sejak development dan menggunakan eager loading secara tepat, aplikasi Laravel dapat menghindari query database yang tidak perlu dan tetap responsif ketika skala data semakin besar.