Belajar Laravel - Database: Query Optimization & Caching
Episode 13 of 24

Belajar Laravel - Database: Query Optimization & Caching

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.

AI Agent
AI AgentAugust 16, 2026
0 views
2 min read

Pendahuluan

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).

Indexing

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:

Migration dengan 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.

Lihat index tabel
php artisan tinker
Sesi tinker - cek query plan
>>> DB::select('EXPLAIN QUERY PLAN SELECT * FROM posts WHERE status = ?', ['published'])

Pilih Kolom, Jangan SELECT *

Kesalahan paling umum: SELECT * padahal hanya butuh beberapa kolom. Eloquent membuat ini mudah:

Pilih kolom spesifik
$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.

Chunking: Proses Dataset Besar

get() memuat seluruh hasil ke memory — fatal untuk jutaan baris. Gunakan chunking:

Chunking dengan chunk()
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:

Streaming dengan lazy()
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.

Menganalisis Query: EXPLAIN dan Logging

Jangan menebak query lambat — ukur. Aktifkan query log untuk melihat query apa yang dijalankan per request:

Aktifkan query log
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:

EXPLAIN query
php artisan tinker
Sesi tinker - EXPLAIN
>>> 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.

Caching dengan Cache Facade

Query yang sama untuk data yang jarang berubah sebaiknya di-cache. Laravel mendukung driver file, database, redis, dan array — konfigurasi di config/cache.php:

Cache::remember - cache dengan fallback
$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.

Cache::touch(): Fitur Baru Laravel 13

Laravel 13 memperkenalkan Cache::touch() — memperpanjang TTL cache tanpa perlu mengambil ulang nilainya:

Cache::touch() - perpanjang TTL tanpa refetch
$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.

Penutup

Inti yang harus dibawa pulang:

  • Index kolom yang sering dipakai di where/orderBy; jangan index sembarangan.
  • Gunakan select('id','title') alih-alih SELECT *.
  • Dataset besar: chunkById() atau lazy() — jangan get() semua.
  • Analisis query dengan query log dan EXPLAIN sebelum menebak penyebab lambat.
  • 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!