هتكسب إيه من المقال ده: هتتعلم تكتشف مشكلة N+1 Query في صفحتك في أقل من دقيقة، وتنزّل زمن تحميل صفحة فيها 100 منتج من 1.4 ثانية إلى 47 مللي ثانية بتعديل سطرين في الـ ORM.
مستوى المقال: متوسط. الافتراض إنك تعرف SQL أساسي وبتستخدم ORM زي Sequelize أو Prisma أو Django ORM. لو لسه مبتدئ تمامًا، في فقرة "الفكرة بمثال بسيط" تحت هتوضّحلك الموضوع قبل الجزء التقني.
مشكلة N+1 Query: ليه صفحة المنتجات بتعمل 101 استعلام بدل واحد
المشكلة باختصار
عندك صفحة بتعرض 100 منتج، وجنب كل منتج اسم القسم بتاعه. الكود بيجيب المنتجات في استعلام واحد، وبعدين بيلف على كل منتج علشان يجيب القسم بتاعه. النتيجة استعلام للمنتجات زائد 100 استعلام للأقسام، يعني 101 استعلام في كل فتح للصفحة. ده اسمه N+1: استعلام أصلي واحد، زائد استعلام لكل صف من الـ N صف.
المشكلة مش في إن السيرفر بطيء. كل استعلام لوحده سريع. اللي بيقتل الأداء هو عدد مرات الذهاب والإياب بين التطبيق وقاعدة البيانات. لو كل round-trip بياخد 14 مللي ثانية على الشبكة، 101 استعلام معناها 1.4 ثانية ضايعة في الانتظار بس.
الفكرة بمثال بسيط قبل التقني
تخيّل إنك في مكتبة وعايز قائمة بـ 100 كتاب مع اسم مؤلف كل كتاب. الطريقة الغلط: تطلب من أمين المكتبة قائمة الكتب الأول، وبعدين ترجعله 100 مرة تسأله "ومين مؤلف الكتاب ده؟" كتاب ورا كتاب. ده 101 رحلة لمكتبه. الطريقة الصح: تقوله من الأول "هاتلي الكتب وكل واحد جنبه اسم مؤلفه" في رحلة واحدة بكل المعلومات.
القاعدة بتشتغل بنفس المنطق بالظبط: كل رحلة لقاعدة البيانات ليها تكلفة ثابتة، والمكسب الحقيقي إنك تقلّل عدد الرحلات، مش حجم البيانات في الرحلة الواحدة.
علميًا: N+1 query هو نمط وصول للبيانات بيصدر استعلامًا منفصلًا لكل عنصر في مجموعة نتائج، بدل ما يجمع البيانات المرتبطة في استعلام واحد عبر JOIN أو في استعلام تجميعي بشرط IN. التكلفة بتيجي من الـ latency لكل round-trip، مش من زمن تنفيذ الاستعلام نفسه.
ليه الـ ORM بيوقعك في الفخ ده
الـ ORM بيخفي الـ SQL عنك، وده مريح لكنه خطير. لما تكتب وصول لعلاقة زي product.category جوه حلقة، الـ ORM بيترجم كل وصول (lazy loading) لاستعلام جديد ورا ضهرك. انت شايف كود نضيف، لكن اللي بيحصل فعلاً 101 استعلام.
// الطريقة الغلط اللي بتعمل N+1
const products = await Product.findAll({ limit: 100 });
for (const p of products) {
// كل سطر هنا بيطلق استعلام SELECT منفصل على جدول categories
const category = await p.getCategory();
console.log(p.name, category.name);
}
// المجموع: 1 + 100 = 101 استعلام