Home / Articles / Web Development
Web Development

Query N+1 di PHP dan MySQL: Penyebab Website Lambat yang Sering Tidak Terlihat

Halaman bisa terasa lambat bukan karena satu query yang buruk, melainkan karena ratusan query kecil yang dipanggil berulang kali. Kenali pola N+1 dan perbaiki dari sumbernya sebelum menambah server atau cache.

Query N+1 di PHP dan MySQL: Penyebab Website Lambat yang Sering Tidak Terlihat

Ketika halaman daftar produk terasa lambat, dugaan pertama biasanya mengarah ke hosting, koneksi internet, atau ukuran gambar. Padahal, ada masalah yang sering lebih sederhana dan tersembunyi di dalam kode: aplikasi menjalankan satu query untuk mengambil daftar utama, lalu satu query lagi untuk setiap item di dalam daftar tersebut.

Pola ini dikenal sebagai N+1 query. Jika halaman menampilkan 100 pesanan, aplikasi mungkin menjalankan satu query untuk mengambil 100 pesanan, kemudian 100 query tambahan untuk mengambil data pengguna, alamat, atau detail lain. Di komputer pengembangan, masalah ini bisa terasa biasa saja. Setelah jumlah data bertambah, waktu respons ikut membengkak.

Memahami pola N+1 dengan contoh sederhana

Misalnya, sebuah halaman menampilkan daftar artikel dan nama penulisnya. Kode awalnya bisa terlihat masuk akal:

$posts = $pdo->query("SELECT id, title, author_id FROM posts LIMIT 100")->fetchAll();

foreach ($posts as $post) {
    $stmt = $pdo->prepare("SELECT name FROM users WHERE id = ?");
    $stmt->execute([$post['author_id']]);
    $author = $stmt->fetchColumn();

    echo htmlspecialchars($post['title']);
    echo htmlspecialchars($author);
}

Secara logika, kode tersebut benar. Namun secara operasional, ada satu query untuk mengambil artikel dan hingga 100 query untuk mengambil penulis. Totalnya bisa menjadi 101 query untuk satu halaman.

Inilah arti huruf N pada N+1: jumlah query tambahan mengikuti jumlah data yang sedang diproses. Jika daftar hanya berisi 10 item, masalahnya mungkin tidak terasa. Jika daftar berisi 1.000 item atau kode tersebut dipanggil berkali-kali dalam satu request, dampaknya menjadi serius.

Kenapa prepared statement tidak menyelesaikan masalah ini?

Prepared statement tetap penting. Fitur ini membantu memisahkan struktur SQL dari nilai input dan merupakan salah satu cara yang tepat untuk menjalankan query dengan parameter. Namun, prepared statement tidak otomatis mengurangi jumlah query yang dijalankan.

Dalam contoh sebelumnya, statement memang bisa dipersiapkan sekali di luar perulangan. Itu mengurangi pekerjaan berulang di sisi aplikasi, tetapi database tetap menerima satu eksekusi untuk setiap penulis.

$stmt = $pdo->prepare("SELECT name FROM users WHERE id = ?");

foreach ($posts as $post) {
    $stmt->execute([$post['author_id']]);
    $author = $stmt->fetchColumn();
}

