مشكلة N+1 Query: ليه صفحة بتعرض 50 منتج بتضرب الداتابيز 51 مرة
المستوى: متوسط — الكلام ده مناسب لو بتستخدم ORM زي Sequelize أو Django ORM أو Prisma، وعندك صفحة بتعرض قائمة عناصر مرتبطة بجداول تانية.
صفحة المنتجات بتاعتك بطيئة مش لأن السيرفر ضعيف. السبب إنها بتفتح 51 رحلة للداتابيز بدل اتنين. المقال ده بيوريك ازاي تكتشف المشكلة دي وتحلها في خطوة واحدة، وتنزّل زمن الاستجابة من 340ms لـ 12ms.
المشكلة باختصار
عندك جدول products فيه 50 منتج، وكل منتج ليه category في جدول تاني. بتعرض القائمة فبتجيب الـ 50 منتج بـ query واحد. بعد كده علشان تطبع اسم القسم لكل منتج، الكود بيعمل query صغير لكل منتج لوحده. النتيجة: query واحد للمنتجات زائد 50 query للأقسام يساوي 51 رحلة للداتابيز. ده اسمه N+1 Query Problem: الواحد بتاع القائمة، وزياده N استعلام لكل صف.
تشبيه بسيط الأول
تخيّل جرسون أخد طلب 50 شخص على ورقة واحدة. بدل ما يدخل المطبخ مرة واحدة بالـ 50 طلب، بيروح يسأل عن طبق واحد، يرجع، يسأل عن التاني، يرجع، وهكذا 50 مرة. المشي والرجوع نفسه هو اللي بياكل الوقت، مش الطبخ.
علميًا: كل query بياخد زمن شبه ثابت للـ round-trip بين السيرفر والداتابيز، حتى لو رجّع صف واحد. لو الـ round-trip 6ms، يبقى 50 query زيادة يساوي 300ms مهدورة في الانتظار على الشبكة، مش في الحساب الفعلي.
ليه بيحصل: الـ Lazy Loading
الـ ORM بيعمل ده من غير ما تقصد. لما تكتب product.category جوه loop، الـ ORM بيلاقي إن البيانات دي لسه مش متحمّلة، فبيبعت query لحظتها. الكود بيبان نضيف، بس كل سطر فيه بيخبّي رحلة شبكة كاملة.
// الكود اللي بيسبب N+1 (Sequelize)
const products = await Product.findAll({ limit: 50 }); // query رقم 1
for (const p of products) {
// كل لفة بتبعت query جديد للأقسام
const category = await p.getCategory(); // query 2 ... 51
console.log(p.name, category.name);
}الحل: Eager Loading في query واحد
- قول للـ ORM يجيب العلاقة مع الأصل في نفس الـ query عن طريق JOIN.
- في Sequelize ده اسمه include. في Django ORM اسمه select_related / prefetch_related. في Prisma اسمه include برضه.
- النتيجة: query واحد بدل 51.
// الحل: include بيعمل JOIN ويجيب كل حاجة مرة واحدة
const products = await Product.findAll({
limit: 50,
include: [{ model: Category }], // JOIN
}); // query واحد بس
for (const p of products) {
console.log(p.name, p.Category.name); // مفيش أي query هنا
}