أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالمناهج والباقات
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • المناهج والباقات
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالمناهجالمدونةالدخول
البرمجة بالعربي

مشكلة N+1 Query للمتوسط: ليه صفحة بتعرض 50 منتج بتضرب الداتابيز 51 مرة

متوسط٢٢ يونيو ٢٠٢٦4 دقائق قراءة
مشكلة N+1 Query للمتوسط: ليه صفحة بتعرض 50 منتج بتضرب الداتابيز 51 مرة

مشكلة 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 لحظتها. الكود بيبان نضيف، بس كل سطر فيه بيخبّي رحلة شبكة كاملة.

JavaScript
// الكود اللي بيسبب 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 واحد

  1. قول للـ ORM يجيب العلاقة مع الأصل في نفس الـ query عن طريق JOIN.
  2. في Sequelize ده اسمه include. في Django ORM اسمه select_related / prefetch_related. في Prisma اسمه include برضه.
  3. النتيجة: query واحد بدل 51.
JavaScript
// الحل: 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 هنا
}

الـ SQL اللي بيتنفّذ ورا الكود ده تقريبًا:

SQL
SELECT p.id, p.name, c.name AS category_name
FROM products p
JOIN categories c ON c.id = p.category_id
LIMIT 50;

الأرقام: قبل وبعد

على جدول 50 منتج و round-trip للداتابيز حوالي 6ms:

  • قبل: 51 query مضروبة في حوالي 6ms تساوي حوالي 306ms انتظار، والاستجابة الكلية حوالي 340ms.
  • بعد: query JOIN واحد حوالي 8ms، والاستجابة الكلية حوالي 12ms.
  • التحسّن: حوالي 28 ضعف أقل. (الأرقام تقديرية على إعداد PostgreSQL محلي، وبتكبر أكتر لو الداتابيز على شبكة بعيدة.)

ازاي تكتشفها بنفسك: شغّل logging للـ SQL. في Sequelize حط logging: console.log في إعداد الاتصال. لو لقيت نفس الـ SELECT بيتكرر بأرقام id مختلفة في request واحد، ده N+1 بالظبط.

الـ trade-offs

الـ JOIN مش مجاني. بتكسب رحلات شبكة أقل، بتخسر إن حجم الصف الراجع بيكبر: لو المنتج عنده 5 علاقات وعملتلهم كلهم JOIN في نفس الاستعلام، الداتابيز بترجّع بيانات مكررة وبيظهر اسمه Cartesian explosion. الافتراض هنا إن عدد العلاقات قليل (واحدة لتلاتة). لو أكتر من كده، استخدم prefetch (استعلام منفصل واحد لكل علاقة) بدل JOIN واحد عملاق: يبقى عندك 1 زائد عدد العلاقات بدل N+1.

متى لا تستخدم هذه الطريقة

لو محتاج علاقة واحدة لمنتج واحد بس (مش قائمة)، الـ N+1 مش موجود من الأساس، فمتعقّدش الكود بـ include. وكمان لو القائمة فيها عنصرين، الفرق بين 2 query و 3 query مش هيتحس. المشكلة بتظهر مع القوائم الكبيرة والـ endpoints اللي بتترندر كتير. ركّز على أبطأ صفحة قائمة عندك الأول، مش على كل query في المشروع.

الخطوة التالية

افتح أبطأ endpoint بيعرض قائمة عندك، فعّل SQL logging، وعُدّ عدد الـ queries في request واحد. لو لقيت الرقم بيكبر كل ما الصفوف تزيد، ضيف include أو select_related وقيس تاني. لو نزل لرقم ثابت صغير، يبقى قتلت الـ N+1.

المصادر

  • توثيق Sequelize الرسمي — Eager Loading: https://sequelize.org/docs/v6/advanced-association-concepts/eager-loading/
  • توثيق Django الرسمي — select_related و prefetch_related: https://docs.djangoproject.com/en/5.0/ref/models/querysets/#select-related
  • توثيق Prisma — Relation queries (include): https://www.prisma.io/docs/orm/prisma-client/queries/relation-queries
  • Martin Fowler — تكلفة الـ round-trips في الأنظمة الموزّعة: https://martinfowler.com/articles/distributed-objects-microservices.html

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة