KelasIT.com
Database

Tips Optimasi Query Laravel

Mengatasi query lambat di Laravel: memahami masalah N+1 dan eager loading, memilih kolom, chunk dan cursor untuk data besar, withCount, index, caching, serta alat debugging seperti Debugbar dan Telescope.

Tips Optimasi Query Laravel

Aplikasi Laravel yang terasa cepat saat pengembangan bisa melambat drastis di produksi. Penyebabnya hampir selalu sama: terlalu banyak query atau query yang berat. Artikel ini fokus pada teknik di sisi Laravel/Eloquent. Untuk sisi database (index, EXPLAIN), baca juga Tips Optimasi Query Database MySQL.

Langkah pertama: lihat query yang berjalan

Kamu tidak bisa memperbaiki apa yang tidak terlihat. Alat bantu:

  • Laravel Debugbar (barryvdh/laravel-debugbar): menampilkan jumlah dan waktu query pada setiap halaman.
  • Laravel Telescope: memantau query, request, job, dan exception.
  • DB::listen untuk mencatat query sendiri:
DB::listen(function ($query) {
    if ($query->time > 100) {     // lebih dari 100 ms
        Log::warning('Query lambat', ['sql' => $query->sql, 'ms' => $query->time]);
    }
});
  • ->toSql() dan ->explain() pada query builder untuk melihat SQL dan rencana eksekusinya.

Masalah #1: N+1 query

Ini penyebab kelambatan paling umum di Laravel. Contoh:

$posts = Post::all();                  // 1 query

foreach ($posts as $post) {
    echo $post->author->name;          // +1 query PER post
}

Untuk 100 artikel, terjadi 101 query. Penyebabnya: relasi author di-lazy load satu per satu saat diakses.

Solusi: eager loading

$posts = Post::with('author')->get();  // hanya 2 query

Laravel mengambil semua penulis dengan satu query WHERE id IN (...) lalu mencocokkannya di memori. Beberapa variasi:

Post::with(['author', 'categories'])->get();          // beberapa relasi
Post::with('comments.user')->get();                    // relasi bersarang
Post::with(['author:id,name'])->get();                 // hanya kolom tertentu
Post::with(['comments' => fn ($q) => $q->latest()->limit(5)])->get();
$posts->load('author');                                // setelah data terlanjur diambil
Catatan: kolom kunci (id, foreign key) harus ikut dipilih saat memakai author:id,name.

Mencegah N+1 secara otomatis

Di AppServiceProvider::boot():

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

Di lingkungan lokal, Laravel akan melempar exception setiap kali terjadi lazy loading yang tidak disengaja, sehingga masalah ketahuan sebelum rilis. (Pada produksi, sebaiknya hanya dicatat ke log.)

Relasi dijelaskan lebih lanjut di Mengenal Eloquent Relationship.

Masalah #2: Mengambil data yang tidak dibutuhkan

// Boros: mengambil semua kolom termasuk teks panjang
$users = User::all();

// Hemat
$users = User::select('id', 'name', 'email')->get();

// Hanya satu kolom
$emails = User::pluck('email');

Dan jangan memuat seluruh data hanya untuk menghitung atau mengecek:

User::count();                                  // bukan User::all()->count()
Post::where('user_id', 5)->exists();            // bukan ->get()->isNotEmpty()

Menghitung relasi tanpa memuatnya

$posts = Post::withCount('comments')->get();
// akses: $post->comments_count

$users = User::withSum('orders', 'total')->get();    // $user->orders_sum_total

Jauh lebih efisien daripada memanggil $post->comments->count() untuk setiap baris.

Masalah #3: Memproses data besar sekaligus

Memuat jutaan baris ke memori akan menghabiskan RAM. Pecah:

// chunk: ambil per 1000 baris
Pesanan::where('status', 'baru')->chunkById(1000, function ($pesanan) {
    foreach ($pesanan as $p) { /* proses */ }
});

// cursor: satu model dalam memori pada satu waktu (memakai generator)
foreach (Pesanan::where('status', 'baru')->cursor() as $p) { /* proses */ }

