💡 TIPS UNGGULAN
Programming
Intermediate
6 min read

7 Query Eloquent yang Sering Bikin Aplikasi Laravel Lambat (dan Cara Memperbaikinya)

N+1 query, select berlebihan, sampai lupa index — 7 pola yang paling sering ditemukan saat code review, lengkap dengan contoh kode sebelum dan sesudah diperbaiki.

KT

Tim Javasal Code

24 Agt 2026
12 views

Aplikasi Laravel yang lambat hampir selalu bukan karena "Laravel-nya lambat" — hampir selalu karena cara query Eloquent ditulis. Kami sudah melakukan puluhan code review untuk proyek klien, dan pola-pola berikut ini muncul berulang kali, bahkan di tim yang sudah berpengalaman. Artikel ini membahas 7 pola paling umum, kenapa masing-masing bikin aplikasi lambat, dan cara memperbaikinya dengan contoh kode yang bisa langsung Anda pakai.

1. N+1 Query Problem

Ini penyebab lambat nomor satu yang paling sering luput dari perhatian karena kodenya terlihat baik-baik saja saat ditulis, dan baru terasa lambat setelah data production bertambah banyak.

// ❌ Buruk: 1 query untuk posts, lalu 1 query TAMBAHAN untuk tiap post
$posts = Post::all();

foreach ($posts as $post) {
    echo $post->author->name; // query baru setiap iterasi!
}
// Kalau ada 100 post = 101 query total
// Kalau ada 10.000 post = 10.001 query total (!)

Solusinya adalah eager loading dengan with(), yang menggabungkan semua relasi jadi satu query tambahan saja, berapa pun jumlah baris datanya:

// ✅ Baik: cuma 2 query total, berapa pun jumlah post-nya
$posts = Post::with('author')->get();

foreach ($posts as $post) {
    echo $post->author->name; // sudah di-preload, tidak ada query baru
}

N+1 juga bisa terjadi di relasi bersarang (nested). Untuk kasus ini, gunakan dot notation:

// Memuat relasi 2 tingkat sekaligus: post -> author -> country
$posts = Post::with('author.country')->get();

// Memuat beberapa relasi berbeda dalam satu pemanggilan
$posts = Post::with(['author', 'comments', 'tags'])->get();
Tips: install package barryvdh/laravel-debugbar saat development. Kalau lihat jumlah query melonjak sebanding dengan jumlah baris data yang ditampilkan, itu tanda pasti N+1. Anda juga bisa memanggil Model::preventLazyLoading() di AppServiceProvider agar Laravel melempar exception setiap kali lazy loading terjadi selama development — cara paling efektif menangkap N+1 sebelum sempat masuk ke production.

2. Mengambil Semua Kolom Padahal Cuma Butuh Beberapa

// ❌ Menarik semua kolom, termasuk kolom besar yang tidak dipakai
$users = User::all();

// ✅ Ambil kolom yang benar-benar dibutuhkan saja
$users = User::select('id', 'name', 'email')->get();

Dampaknya makin terasa kalau tabel punya kolom besar seperti TEXT, LONGTEXT, atau JSON yang tidak selalu dipakai di halaman tersebut — setiap baris jadi lebih berat ditransfer dari database ke aplikasi, dan lebih berat lagi disimpan di memori PHP kalau jumlah barisnya banyak.

Aturan ini juga berlaku untuk eager loading — jangan tarik seluruh kolom relasi kalau cuma butuh satu-dua kolom saja:

// ✅ Batasi kolom yang diambil dari relasi juga
$posts = Post::with('author:id,name')->get();

3. Loop Sambil Insert/Update Satu per Satu

// ❌ 500 produk = 500 query INSERT terpisah, masing-masing bolak-balik ke database
foreach ($products as $product) {
    Product::create($product);
}

// ✅ Satu query INSERT untuk semua data sekaligus
Product::insert($products);

Perlu diingat, insert() adalah query builder murni — dia tidak menjalankan event Eloquent seperti creating atau created, dan tidak mengisi created_at/updated_at secara otomatis kecuali Anda sertakan manual. Kalau Anda butuh event model tetap jalan, pertimbangkan insertOrIgnore() atau tetap loop tapi bungkus dalam transaction supaya prosesnya jauh lebih cepat:

DB::transaction(function () use ($products) {
    foreach ($products as $product) {
        Product::create($product);
    }
});
// Masih N query, tapi jauh lebih cepat karena tidak ada commit
// terpisah untuk tiap query — semua di-commit sekali di akhir.

4. Memuat Seluruh Tabel ke Memori untuk Diproses

// ❌ Kalau tabel punya 1 juta baris, ini bisa bikin memory habis (fatal error)
$allOrders = Order::all();
foreach ($allOrders as $order) {
    // proses satu-satu
}

