Chapter Query Optimization
menutup diskusi Eloquent ORM dengan sekilas masalah N+1: satu query untuk koleksi utama, lalu N query tambahan karena relasi dimuat terpisah di dalam loop. Section ini adalah tempat membahas obatnya secara utuh. Method with() pada Eloquent
memuat relasi di muka — bersamaan dengan query utama — sehingga satu halaman yang tadinya menjalankan puluhan query bisa turun menjadi dua-tiga query saja. Kita akan memakai data yang sama seperti section sebelumnya: Budi dan Siti menulis enam post secara bergantian, diselingi sembilan komentar dari tiga user, dan setiap bukti query kita ukur lewat query log.
Satu Relasi: with('user') #
Bentuk paling dasar eager loading adalah menambahkan with() pada query sebelum get():
use App\Models\Post;
use Illuminate\Support\Facades\DB;
DB::enableQueryLog();
foreach (Post::with('user')->orderBy('id')->get() as $post) {
echo $post->judul, ' — ', $post->user->name, "\n";
}
$log = DB::getQueryLog();
echo 'total query: ', count($log), "\n";
foreach ($log as $q) {
echo '[', $q['query'], "]\n";
}Output yang Dihasilkan:
Belajar Relasi 01 — Budi
Belajar Relasi 02 — Siti
Belajar Relasi 03 — Budi
Belajar Relasi 04 — Siti
Belajar Relasi 05 — Budi
Belajar Relasi 06 — Siti
total query: 2
[select * from "posts" order by "id" asc]
[select * from "users" where "users"."id" in (1, 2)]Perhatikan query keduanya: Eloquent mengumpulkan seluruh user_id dari hasil query pertama, lalu mengambil semua user sekaligus dengan satu klausa where ... in (1, 2) dan mencocokkan setiap post dengan user-nya di memori. Tanpa with(), loop yang sama menjalankan tujuh query; dengan with(), hanya dua — dan jumlahnya tetap dua berapa pun banyak post yang kita tampilkan.
Beberapa Relasi Sekaligus #
Argumen with() menerima array bila kita butuh lebih dari satu relasi:
DB::flushQueryLog();
$posts = Post::with(['user', 'comments'])->orderBy('id')->get();
foreach ($posts as $post) {
echo $post->judul, ' oleh ', $post->user->name,
' punya ', $post->comments->count(), " komentar\n";
}
echo 'total query: ', count(DB::getQueryLog()), "\n";Output yang Dihasilkan:
Belajar Relasi 01 oleh Budi punya 3 komentar
Belajar Relasi 02 oleh Siti punya 2 komentar
Belajar Relasi 03 oleh Budi punya 0 komentar
Belajar Relasi 04 oleh Siti punya 3 komentar
Belajar Relasi 05 oleh Budi punya 1 komentar
Belajar Relasi 06 oleh Siti punya 0 komentar
total query: 3Tiga query: satu untuk posts, satu untuk users, satu untuk comments. Relasi yang tidak kita sebut tidak ikut dimuat — memanggil $post->user->email di sini bebas query, tetapi menyentuh relasi lain yang belum dimuat tetap memicu query baru.
Eager Loading Bersarang #
Notasi titik memuat relasi dari relasi: comments.user berarti “setiap komentar beserta penulisnya”. Pola ini penting karena N+1 bisa bersarang — post memuat komentar, komentar memuat user:
DB::flushQueryLog();
$posts = Post::with('comments.user')->where('id', '<=', 2)->orderBy('id')->get();
foreach ($posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->user->name, ' berkomentar di ', $post->judul, ': ',
$comment->isi, "\n";
}
}
$log = DB::getQueryLog();
echo 'total query: ', count($log), "\n";
foreach ($log as $q) {
echo '[', $q['query'], "]\n";
}Output yang Dihasilkan:
Siti berkomentar di Belajar Relasi 01: Penjelasannya jelas, terima kasih!
Anton berkomentar di Belajar Relasi 01: Baru mulai belajar, sangat membantu.
Siti berkomentar di Belajar Relasi 01: Promosi produk murah, cek bio saya.
Budi berkomentar di Belajar Relasi 02: Contoh kodenya bisa diunduh?
Budi berkomentar di Belajar Relasi 02: Jangan lupa bahas eager loading.
total query: 3
[select * from "posts" where "id" <= ? order by "id" asc]
[select * from "comments" where "comments"."post_id" in (1, 2)]
[select * from "users" where "users"."id" in (1, 2, 3)]Lima komentar dari tiga penulis hanya membutuhkan satu query users — tiga level relasi (post → comment → user) selesai dalam tiga query.
Eager Loading dengan Kendala #
Terkadang kita hanya ingin memuat sebagian relasi — misalnya komentar yang sudah di-approve saja. Nilai array diganti menjadi pasangan relasi => closure:
$posts = Post::with(['comments' => fn ($query) => $query->where('approved', true)])
->orderBy('id')->get();
foreach ($posts as $post) {
echo $post->judul, ': ', $post->comments->count(), " komentar approved\n";
}Output yang Dihasilkan:
Belajar Relasi 01: 2 komentar approved
Belajar Relasi 02: 1 komentar approved
Belajar Relasi 03: 0 komentar approved
Belajar Relasi 04: 2 komentar approved
Belajar Relasi 05: 1 komentar approved
Belajar Relasi 06: 0 komentar approvedClosure menerima query builder relasi, sehingga where, orderBy, sampai limit bisa kita rantai di situ. Perlu dicatat batas pentingnya: $post->comments setelah ini hanya berisi komentar approved — kode lain yang mengharapkan koleksi lengkap akan melihat data yang sudah terfilter.
Menghitung Tanpa Memuat: withCount() #
Bila yang dibutuhkan hanya jumlah, memuat seluruh baris komentar sayang. withCount() menambahkan subquery count(*) sebagai kolom virtual:
$posts = Post::withCount([
'comments',
'comments as approved_comments_count' => fn ($q) => $q->where('approved', true),
])->orderBy('id')->get();
foreach ($posts as $post) {
echo $post->judul, ': ', $post->comments_count, ' total / ',
$post->approved_comments_count, " approved\n";
}Output yang Dihasilkan:
Belajar Relasi 01: 3 total / 2 approved
Belajar Relasi 02: 2 total / 1 approved
Belajar Relasi 03: 0 total / 0 approved
Belajar Relasi 04: 3 total / 2 approved
Belajar Relasi 05: 1 total / 1 approved
Belajar Relasi 06: 0 total / 0 approvedAlias as approved_comments_count membuat hitungan terfilter menjadi atribut tersendiri. SQL yang dihasilkan berbentuk select "posts".*, (select count(*) from "comments" where "posts"."id" = "comments"."post_id") as "comments_count" from "posts" — tanpa memuat satu pun model Comment ke memori.
load() Setelah Model Diambil #
Eager loading tidak harus diputuskan sebelum query. Method load() pada model yang sudah ada memuat relasi setelahnya:
$post = Post::first();
echo 'sudah termuat? ', var_export($post->relationLoaded('user'), true), "\n";
$post->load('user');
echo 'sudah termuat? ', var_export($post->relationLoaded('user'), true), "\n";
echo $post->user->name, "\n";Output yang Dihasilkan:
sudah termuat? false
sudah termuat? true
BudiPola ini berguna saat relasi baru diketahui dibutuhkan di tengah kode — misalnya di dalam method controller setelah beberapa percabangan. Versi loadMissing() bersifat lebih sopan: relasi yang sudah termuat tidak dimuat ulang.
Setelah load() atau with(), relasi menetap di property relations model. Akses $post->user->name tiga kali pun tidak menambah query — Eloquent memeriksa relationLoaded() lebih dulu dan hanya menjalankan query bila relasi belum ada di memori. Mekanisme cache inilah yang akan kita bedah dari sisi sebaliknya di chapter berikutnya: lazy loading, perilaku bawaan saat kita lupa memuat relasi sejak awal.