Home / Articles / Web Development
Web Development

Halaman Web Lambat Padahal Query Terlihat Benar? Kenali Masalah N+1 Query

Aplikasi PHP dan MySQL bisa terasa lambat bukan karena satu query yang buruk, melainkan karena terlalu banyak query kecil yang dijalankan berulang. Kenali pola N+1 query dan pelajari cara memperbaikinya tanpa mengubah s…

Halaman Web Lambat Padahal Query Terlihat Benar? Kenali Masalah N+1 Query

Ketika halaman daftar pesanan mulai lambat, banyak pengembang langsung memeriksa ukuran tabel atau menambah index. Langkah itu memang kadang membantu, tetapi ada masalah lain yang sering luput: aplikasi menjalankan terlalu banyak query untuk menghasilkan satu halaman.

Salah satu penyebab paling umum adalah N+1 query. Polanya sederhana: aplikasi menjalankan satu query untuk mengambil daftar utama, lalu menjalankan satu query tambahan untuk setiap baris data. Jika ada 100 pesanan, halaman tersebut bisa memicu 101 query ke database.

Masing-masing query mungkin terlihat cepat ketika diuji sendiri. Masalahnya muncul ketika semuanya dijalankan dalam satu request web. Waktu tunggu bertambah, koneksi database lebih sibuk, dan performa memburuk seiring jumlah data.

Apa itu N+1 query?

Bayangkan sebuah halaman menampilkan daftar pesanan beserta nama pelanggan. Kode awalnya mungkin terlihat seperti ini:

$orders = $db->query("SELECT id, customer_id, total FROM orders")->fetchAll();

foreach ($orders as $order) {
    $customer = $db->query(
        "SELECT name FROM customers WHERE id = " . (int) $order['customer_id']
    )->fetch();

    echo $customer['name'];
}

Pada contoh tersebut, query pertama mengambil semua pesanan. Setelah itu, setiap pesanan memicu query baru untuk mengambil data pelanggan. Jika jumlah pesanan adalah N, total query menjadi 1 + N. Itulah asal istilah N+1.

Selain persoalan performa, contoh di atas juga sebaiknya tidak digunakan dalam aplikasi nyata karena menyusun SQL langsung dari nilai input. Walaupun nilai tersebut sudah dikonversi menjadi integer, kebiasaan yang lebih aman adalah menggunakan prepared statement.

Mengapa N+1 query bisa terasa sangat lambat?

Database tidak hanya menghabiskan waktu untuk menjalankan logika pencarian. Setiap query juga melibatkan pengiriman permintaan, pemrosesan, pengembalian hasil, dan komunikasi antara aplikasi dengan server database.

Satu query besar sering kali lebih efisien daripada puluhan atau ratusan query kecil. Dampaknya semakin terasa jika aplikasi dan database berada di server berbeda. Jarak jaringan yang terlihat kecil dapat menjadi signifikan ketika diulang berkali-kali dalam satu halaman.

N+1 query juga sering tidak terlihat pada data uji. Saat hanya ada lima pesanan, enam query mungkin terasa normal. Setelah aplikasi dipakai banyak pengguna dan satu halaman menampilkan ratusan baris, masalahnya baru tampak melalui waktu respons, penggunaan koneksi database, atau beban server.

Cara memperbaikinya dengan JOIN

Solusi yang paling umum adalah mengambil data yang diperlukan dalam satu query menggunakan JOIN. Untuk contoh pesanan dan pelanggan, query-nya dapat diubah menjadi:

SELECT
    o.id,
    o.total,
    c.name AS customer_name
FROM orders o
JOIN customers c ON c.id = o.customer_id
ORDER BY o.id DESC;

Dengan cara ini, database menggabungkan data pesanan dan pelanggan dalam satu operasi. Kode PHP kemudian hanya perlu membaca hasilnya:

$statement = $db->prepare(
    "SELECT o.id, o.total, c.name AS customer_name
     FROM orders o
     JOIN customers c ON c.id = o.customer_id
     ORDER BY o.id DESC"
);

$statement->execute();
$orders = $statement->fetchAll();

foreach ($orders as $order) {
    echo htmlspecialchars($order['customer_name'], ENT_QUOTES, 'UTF-8');
}

Contoh ini bukan hanya mengurangi jumlah query. Pemanggilan htmlspecialchars juga penting ketika data dari database ditampilkan ke HTML, agar karakter khusus tidak langsung diperlakukan sebagai markup.

