Halaman daftar pesanan bisa terlihat sederhana bagi pengguna: tampilkan nama pelanggan, tanggal, total, dan status. Namun di belakang layar, aplikasi PHP mungkin menjalankan satu query untuk mengambil daftar pesanan, lalu satu query lagi untuk setiap pelanggan. Jika ada 100 pesanan, database bisa menerima 101 query hanya untuk satu halaman.
Pola ini dikenal sebagai N+1 query. Angka “N” adalah jumlah data utama yang diproses, sedangkan angka “1” adalah query awal untuk mengambil data tersebut. Masalahnya bukan sekadar jumlah query, tetapi biaya perjalanan berulang antara aplikasi dan database, pekerjaan parsing, pencarian data, serta beban koneksi yang ikut menumpuk.
Seperti apa pola N+1?
Contoh sederhananya bisa terlihat seperti ini:
$orders = $pdo->query("SELECT id, customer_id, total FROM orders")->fetchAll();
foreach ($orders as $order) {
$stmt = $pdo->prepare("SELECT name FROM customers WHERE id = ?");
$stmt->execute([$order['customer_id']]);
$customer = $stmt->fetch();
echo $customer['name'];
}Query pertama mengambil semua pesanan. Query berikutnya dipanggil di dalam perulangan untuk mengambil pelanggan setiap pesanan. Jika beberapa pesanan dimiliki pelanggan yang sama, aplikasi tetap dapat meminta data pelanggan berulang kali.
Di lingkungan lokal dengan data sedikit, pola ini mungkin tidak terasa. Setelah jumlah data bertambah atau server database berada di mesin berbeda, waktu tunggu dari banyak query kecil dapat menjadi bagian besar dari waktu respons. Ini adalah analisis praktis tentang pola akses aplikasi, bukan berarti setiap query di dalam perulangan pasti salah. Kadang-kadang query berulang memang diperlukan, tetapi sebaiknya keputusan itu dibuat setelah melihat data dan pengukuran.
Perbaikan pertama: ambil data dengan JOIN
Untuk kasus daftar pesanan dan pelanggan, satu query gabungan biasanya lebih mudah dipahami dan lebih efisien:
SELECT
o.id,
o.total,
o.created_at,
c.name AS customer_name
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id
ORDER BY o.created_at DESC;Dengan cara ini, database diminta menggabungkan data yang memang akan ditampilkan. Aplikasi tidak perlu meminta detail pelanggan satu per satu.
Gunakan LEFT JOIN jika pesanan tetap perlu ditampilkan walaupun data pelanggan terkait tidak ditemukan. Gunakan JOIN atau INNER JOIN jika hanya baris yang memiliki pasangan yang ingin ditampilkan. Perbedaan kecil ini penting ketika data lama atau relasi yang tidak lengkap masih mungkin ada.
Jika JOIN belum cocok, gunakan pola eager loading
Tidak semua data perlu dipaksa menjadi satu query besar. Pada beberapa kasus, terutama ketika relasi cukup kompleks, pendekatan yang lebih rapi adalah eager loading: ambil data utama terlebih dahulu, kumpulkan semua ID yang dibutuhkan, lalu ambil data terkait dengan satu query menggunakan IN.
$orders = $pdo->query(
"SELECT id, customer_id, total FROM orders"
)->fetchAll();
$customerIds = array_unique(array_column($orders, 'customer_id'));
$placeholders = implode(',', array_fill(0, count($customerIds), '?'));
$stmt = $pdo->prepare(
"SELECT id, name FROM customers WHERE id IN ($placeholders)"
);
$stmt->execute($customerIds);
$customers = [];
foreach ($stmt->fetchAll() as $customer) {
$customers[$customer['id']] = $customer;
}Setelah itu, aplikasi dapat mencari nama pelanggan dari array $customers, bukan kembali ke database untuk setiap baris. Pastikan daftar ID tidak kosong sebelum membentuk query IN, karena hasil IN () dapat menjadi SQL yang tidak valid.
Contoh di atas juga menggunakan PDO::prepare() untuk parameter. Dokumentasi PHP menyarankan agar nilai masukan diberikan melalui parameter, bukan digabung langsung ke string SQL. Prepared statement membantu driver menangani nilai parameter dan mengurangi risiko kesalahan ketika data mengandung karakter khusus. ([php.net](https://www.php.net/pdo.prepare?utm_source=openai))
Jangan menebak: ukur query yang benar-benar berjalan
Sebelum memperbaiki, cari tahu apakah masalahnya memang N+1. Tambahkan pencatatan sederhana untuk nama query, durasi, dan jumlah pemanggilan. Di framework, gunakan fitur query log atau profiler yang tersedia. Di aplikasi PHP biasa, lapisan pembungkus di sekitar PDO dapat mencatat waktu sebelum dan sesudah execute().
Perhatikan pola seperti:
- Query yang sama muncul puluhan atau ratusan kali dalam satu request.
- Query mengambil satu baris berdasarkan ID yang berbeda-beda.
- Waktu halaman meningkat sebanding dengan jumlah item yang ditampilkan.
- Query utama terlihat cepat, tetapi total waktu request tetap tinggi.
Setelah menemukan query yang lambat atau terlalu sering dipanggil, jalankan EXPLAIN. MySQL menggunakan perintah ini untuk menunjukkan rencana eksekusi, termasuk cara tabel dibaca, urutan join, dan indeks yang dipilih. Pada versi yang mendukungnya, EXPLAIN ANALYZE dapat membantu membandingkan rencana dengan pelaksanaan aktual. ([dev.mysql.com](https://dev.mysql.com/doc/refman/8.4/en/explain.html?utm_source=openai))
EXPLAIN
SELECT o.id, o.total, c.name
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id
WHERE o.status = 'paid';Indeks membantu, tetapi bukan pengganti desain query
Kolom yang dipakai untuk relasi dan penyaringan perlu diperiksa. Biasanya, kolom primary key pada tabel pelanggan sudah memiliki indeks. Kolom seperti orders.customer_id, orders.status, atau gabungan kolom yang sering dipakai bersama mungkin memerlukan indeks tambahan.
Namun, menambahkan indeks secara membabi buta juga bukan solusi. Indeks menambah pekerjaan saat data ditulis dan memerlukan ruang penyimpanan. Untuk query yang selalu memfilter status lalu mengurutkan waktu, indeks gabungan bisa dipertimbangkan setelah melihat pola akses dan hasil EXPLAIN. MySQL dapat memanfaatkan indeks multi-kolom dari kolom paling kiri dan urutan kolomnya memengaruhi query yang bisa dibantu. ([dev.mysql.com](https://dev.mysql.com/doc/refman/9.7/en/multiple-column-indexes.html?utm_source=openai))
Apa artinya bagi kita?
N+1 query adalah pengingat bahwa performa aplikasi tidak hanya ditentukan oleh cepat atau lambatnya satu SQL. Cara aplikasi menyusun banyak query juga penting. Kode yang terlihat bersih karena setiap objek mengambil datanya sendiri dapat menghasilkan beban besar ketika jumlah objek bertambah.
Yang bisa dilakukan sekarang:
- Ambil satu halaman data dengan jumlah item yang realistis, lalu catat jumlah query-nya.
- Cari query yang sama dan berulang di dalam
foreachatau fungsi pemanggil relasi. - Ubah menjadi
JOINatau pengambilan batch denganINjika memang sesuai kebutuhan. - Jalankan
EXPLAINpada query baru, bukan hanya membandingkan waktu secara kasat mata. - Uji dengan data yang lebih mendekati kondisi produksi.
Perbaikan terbaik biasanya bukan query yang paling rumit, melainkan query yang jelas, terukur, dan mengambil data sesuai kebutuhan halaman. Dengan kebiasaan memeriksa jumlah query sejak tahap pengembangan, masalah performa dapat ditemukan sebelum pengguna menemukannya lebih dulu.
Sumber & bacaan lebih lanjut
- PHP Manual: PDO::prepare
- PHP Manual: Prepared statements and stored procedures
- MySQL 8.4 Reference Manual: EXPLAIN Statement
- MySQL 8.4 Reference Manual: EXPLAIN Output Format
- MySQL Reference Manual: Multiple-Column Indexes
– Rio Yotto @rioyotto
