Query builder
Eloquent dibangun di atas query builder. Untuk laporan, agregasi, dan operasi massal, sering kali lapisan bawah inilah yang paling tepat — dan paling cepat.
Intisari
- Semua nilai di-bind sebagai parameter, jadi kebal SQL injection selama kamu tidak menyambung string sendiri.
chunkById()untuk data besar.get()atas jutaan baris akan menghabiskan memori.upsert()danupdateOrInsert()menggantikan pola cek-lalu-simpan yang rawan balapan.- Query builder mengembalikan objek
stdClass, bukan model — tanpa event, tanpa cast, tanpa relasi. ->toSql()dan->dd()memperlihatkan SQL sebelum dijalankan.
Bentuk dasar
use Illuminate\Support\Facades\DB;
$produk = DB::table('produk')
->select('produk.id', 'produk.nama', 'kategori.nama as kategori')
->join('kategori', 'kategori.id', '=', 'produk.kategori_id')
->where('produk.stok', '>', 0)
->whereIn('produk.kategori_id', [1, 2, 3])
->whereBetween('produk.harga', [10_000, 100_000])
->whereNull('produk.deleted_at')
->orderByDesc('produk.created_at')
->limit(50)
->get();
Menyusun kondisi secara dinamis
$q = DB::table('produk');
// when(): hanya menambahkan klausa kalau syaratnya terpenuhi
$q->when($request->filled('q'), fn ($q) =>
$q->where('nama', 'like', '%'.$request->input('q').'%'))
->when($request->filled('kategori'), fn ($q) =>
$q->where('kategori_id', $request->integer('kategori')))
->when($request->boolean('tersedia'), fn ($q) =>
$q->where('stok', '>', 0));
$hasil = $q->paginate(20);
when() menggantikan tumpukan if. Tanpa itu, filter dinamis berakhir sebagai
belasan if ($request->has(...)) { $q->where(...); } yang sulit dibaca. Dengan
when(), rangkaiannya tetap satu ekspresi.
Agregasi dan pengelompokan
DB::table('pesanan')->count();
DB::table('pesanan')->where('status', 'dibayar')->sum('total');
DB::table('pesanan')->avg('total');
$per_bulan = DB::table('pesanan')
->selectRaw('DATE_FORMAT(created_at, "%Y-%m") as bulan, COUNT(*) as jumlah, SUM(total) as omzet')
->where('created_at', '>=', now()->subYear())
->groupBy('bulan')
->havingRaw('COUNT(*) > 10')
->orderBy('bulan')
->get();
Untuk laporan seperti ini, query builder lebih tepat daripada Eloquent: kamu tidak butuh model, event, atau relasi — kamu butuh satu query yang menghasilkan angka.
Data besar: chunkById, bukan get
// BAHAYA — memuat seluruh tabel ke memori
foreach (DB::table('pesanan')->get() as $p) { /* ... */ }
// BENAR — 1.000 baris per putaran
DB::table('pesanan')->orderBy('id')->chunkById(1000, function ($baris) {
foreach ($baris as $p) { /* ... */ }
});
// Lebih hemat lagi: satu baris pada satu waktu, kursor tetap terbuka
foreach (DB::table('pesanan')->orderBy('id')->cursor() as $p) { /* ... */ }
chunkById(), bukan chunk(). chunk() memakai OFFSET,
jadi kalau kamu mengubah baris yang sedang diiterasi (misalnya mengisi kolom yang jadi syarat filter),
sebagian baris akan terlewat karena posisinya bergeser. chunkById() menelusuri berdasarkan primary
key dan kebal terhadap masalah itu.
Tulis massal
// Satu INSERT untuk banyak baris — jauh lebih cepat daripada perulangan create()
DB::table('log_impor')->insert([
['berkas' => 'a.csv', 'baris' => 120],
['berkas' => 'b.csv', 'baris' => 340],
]);
// Insert kalau baru, update kalau sudah ada — satu query
DB::table('stok_harian')->upsert(
values: [
['produk_id' => 1, 'tanggal' => '2026-08-12', 'jumlah' => 10],
['produk_id' => 2, 'tanggal' => '2026-08-12', 'jumlah' => 4],
],
uniqueBy: ['produk_id', 'tanggal'], // butuh unique index di kolom ini
update: ['jumlah'],
);
upsert() menghapus satu kelas balapan. Pola "cek dulu, kalau tidak ada baru insert"
bisa dijalankan dua proses bersamaan dan menghasilkan duplikat. upsert() menyerahkannya ke
database sebagai satu operasi atomik — asalkan unique index-nya benar-benar ada.
Query builder atau Eloquent?
| Kebutuhan | Pakai | Kenapa |
|---|---|---|
| CRUD satu entitas | Eloquent | Relasi, cast, event, policy ikut bekerja |
| Laporan dan agregasi | Query builder | Tidak butuh objek model, jauh lebih ringan |
| Update jutaan baris | Query builder | Satu query, bukan sejuta objek |
| Ada logika bisnis di model | Eloquent | Query builder melewati semua event |
| Migrasi & skrip pemeliharaan | Query builder | Tidak terikat bentuk model saat ini |
Konsekuensi yang gampang terlewat. DB::table('produk')->delete() tidak menjalankan
event deleting, tidak menghormati soft delete, dan tidak menyentuh relasi. Kadang itu
persis yang kamu mau; kadang itu bug yang baru ketahuan sebulan kemudian saat ada yang bertanya kenapa berkas
terkait tidak ikut terhapus.
Mengintip SQL
$q = DB::table('produk')->where('stok', '>', 0);
$q->toSql(); // "select * from `produk` where `stok` > ?"
$q->toRawSql(); // lengkap dengan nilainya — untuk ditempel ke klien SQL
$q->dd(); // cetak lalu hentikan
$q->explain()->dd(); // rencana eksekusi — apakah index-nya terpakai?
Latihan: tulis satu query laporan yang menghasilkan omzet per bulan selama setahun terakhir, jalankan
->explain()->dd(), dan periksa apakah database memindai seluruh tabel. Lalu tambahkan index
pada created_at dan jalankan ulang explain-nya.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.