Sebagian besar isu performance di backend umumnya disebabkan oleh masalah pengolahan database. Database sangat mudah menjadi bottleneck isu performance karena cara penggunaan query yang kurang tepat dapat berakibat sangat fatal yang mengakibatkan loading halaman menjadi lambat, atau bahkan bisa sampai timeout. Kali ini saya ingin sedikit berbagi mengenai isu yang sangat mendasar yaitu N+1 Query.
Masalah N+1 Query ini sangat umum terjadi di bahasa backend manapun. Penyebabnya adalah karena kita melakukan eksekusi query yang terlalu banyak tanpa disadari. Masalah ini sangat umum terjadi ketika kita melakukan eksekusi query A, lalu melakukan eksekusi query lainnya (B) didalam sebuah looping. Karena itu N yang dimaksud dalam istilah ini adalah N = jumlah perulangan, +1 merujuk pada query awal yang memicu terjadinya perulangan sebanyak N.
Masalah ini sangat sering terjadi terutama di Laravel karena fitur Model yang sangat memudahkan. Dengan Model Laravel, kita dapat mengakses relasi dengan sangat mudah, akan tetapi jika tidak hati-hati ya pasti akan terjebak di masalah N+1 ini. Sebagai contoh, kita memiliki sebuah model bernama Post yang memiliki relasi bernama category. Lalu saat kita ingin menampilkan seluruh data post, kita memanggilnya dengan cara seperti ini :
...
public function getPost(){
// memanggil seluruh data post
$posts = Post::where('is_active', 1)->get();
$data = new Array();
foreach($posts as $row){
// assign $row ke array $data
$data[] = [
'title' => $row->title,
'content' => $row->content,
'category' => $row->category->category_name, //inilah sumber masalah N+1 di model Laravel
'created_at' => $row->created_at,
];
}
return $data;
}
...
Jika kita melihat potongan method diatas, sekilas tidak ada yang aneh. Post::get() di awal hanya melakukan 1x pemanggilan query, lalu melakukan looping sebanyak jumlah data untuk menyimpan response query yang diterima ke sebuah array. Sayangnya, jika kita memperhatikan di bagian $row->category->category_name, disana kita dapat melihat ada akses object relation ->category, dan mengembalikan field category_name. Saat mengakses object relation, sebenarnya saat itu juga Laravel mengeksekusi query baru untuk mendapatkan category_name dengan query seperti "SELECT * FROM categories WHERE category_id = ?" sebanyak jumlah looping.
Bayangkan jika $posts adalah 1 query yang mengembalikan 1000 baris data, artinya ada pengulangan query select category_name sebanyak 1000x, sehingga total query yang dieksekusi di bagian ini adalah 1000+1 = 1001 query! Mungkin untuk data sedikit tidak terlihat ada masalah, tapi bayangkan jika jumlah loopingnya suatu saat mencapai 1.000.000!
Untungnya, menyelesaikan masalah N+1 Query di Laravel dengan contoh diatas sama sekali tidak sulit (N+1 ini akan lebih sulit diperbaiki di bahasa pemrograman lain). Kita hanya perlu memanfaatkan fitur model Eager Loading, agar data relasi langsung dieksekusi dari awal, sehingga biarpun loop dilakukan ribuan kali, jumlah query yang benar-benar dieksekusi hanya 1 + jumlah relasi saja! Eager loading di Laravel pun sangat mudah dilakukan, kita hanya perlu menambahkan method with() di model yang ingin kita lakukan perulangan.
...
public function getPost(){
// memanggil seluruh data post
$posts = Post::with('category')->where('is_active', 1)->get();
// selebihnya tidak ada perubahan!
$data = new Array();
foreach($posts as $row){
// assign $row ke array $data
$data[] = [
'title' => $row->title,
'content' => $row->content,
'category' => $row->category->category_name, //karena sudah dieagerload di awal, baris ini tidak akan mengeksekusi query baru lagi
'created_at' => $row->created_at,
];
}
return $data;
}
...
Apakah hanya semudah itu? Ya benar. Laravel benar-benar framework yang elegan sehingga segala kebutuhannya sudah dipikirkan dengan matang. Dalam kasus-kasus tertentu, saya cenderung lebih memilih raw query daripada memanfaatkan model untuk performance yang lebih baik. Akan tetapi sebenarnya dengan selalu ingat memberi eager loading di model saja sudah sangat membantu lho.
Kesimpulannya, untuk mencegah isu performance N+1 kita harus mencegah segala kemungkinan yang menyebabkan kita melakukan pemanggilan query didalam sebuah looping. Eager loading adalah solusi untuk mencegah pemanggilan query yang tidak kita sadari karena memanggil relasi model didalam looping saat dibutuhkan.
Demikian yang bisa saya bagikan saat ini, dalam postingan selanjutnya saya akan berbagi tentang issue performance lain yang tidak kalah pentingnya.
Referensi :
https://laravel.com/docs/8.x/eloquent-relationships#eager-loading