31 Mar 2026 • Admin KhalimZone

Database Query Optimization di CI4 — Bikin Aplikasi Jauh Lebih Cepat

Home / Blog / Database Query Optimization di CI4
CodeIgniter 4 Performance Database

Database Query Optimization di CI4 — Bikin Aplikasi Jauh Lebih Cepat

Khalim Database & Performance 13 menit baca

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.

Optimization Techniques
01 Pakai 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.

php
// ❌ 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.

02 Pasang index di kolom yang sering di-query

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.

app/Database/Migrations/..._CreateUsersTable.php
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.

03 Solve N+1 dengan 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.

php
// ❌ 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().

php
// ✅ 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.

04 Selalu pakai 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.

php
// ❌ 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:

php
$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.

05 Cache hasil query yang jarang berubah

Query yang sama dijalankan berkali-kali dengan hasil yang sama — itu pekerjaan sia-sia buat database. Cache hasilnya dan baca dari memory.

php
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:

php
// 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.

06 Gunakan 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.

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:

php
$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; }
07 Debug query dengan 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.

php
$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:

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

08 Pakai Database View untuk query kompleks yang berulang

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.

sql
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:

php
$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.


Penutup

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.