Perubahan tersebut lebih baik daripada membuat statement baru di setiap putaran, tetapi belum menghilangkan pola N+1. Prepared statement adalah praktik keamanan dan efisiensi yang penting, bukan pengganti desain query yang tepat. ([php.net](https://www.php.net/pdo.prepared-statements?utm_source=openai))

Perbaikan utama: ambil data dengan JOIN

Untuk kasus artikel dan penulis, data yang dibutuhkan biasanya bisa diambil dalam satu query menggunakan JOIN:

SELECT
    posts.id,
    posts.title,
    users.name AS author_name
FROM posts
JOIN users ON users.id = posts.author_id
ORDER BY posts.id DESC
LIMIT 100;

Dengan pendekatan ini, database diminta menyelesaikan hubungan antara kedua tabel sekaligus. Aplikasi menerima hasil yang sudah lebih siap ditampilkan, sehingga tidak perlu bolak-balik menghubungi database untuk setiap baris.

Namun, JOIN bukan berarti selalu lebih cepat dalam semua situasi. Query tetap perlu diuji, terutama ketika tabel besar, relasi bertingkat, atau kolom yang dipakai untuk pencarian belum memiliki indeks yang sesuai.

Cara menemukan N+1 sebelum menebak-nebak

1. Hitung jumlah query per request

Langkah pertama adalah mengetahui berapa banyak query yang dijalankan ketika satu halaman dibuka. Anda bisa menambahkan pencatatan sementara pada lapisan akses database, memakai profiler aplikasi, atau menggunakan alat debugging yang menampilkan daftar query dan durasinya.

Perhatikan pola seperti satu query utama yang diikuti query serupa berulang kali dengan nilai id berbeda. Contohnya, query SELECT name FROM users WHERE id = ? muncul puluhan kali dalam satu request.

2. Amati waktu, bukan hanya jumlah query

Jumlah query memang penting, tetapi tidak semua query memiliki biaya yang sama. Sepuluh query sederhana yang memakai indeks mungkin lebih ringan daripada satu query yang membaca jutaan baris.

MySQL menyediakan slow query log untuk mencatat query yang melewati ambang waktu tertentu. Fitur ini dapat membantu menemukan query yang lambat di lingkungan pengujian atau produksi, dengan tetap memperhatikan dampak penyimpanan log dan privasi data. ([dev.mysql.com](https://dev.mysql.com/doc/refman/8.4/en/slow-query-log.html?utm_source=openai))

3. Gunakan EXPLAIN

Perintah EXPLAIN membantu melihat bagaimana MySQL berencana menjalankan sebuah query: tabel mana yang dibaca lebih dulu, indeks apa yang dipakai, dan perkiraan jumlah baris yang diperiksa.

EXPLAIN
SELECT posts.id, posts.title, users.name AS author_name
FROM posts
JOIN users ON users.id = posts.author_id
ORDER BY posts.id DESC
LIMIT 100;

Jika hasilnya menunjukkan terlalu banyak baris yang harus diperiksa, query mungkin membutuhkan indeks, kondisi filter yang lebih tepat, atau struktur data yang ditinjau ulang. MySQL juga menyediakan EXPLAIN ANALYZE untuk melihat informasi eksekusi yang lebih dekat dengan kondisi nyata. ([dev.mysql.com](https://dev.mysql.com/doc/refman/8.4/en/explain.html?utm_source=openai))

Alternatif ketika JOIN tidak cukup

Tidak semua kasus cocok diselesaikan dengan satu JOIN. Untuk relasi tertentu, Anda bisa mengambil semua ID yang dibutuhkan terlebih dahulu, lalu mengambil data terkait dengan klausa IN.

SELECT id, name
FROM users
WHERE id IN (12, 18, 25, 31);

Hasilnya kemudian dipetakan di PHP menggunakan array berdasarkan ID. Pendekatan ini tetap membutuhkan lebih dari satu query, tetapi jumlahnya tidak lagi bertambah satu per item. Polanya berubah dari satu query per baris menjadi beberapa query yang jumlahnya dapat dikendalikan.

Untuk data yang jarang berubah, cache juga bisa membantu. Namun, cache sebaiknya menjadi lapisan tambahan setelah pola query diperbaiki. Menambahkan cache pada kode yang terus menghasilkan ratusan query hanya dapat menyamarkan masalah, bukan menghilangkannya.

Yang bisa dilakukan sekarang

  1. Catat query dalam satu halaman. Mulai dari halaman yang paling sering dibuka atau paling sering dikeluhkan lambat.
  2. Cari query berulang. Periksa apakah query yang sama dijalankan dengan parameter berbeda di dalam perulangan.
  3. Gabungkan pengambilan data. Gunakan JOIN atau pengambilan data secara batch dengan IN.
  4. Uji dengan data yang realistis. Jangan hanya menguji dengan 10 baris jika produksi akan berisi puluhan ribu baris.
  5. Periksa rencana eksekusi. Jalankan EXPLAIN dan pastikan kolom pencarian serta relasi memiliki indeks yang masuk akal.
  6. Ukur sebelum dan sesudah. Bandingkan jumlah query, waktu respons, dan jumlah baris yang diperiksa.

Intinya

N+1 bukan sekadar masalah “terlalu banyak query”. Ini adalah tanda bahwa aplikasi mengambil data secara bertahap padahal sebagian kebutuhan bisa diselesaikan lebih efisien oleh database.

Perbaikannya tidak selalu berarti membuat SQL yang rumit. Sering kali, langkah yang paling berdampak adalah menghitung query yang benar-benar terjadi, mengenali pola berulang, lalu mengubah cara aplikasi mengambil data. Dengan kebiasaan ini, performa website bisa meningkat tanpa langsung menambah spesifikasi server atau menumpuk lapisan cache.

Sumber & bacaan lebih lanjut

– Rio Yotto @rioyotto