N+1 query problem adalah salah satu performance issue yang sering terjadi ketika sebuah app melakukan banyak query yang tidak efisien umumnya permasalahan ini terjadi ketika sebuah app menggunakan ORM (Object Relation Mapping) bawaan framework atau third party library. N+1 Query ini bisa berdampak pada performance sebuah app
N+1 Query Problem Case
Contoh kasus N+1 Query, misal di sebuah web e-commerce terdapa User dan Order dimana satu user bisa punya banyak order, dimana relasi dari kedua object tersebut adalah one to many.
Misal di e-commerce tersebut ada sebuah feature untuk list semua User dan beserta Order yang sudah mereka lakukan, yang kemungkinan query-nya seperti ini, jika menggunakan ORM :
Contoh Menggunakan Laravel (PHP)
<?php
class UserController extends Controller
{
public function index()
{
$users = User::all(); // 1 query
$result = $users->map(function ($user) {
return [
'id' => $user->id,
'name' => $user->name,
'orders' => $user->orders, // query per user!
];
});
return response()->json($result);
}
}
Contoh Menggunakan Go dengan orm GORM
func (c *UserController) Index(ctx *gin.Context) {
var users []User
c.DB.Find(&users) // 1 query
var result []gin.H
for _, user := range users {
var orders []Order
c.DB.Where("user_id = ?", user.ID).Find(&orders) // query per user!
result = append(result, gin.H{
"id": user.ID,
"name": user.Name,
"orders": orders,
})
}
ctx.JSON(200, result)
}
Pada potongan dua kode diatas adalah contoh N+1 Query, pertama ada satu query untuk mengambil semua user (1 query), katakanlah hasil query tersebut mendapatkan 100 user, kemudian dari 100 user tersebut dilakukan looping untuk mengambil order per user, jika ini dilakukan maka total jumlah query yang diexecute menjadi 101 (N + 1, dimana nilai N adalah 100) query, hal ini yang disebut N+1 query gambaran dengan raw SQL :
SELECT * FROM users -- result 100
-- looping sampai 100
SELECT * FROM orders WHERE user_id = ?;
Kenapa N+1 Bisa Menjadi Masalah?
N+1 bisa menjadi masalah serius yaitu bisa membuat app/website lambat, ada beberapa alasannya:
- Query yang di-execute dalam satu proses terlalu banyak, ini akan menjadi beban ke database server
- Response time menjadi lama dikarenakan apps/web menunggu banyak query satu per satu hingga selesai
- Semakin banyak data yang ada di database efeknya akan semakin parah (misal ada 1000 users bisa akan menjadi 1001 query)
Cara Fix N+1 Problem
Menggunakan Join dalam satu query
Di banding menggunakan query yang terpisah untuk mengambil order untuk setiap user, kita bisa menggunakan JOIN untuk mengambil semua data dalam satu query saja, contoh nya :
SELECT
users.*,
orders.*
FROM users
LEFT JOIN orders ON users.id = orders.user_id;
Cara ini kita hanya meng-execute 1 query saja dibanding 101 query, membuat app/website menjadi lebih cepat
Mengatasi di ORM
Sebenarnya ORMs sudah memiliki feature bawaan untuk mengatasi masalah ini, Contohnya dalam Laravel, kita bisa menggunakan eager loading:
class UserController extends Controller
{
public function index()
{
$users = User::with('orders')->get();
return response()->json($users);
}
}
Dan ini contoh untuk Golang :
func (c *UserController) Index(ctx *gin.Context) {
var users []User
c.DB.Preload("Orders").Find(&users)
ctx.JSON(200, users)
}
Dalam dua contoh diatas ORM memiliki feature bawaan untuk mencegah N+1 dimana kedua kode tersebut nantinya akan meng-execute satu query saja
Conclusion
N+1 query problem adalah salah satu isu yang mudah terlewati karena secara kode terlihat wajar, padahal di balik layar ada ratusan bahkan ribuan query yang dieksekusi tanpa disadari. Masalah ini baru benar-benar terasa dampaknya saat aplikasi sudah production dan volume data mulai besar, response time yang tadinya cepat bisa melambat secara drastis.
Akan tetapi hampir semua ORM modern (Eloquent di Laravel, GORM di Golang, Sequelize, atau Hibernate di Java) sudah menyediakan fitur bawaan untuk mengatasi ini lewat eager loading, entah itu namanya with(), Preload(), atau include. Dengan sedikit perubahan di query awal, jumlah query bisa ditekan drastis dari N+1 menjadi hanya 2 query saja atau 1 query saja.
Jadi, Jika app/website mulai kerasa lambat padahal datanya belum terlalu besar, bisa dicek terlebih dahulu apakah ada pola N+1 yang mungkin tersembunyi di dalam loop atau closure.