31 Mar 2026 • Admin KhalimZone
Database Query Optimization di CI4 — Bikin Aplikasi Jauh Lebih Cepat
Database Query Optimization di CI4 — Bikin Aplikasi Jauh Lebih Cepat
Query database yang buruk adalah pembunuh performa nomor satu. Gue udah liat banyak developer CI4 yang asal tulis query tanpa mikirin efisiensinya — hasilnya satu request handler bisa makan 3–5 detik padahal datanya sederhana.
Di artikel ini, gue bakal breakdown teknik-teknik optimasi database di CI4 yang beneran kepake di production — bukan cuma teori.
select() — jangan ambil kolom yang gak dibutuhin
Kesalahan paling umum: query ambil semua kolom padahal cuma butuh dua atau tiga. Ini mubazir bandwidth, memory, dan waktu transfer — terutama kalau ada kolom LONGTEXT atau BLOB.
// ❌ Ambil semua kolom — termasuk yang gak perlu
$users = $this->model->findAll();
// ✅ Ambil kolom yang beneran dipakai
$users = $this->model
->select('id, name, email')
->findAll();
Di tabel dengan banyak kolom atau data besar, ini bisa hemat 50–70% transfer data per query. Kebiasaan kecil yang impactnya gede.
Tanpa index, database scan seluruh tabel baris per baris — kompleksitas O(n). Dengan index, jadi O(log n). Bedanya bisa dari milidetik ke detik kalau datanya jutaan baris.
public function up()
{
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'email' => ['type' => 'VARCHAR', 'constraint' => 255],
'status' => ['type' => 'VARCHAR', 'constraint' => 50],
'created_at' => ['type' => 'DATETIME'],
]);
$this->forge->addKey('id', true); // Primary key
$this->forge->addUniqueKey('email'); // Unique — sering di WHERE
$this->forge->addKey('status'); // Regular index — filtering
$this->forge->addKey('created_at'); // Index — ORDER BY & date range
$this->forge->createTable('users');
}
Jangan asal tambah index ke semua kolom — setiap index nambah overhead waktu INSERT dan UPDATE. Index kolom yang masuk WHERE, JOIN ON, dan ORDER BY aja.
join() — bukan loop query
N+1 Query Problem adalah biang kerok paling sering — ambil data parent dalam satu query, terus loop buat ambil data child-nya satu per satu. 100 order? 101 query.
// ❌ N+1 — 101 query untuk 100 order
$orders = $this->orderModel->findAll();
foreach ($orders as &$order) {
$order['items'] = $this->itemModel
->where('order_id', $order['id'])
->findAll();
}
Solusinya: satu query dengan join().
// ✅ Satu query — berapapun jumlah order-nya
$orders = $this->db->table('orders o')
->select('o.id, o.total, u.name, oi.qty, p.title')
->join('users u', 'o.user_id = u.id')
->join('order_items oi', 'o.id = oi.order_id')
->join('products p', 'oi.product_id = p.id')
->where('o.status', 'completed')
->get()
->getResultArray();
Cek berapa banyak query yang jalan pakai dd((string) $db->getLastQuery()) atau aktifin CI Debugbar. Kalau angkanya lebih dari yang lo ekspektasi, ada N+1 yang belum kedeteksi.
limit() dan pagination
Panggil findAll() tanpa limit di tabel yang punya jutaan baris itu bukan bug — itu bencana. Database harus load semua data ke memory sekaligus.
// ❌ Potensial load jutaan baris
$products = $this->model->findAll();
// ✅ Pakai paginate() bawaan CI4
$products = $this->model->paginate(20);
return view('products/index', [
'products' => $products,
'pager' => $this->model->pager,
]);
Kalau butuh kontrol manual offset-nya:
$perPage = 20;
$page = (int) ($this->request->getVar('page') ?? 1);
$offset = ($page - 1) * $perPage;
$products = $this->model
->limit($perPage, $offset)
->findAll();
Jadikan rule buat diri sendiri: findAll() tanpa limit() gak boleh ada di production code. Pasti ada potensi masalahnya di masa depan.
Query yang sama dijalankan berkali-kali dengan hasil yang sama — itu pekerjaan sia-sia buat database. Cache hasilnya dan baca dari memory.
public function getActiveCategories(): array
{
$cache = service('cache');
$cacheKey = 'categories_active';
if ($cached = $cache->get($cacheKey)) {
return $cached;
}
$data = $this->model
->where('status', 'active')
->orderBy('name', 'ASC')
->findAll();
$cache->save($cacheKey, $data, 3600); // cache 1 jam
return $data;
}
Jangan lupa bust cache-nya kalau data berubah:
// Setelah update kategori, hapus cache lama
service('cache')->delete('categories_active');
Setup handler-nya di app/Config/Cache.php — CI4 support Redis, Memcached, dan file cache out of the box.
Cache data yang shared dan jarang berubah: kategori, konfigurasi, daftar provinsi/kota. Jangan cache data yang user-specific atau real-time.
whereIn() sebagai alternatif join yang ringan
Kalau join terlalu kompleks untuk kasus tertentu, whereIn() adalah jalan tengah yang tetap efisien — tetap satu query, tapi lebih mudah dikontrol hasilnya di PHP.
// ❌ N+1 — query per ID
foreach ($userIds as $id) {
$users[] = $this->model->find($id);
}
// ✅ Satu query, semua ID sekaligus
$users = $this->model
->whereIn('id', $userIds)
->findAll();
Contoh real-world — ambil semua user dari daftar order:
$orders = $this->orderModel->findAll();
$userIds = array_unique(array_column($orders, 'user_id'));
// Ambil semua user dalam 1 query
$users = $this->userModel
->select('id, name, email')
->whereIn('id', $userIds)
->findAll();
// Index by ID buat merge yang efisien
$usersById = array_column($users, null, 'id');
foreach ($orders as &$order) {
$order['user'] = $usersById[$order['user_id']] ?? null;
}
getLastQuery() dan CI Debugbar
Jangan tebak-tebak mana query yang lambat. Lihat langsung — berapa query yang jalan, query apa, dan masing-masing butuh berapa lama.
$db = db_connect();
$result = $this->model
->where('status', 'active')
->findAll();
// Tampilin query yang baru aja dieksekusi
dd((string) $db->getLastQuery());
// Atau lihat semua query dalam satu request
foreach ($db->getQueries() as $q) {
echo $q->getQuery() . ' [' . $q->getDuration(4) . 's]' . PHP_EOL;
}
Untuk analisis lebih dalam, install CI4 Debugbar:
composer require codeigniter4/codeigniter4-debugbar --dev
Dengan CI_ENVIRONMENT = development aktif, Debugbar kasih breakdown lengkap: total query per request, execution time masing-masing, memory usage, dan route info.
Debugbar dan getQueries() hanya untuk development. Pastiin CI_ENVIRONMENT = production sebelum deploy — keduanya otomatis nonaktif.
Kalau ada query panjang dengan multiple join yang dipanggil di banyak tempat, pindahkan logikanya ke database view. Kode PHP-nya jadi bersih, dan query optimizer bisa handle lebih efisien.
CREATE VIEW order_summary AS
SELECT
o.id,
o.total,
u.name AS customer_name,
COUNT(oi.id) AS item_count,
SUM(oi.qty) AS total_qty
FROM orders o
JOIN users u ON o.user_id = u.id
LEFT JOIN order_items oi ON o.id = oi.order_id
GROUP BY o.id, o.total, u.name;
Di CI4 tinggal query view-nya seperti table biasa:
$summary = $this->db
->table('order_summary')
->where('total >', 100000)
->get()
->getResultArray();
View dihitung on-the-fly, jadi bukan silver bullet untuk data yang terus-terusan diakses. Kalau perlu hasil yang pre-computed, pertimbangkan materialized table — copy hasil ke tabel fisik dan refresh via scheduled command.
Performance database bukan sesuatu yang dipikirin belakangan — itu fondasi. Index yang tepat, eliminasi N+1, pagination, caching, dan monitoring query adalah hal-hal yang harus jadi habit dari awal nulis kode, bukan ditambal pas aplikasi udah lambat. Mulai dari aktifin Debugbar, liat berapa banyak query yang jalan di satu request, dan selesaikan satu per satu. Di artikel berikutnya gue bakal bahas CI4 Event & Hook — cara bikin arsitektur yang decoupled tanpa class satu pun tau soal class lain.