// ✅ chunk() memproses per-batch tanpa menyimpan semuanya di memori
Order::chunk(500, function ($orders) {
    foreach ($orders as $order) {
        // proses per-batch, 500 baris sekaligus
    }
});

Untuk kasus yang benar-benar butuh iterasi satu per satu dengan penggunaan memori paling rendah, cursor() adalah pilihan yang lebih hemat lagi karena menggunakan PHP generator — hanya satu baris yang ada di memori pada satu waktu:

foreach (Order::where('status', 'pending')->cursor() as $order) {
    // setiap $order dimuat satu per satu, memori tetap ringan
}

Catatan penting: chunk() tidak aman dipakai bersamaan dengan update() atau delete() pada kolom yang sama dengan kondisi where-nya, karena bisa membuat baris "melompat" antar-batch. Untuk kasus itu, gunakan chunkById() yang mengurutkan berdasarkan primary key.

5. Menghitung Jumlah Data dengan count() Setelah get()

// ❌ Menarik SEMUA data ke aplikasi, baru dihitung jumlahnya di PHP
$total = Post::where('status', 'published')->get()->count();

// ✅ Hitung langsung di level database, tidak perlu tarik datanya sama sekali
$total = Post::where('status', 'published')->count();

Perbedaannya terlihat sepele di tabel kecil, tapi di tabel dengan ratusan ribu baris, versi pertama bisa memakan waktu berkali-kali lipat lebih lama karena mentransfer seluruh data yang sebenarnya tidak pernah dipakai — hanya jumlahnya saja yang dibutuhkan.

6. Tidak Memakai Index pada Kolom yang Sering Dipakai di WHERE / JOIN

Ini bukan soal Eloquent, tapi soal struktur tabelnya — dan sering jadi penyebab query lambat yang tidak kelihatan dari kode PHP sama sekali.

// Migration tanpa index — query WHERE di kolom ini akan full table scan
Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->string('status');
    $table->foreignId('user_id');
    $table->timestamps();
});

// ✅ Tambahkan index di kolom yang sering dipakai untuk filter/join
Schema::table('orders', function (Blueprint $table) {
    $table->index('status');
    $table->index('user_id'); // foreignId biasanya sudah otomatis diindeks,
                               // tapi selalu cek dengan `SHOW INDEX FROM orders;`
});

Gunakan EXPLAIN di query mentah untuk memastikan MySQL benar-benar memakai index yang sudah Anda buat, bukan tetap melakukan full scan:

DB::select('EXPLAIN SELECT * FROM orders WHERE status = ?', ['pending']);
// Perhatikan kolom `type` di hasilnya — hindari nilai `ALL` (artinya full scan)

7. Query Berulang untuk Data yang Jarang Berubah

Data seperti daftar kategori, pengaturan situs, atau daftar negara sering di-query ulang di setiap request padahal isinya nyaris tidak pernah berubah dalam hitungan menit atau jam.

// ❌ Query yang sama dijalankan ulang di setiap request
$categories = Category::orderBy('name')->get();

// ✅ Cache hasilnya, refresh otomatis setelah durasi tertentu
$categories = Cache::remember('categories.all', now()->addHour(), function () {
    return Category::orderBy('name')->get();
});

Jangan lupa hapus cache-nya (Cache::forget('categories.all')) setiap kali data kategori diubah lewat admin, supaya pengguna tidak melihat data basi.

Cara Mendeteksi Masalah Ini di Proyek Anda

  • Pasang Laravel Debugbar di environment development untuk melihat jumlah dan durasi query di tiap halaman secara langsung.
  • Aktifkan slow query log di MySQL (long_query_time) untuk menemukan query yang benar-benar lambat di production, bukan cuma dugaan.
  • Gunakan DB::listen() di AppServiceProvider untuk mencatat setiap query beserta waktu eksekusinya ke log selama proses debugging.
  • Biasakan review query di setiap fitur baru sebelum merge — bukan menunggu sampai user melapor aplikasi lemot.

Kesimpulan

Ketujuh pola ini bukan teknik optimasi tingkat lanjut — ini kebiasaan dasar yang seharusnya jadi refleks setiap developer Laravel, sama seperti kebiasaan menulis migration atau validasi. Cek ulang query di fitur yang sedang Anda kerjakan sekarang; ada peluang besar setidaknya satu dari tujuh pola ini bersembunyi di sana, menunggu ditemukan sebelum user Anda yang menemukannya lebih dulu lewat keluhan aplikasi lemot.

Artikel ini bermanfaat? 🚀

Bantu teman-teman developer lainnya dengan membagikan tips ini. Semakin banyak yang terbantu, semakin kuat komunitas tech Indonesia!

Lihat Tips Lainnya

Tips Terkait Yang Mungkin Anda Suka

Konsultasikan Sekarang