Mengoptimalkan akses database: indexing, pemilihan kolom dengan select, chunking chunk()/lazy() untuk dataset besar, analisis EXPLAIN, serta caching dengan Cache::remember dan fitur baru Cache::touch() di Laravel 13.

Di episode 7-9 kita belajar menulis query yang benar; di episode ini kita belajar membuatnya cepat. Ketika aplikasi mulai tumbuh — ribuan post, jutaan baris log, dashboard yang diakses banyak user — query yang tadinya "jalan" mulai terasa lambat. Masalah ini hampir selalu berakar pada dua hal: query yang mengambil terlalu banyak data, dan query yang sama dihitung berulang-ulang.
Dua senjata utama kita: optimasi query (membuat tiap query lebih ringan) dan caching (menghindari kerja berulang).
Index di database bekerja seperti indeks buku — mempercepat pencarian dengan menukar kecepatan tulis. Foreign key dan kolom yang sering dipakai di where harus di-index:
Schema::table('posts', function (Blueprint $table) {
$table->index('status');
$table->index(['user_id', 'status']); // composite index
});Kolom yang dipakai di where/orderBy/join adalah kandidat index. Ingat: jangan index semuanya — setiap index memperlambat INSERT/UPDATE. Profil dulu, index hanya yang sering dipakai.
php artisan tinker>>> DB::select('EXPLAIN QUERY PLAN SELECT * FROM posts WHERE status = ?', ['published'])Kesalahan paling umum: SELECT * padahal hanya butuh beberapa kolom. Eloquent membuat ini mudah:
$posts = Post::select('id', 'title', 'slug')
->where('status', 'published')
->get();Untuk indeks yang benar-benar ringan, hindari mengambil kolom body (teks panjang) saat hanya menampilkan daftar judul. ->get() lalu ->each() juga menunda kerja sampai dibutuhkan.
get() memuat seluruh hasil ke memory — fatal untuk jutaan baris. Gunakan chunking:
Post::where('status', 'published')
->chunkById(500, function ($posts) {
foreach ($posts as $post) {
// proses per-chunk 500 baris
}
});chunkById aman untuk data yang berubah di tengah proses. Alternatifnya lazy() yang memakai cursor:
foreach (Post::lazy() as $post) {
// hanya satu record di memory pada satu waktu
}Pola ini krusial saat meng-export data, menghitung laporan, atau backfill kolom — di situlah memori jadi batasan nyata.
Jangan menebak query lambat — ukur. Aktifkan query log untuk melihat query apa yang dijalankan per request:
use Illuminate\Support\Facades\DB;
DB::enableQueryLog();
Post::where('status', 'published')->get();
dump(DB::getQueryLog());Untuk query spesifik yang lambat, jalankan EXPLAIN dan periksa apakah database memakai index:
php artisan tinker>>> DB::select('EXPLAIN SELECT * FROM posts WHERE status = ?', ['published'])
=> [
["id" => 1, "type" => "index", "key" => "posts_status_index", ...],
]Jika kolom type menunjukkan ALL (full table scan) padahal seharusnya bisa pakai index, tambahkan index yang sesuai.
Query yang sama untuk data yang jarang berubah sebaiknya di-cache. Laravel mendukung driver file, database, redis, dan array — konfigurasi di config/cache.php:
$stats = Cache::remember('dashboard-stats', 600, function () {
return [
'posts' => Post::count(),
'comments' => Comment::count(),
'users' => User::count(),
];
});Cache::remember memeriksa cache dulu; jika tidak ada, menjalankan closure lalu menyimpannya selama 600 detik. Ini pola cache-aside standar. Invalidation terjadi lewat timeout atau Cache::forget('dashboard-stats') saat data berubah.
Tip
Untuk production multi-server, pakai Redis sebagai cache driver. file dan database bekerja untuk single server, tapi tidak terbagi antar server — Redis membuat semua instance berbagi cache yang sama. Konfigurasi Redis kita bahas praktiknya di episode 14.
Laravel 13 memperkenalkan Cache::touch() — memperpanjang TTL cache tanpa perlu mengambil ulang nilainya:
$stats = Cache::remember('dashboard-stats', 60, fn () => $this->computeStats());
// 30 detik kemudian, user lain datang:
// TTL diperpanjang 60 detik lagi tanpa memanggil computeStats()
Cache::touch('dashboard-stats', 60);Sebelumnya, memperpanjang umur cache berarti membaca nilainya (mahal untuk data besar) lalu menulis ulang. Cache::touch() hanya memperbarui metadata TTL di backend — jauh lebih murah. Sangat berguna untuk pola sliding expiration: cache tetap hidup selama masih ada yang memakainya.
Inti yang harus dibawa pulang:
where/orderBy; jangan index sembarangan.select('id','title') alih-alih SELECT *.chunkById() atau lazy() — jangan get() semua.Cache::remember() untuk data yang jarang berubah; Cache::touch() (Laravel 13) untuk sliding TTL.Di episode 14 selanjutnya kita akan membahas queues & jobs — queue driver database/Redis, ShouldQueue, dispatch(), job chaining, retry/backoff, php artisan queue:work, dan dashboard Horizon untuk Redis queues. Sampai jumpa di episode 14!