// lazy: seperti chunk tetapi API-nya aliran (LazyCollection)
Pesanan::lazyById(1000)->each(fn ($p) => ...);

Gunakan chunkById, bukan chunk, jika kamu mengubah kolom yang dipakai untuk penyaringan selama iterasi, agar tidak ada baris yang terlewat.

Untuk perubahan massal, lakukan di database, bukan di PHP:

Pesanan::where('status', 'baru')->update(['status' => 'diproses']);   // 1 query

bukan perulangan yang memanggil save() untuk tiap baris.

Masalah #4: Pagination yang berat

  • paginate() menjalankan query COUNT(*) tambahan. Jika kamu tidak butuh total halaman, gunakan simplePaginate() atau cursorPaginate() (paling efisien untuk tabel besar).
  • Untuk tabel besar dengan banyak filter, pertimbangkan menghitung total secara terpisah atau memakai cache.

Masalah #5: Kurangnya index

Eloquent hanya membuat SQL; kecepatannya tetap ditentukan index di database. Pastikan kolom di where, orderBy, dan foreign key punya index ($table->index([...]) di migration). Lihat Index Database dan Pengaruhnya terhadap Performa.

Masalah #6: Menghitung hal yang sama berulang kali

Caching menyimpan hasil agar tidak dihitung ulang:

$kategori = Cache::remember('kategori.semua', now()->addHour(), function () {
    return Kategori::orderBy('nama')->get();
});

Panduan:

  • Cache data yang sering dibaca, jarang berubah (menu, kategori, pengaturan, statistik).
  • Hapus atau perbarui cache saat data berubah (Cache::forget('kategori.semua')), atau gunakan cache tags pada driver yang mendukung (Redis).
  • Gunakan driver cepat seperti Redis di produksi, bukan file.
  • Aktifkan cache konfigurasi dan route: php artisan config:cache dan route:cache.

Masalah #7: Pekerjaan berat di dalam request

Kirim email, olah gambar, atau buat laporan PDF di dalam request membuat pengguna menunggu. Pindahkan ke queue:

ProsesLaporan::dispatch($laporan);     // dikerjakan worker di latar belakang

Contoh sebelum-sesudah

Sebelum (halaman daftar pesanan, 1 + 2N query, memuat semua kolom):

$pesanan = Pesanan::all();
foreach ($pesanan as $p) {
    echo $p->pelanggan->nama . ' - ' . $p->items->count();
}

Sesudah (3 query, hanya kolom perlu, terpaginasi):

$pesanan = Pesanan::select('id', 'pelanggan_id', 'total', 'created_at')
    ->with('pelanggan:id,nama')
    ->withCount('items')
    ->latest()
    ->cursorPaginate(25);

Alur kerja optimasi yang disarankan

  1. Ukur: aktifkan Debugbar/Telescope, catat jumlah query dan waktu halaman bermasalah.
  2. Cari pola: apakah N+1? kolom berlebih? query tanpa index?
  3. Perbaiki satu hal pada satu waktu.
  4. Ukur lagi untuk memastikan ada peningkatan nyata.
  5. Cegah regresi: aktifkan preventLazyLoading, tulis test untuk halaman kritis (misalnya assert jumlah query).

Kesimpulan

Mayoritas masalah performa Laravel berasal dari N+1, pengambilan data berlebih, dan ketiadaan index. Gunakan eager loading, select/withCount, chunk/cursor untuk data besar, cache untuk data berulang, dan queue untuk pekerjaan berat, dan selalu ukur sebelum dan sesudah mengubah.

B
Budi

Menulis tentang pemrograman, database, AI, dan karier di dunia IT supaya belajar terasa lebih mudah dan menyenangkan.

Komentar (0)

Komentar hanya untuk member. Masuk atau daftar gratis untuk ikut berdiskusi.

Mau belajar lebih dalam?

Course online dengan latihan coding interaktif dan sertifikat.

Lihat Course →

Gabung komunitas

Tanya jawab, info event, dan teman belajar di grup WhatsApp.

Gabung Grup