JOIN bukan selalu jawaban untuk semua kondisi

Menggunakan JOIN secara membabi buta juga bukan praktik yang ideal. Query yang menggabungkan terlalu banyak tabel dapat menghasilkan baris duplikat atau mengambil data jauh lebih banyak daripada yang dibutuhkan.

Contohnya, satu pesanan memiliki banyak produk. Jika pesanan digabungkan langsung dengan tabel detail pesanan dan tabel produk, satu pesanan dapat muncul berkali-kali—satu baris untuk setiap produk. Dalam kondisi seperti ini, pengembang perlu menentukan bentuk data yang memang dibutuhkan oleh halaman.

Beberapa pilihan yang dapat digunakan:

  • Gunakan satu query untuk daftar pesanan dan query kedua untuk mengambil semua detail produk berdasarkan kumpulan ID pesanan.
  • Gunakan agregasi seperti COUNT atau SUM jika halaman hanya membutuhkan jumlah dan total.
  • Pisahkan data ringkasan dan detail ke endpoint berbeda jika detail hanya dibuka ketika pengguna memilih satu pesanan.
  • Gunakan pagination agar aplikasi tidak mengambil ribuan baris sekaligus.

Alternatif: mengambil data terkait secara berkelompok

Tidak semua masalah N+1 harus diselesaikan dengan satu JOIN. Dalam beberapa kasus, pendekatan yang lebih mudah dirawat adalah batch loading: ambil daftar utama, kumpulkan semua ID yang diperlukan, lalu jalankan satu query untuk data terkait.

Misalnya, setelah mengambil pesanan, aplikasi mengumpulkan seluruh customer_id yang unik:

$customerIds = array_unique(array_column($orders, 'customer_id'));

Kemudian aplikasi mengambil semua pelanggan tersebut dengan query menggunakan parameter yang sesuai. Hasilnya disimpan dalam array berdasarkan ID, sehingga kode tidak perlu bertanya ke database untuk setiap baris.

Pendekatan ini berguna ketika struktur data tidak cocok digabungkan dengan JOIN atau ketika data berasal dari sumber berbeda. Prinsipnya tetap sama: jangan mengulang permintaan yang sebenarnya dapat dikelompokkan.

Bagaimana menemukan N+1 query?

Langkah pertama adalah mengamati jumlah query yang dijalankan dalam satu request. Pada lingkungan pengembangan, gunakan logging database atau alat profiling yang dapat menampilkan query, durasi, dan jumlah pemanggilannya.

Perhatikan pola seperti query yang sama muncul puluhan kali dengan parameter berbeda. Misalnya:

SELECT name FROM customers WHERE id = 10;
SELECT name FROM customers WHERE id = 11;
SELECT name FROM customers WHERE id = 12;

Jika query semacam itu muncul setelah query daftar utama, ada kemungkinan besar aplikasi mengalami N+1 query. Periksa juga endpoint API, proses pembuatan laporan, dan kode yang berada di dalam perulangan. Masalah ini tidak hanya terjadi pada halaman HTML.

Yang bisa dilakukan sekarang

  1. Catat jumlah query untuk halaman atau endpoint yang terasa lambat.
  2. Cari query yang sama dan berulang dengan parameter berbeda.
  3. Periksa apakah data tersebut dapat diambil melalui JOIN atau batch loading.
  4. Ambil hanya kolom yang benar-benar digunakan, bukan seluruh isi tabel.
  5. Tambahkan pagination pada daftar yang bisa bertambah besar.
  6. Uji ulang menggunakan data yang jumlahnya mendekati kondisi produksi.
  7. Bandingkan waktu respons sebelum dan sesudah perubahan.

Performa bukan hanya soal menambah index

Index tetap penting, tetapi index tidak dapat memperbaiki desain aplikasi yang terlalu sering meminta data. Sebelum menambah index atau menaikkan spesifikasi server, periksa dulu berapa banyak query yang berjalan dalam satu request.

N+1 query sering muncul dari kode yang tampak rapi: sebuah fungsi mengambil daftar, lalu fungsi lain mengambil detail untuk setiap item. Dengan logging dan pengujian yang tepat, pola tersebut bisa ditemukan lebih awal.

Tujuannya bukan membuat semua halaman hanya menggunakan satu query. Tujuannya adalah memastikan setiap query memang diperlukan, jumlahnya masuk akal, dan bentuk data yang diambil sesuai dengan kebutuhan pengguna.

– Rio Yotto @rioyotto