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

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

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

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

المنصة

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

الدعم

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

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

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

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

متوسط١٧ يونيو ٢٠٢٦5 دقائق قراءة
مشكلة N+1 Query للمتوسط: ليه صفحة المنتجات بتعمل 101 استعلام بدل واحد

هتكسب إيه من المقال ده: هتتعلم تكتشف مشكلة 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، مش من زمن تنفيذ الاستعلام نفسه.

رسم بياني خطي يقارن زمن الاستجابة بين نمط N+1 الذي ينمو خطياً ونمط JOIN الذي يظل شبه ثابت مع زيادة عدد الصفوف على PostgreSQL

ليه الـ ORM بيوقعك في الفخ ده

الـ ORM بيخفي الـ SQL عنك، وده مريح لكنه خطير. لما تكتب وصول لعلاقة زي product.category جوه حلقة، الـ ORM بيترجم كل وصول (lazy loading) لاستعلام جديد ورا ضهرك. انت شايف كود نضيف، لكن اللي بيحصل فعلاً 101 استعلام.

JavaScript
// الطريقة الغلط اللي بتعمل 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 استعلام

الحل: اجمع البيانات المرتبطة في استعلام واحد

  1. فعّل eager loading في الـ ORM علشان يجيب العلاقة مع الاستعلام الأصلي.
  2. اعرض البيانات زي ما هي، من غير أي وصول إضافي للعلاقة جوه الحلقة.
  3. قِس عدد الاستعلامات قبل وبعد علشان تتأكد إن الإصلاح اشتغل.
JavaScript
// الطريقة الصح في Sequelize: استعلام واحد (أو اتنين) بدل 101
const products = await Product.findAll({
  limit: 100,
  include: [{ model: Category, attributes: ['name'] }], // eager loading
});
for (const p of products) {
  console.log(p.name, p.Category.name); // مفيش استعلام جديد هنا
}

نفس الفكرة موجودة في كل ORM:

  • Prisma: prisma.product.findMany({ include: { category: true } })
  • Django: Product.objects.select_related('category') للعلاقات واحد-لواحد، وprefetch_related للعلاقات واحد-لمتعدد.
  • Rails: Product.includes(:category)

ولو بتكتب 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 100;
-- استعلام واحد يرجّع المنتج والقسم مع بعض

إزاي تكتشفها في دقيقة

شغّل تسجيل الاستعلامات: في Sequelize حط logging: console.log، في Prisma استخدم log: ['query']، وفي Django ركّب Django Debug Toolbar. افتح الصفحة وعُد الاستعلامات. لو الرقم بيكبر كل ما يزيد عدد الصفوف، ده N+1 بالظبط. وعلى مستوى القاعدة، EXPLAIN ANALYZE بيوريك خطة التنفيذ وزمن كل استعلام.

النتيجة بالأرقام

على PostgreSQL 16 محليًا، صفحة بـ 100 منتج: النسخة بـ N+1 أخدت حوالي 1,420 مللي ثانية موزّعة على 101 استعلام. النسخة بـ JOIN أخدت 47 مللي ثانية في استعلامين بس. ده تحسّن حوالي 30 ضعف، وتقليل round-trips بنسبة تقارب 98%. الأرقام تقديرية ومقاسة على بيئة محلية؛ على شبكة إنتاج بـ latency أعلى الفرق بيكبر أكتر.

رسم بياني شريطي أفقي يقارن زمن تحميل صفحة 100 منتج: 1420 مللي ثانية مع N+1 مقابل 47 مللي ثانية مع JOIN

والأهم إن المشكلة بتسوء خطيًا. مع 1000 منتج، N+1 بيوصل لآلاف الاستعلامات وزمن بيتعدّى 14 ثانية، بينما JOIN بيفضل في حدود مئات المللي ثانية. الرسم الأول فوق بيوضّح الفرق ده وانت بتزوّد عدد الصفوف.

الـ trade-offs اللي لازم تعرفها

الحل مش مجاني. لما تعمل JOIN على علاقة واحد-لمتعدد (منتج له صور كتير مثلًا)، الـ JOIN بيكرّر بيانات المنتج لكل صورة، فبتنقل داتا زيادة على الشبكة. هنا الأفضل تستخدم استراتيجية الاستعلامين: استعلام للمنتجات، واستعلام تاني للصور بشرط WHERE product_id IN (...)، وده اللي Prisma وSequelize بيعملوه افتراضيًا في حالات كتير. بتكسب: تقليل round-trips بشكل حاسم. بتخسر: احتمال over-fetching في الـ JOIN، أو تعقيد بسيط في إدارة الاستعلامين.

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

لو الصفحة بتعرض عدد صغير من الصفوف (5 أو 10)، الفرق غير محسوس وممكن تسيب الكود أبسط. لو مش هتعرض الحقل المرتبط أصلًا، متجيبهوش، لأن eager loading لبيانات مش بتستخدمها هدر صريح. ولو العلاقة متعددة المستويات وعميقة، JOIN واحد ضخم ممكن يبقى أبطأ من كذا استعلام مركّز بشرط IN؛ قِس قبل ما تقرر، متفترضش.

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

دلوقتي حالًا: افتح أبطأ صفحة في تطبيقك، فعّل تسجيل استعلامات الـ ORM، وعُد عدد الاستعلامات وانت بتزوّد عدد العناصر المعروضة. لو الرقم بيكبر مع الصفوف، ضيف include أو select_related للعلاقة وأعد القياس. لو نزل عدد الاستعلامات من العشرات لاتنين، يبقى كنت واقع في N+1 وحليتها بالفعل.

المصادر

  • Sequelize — Eager Loading: sequelize.org
  • Prisma — Relation queries (include / nested reads): prisma.io
  • Django — select_related & prefetch_related: docs.djangoproject.com
  • Ruby on Rails Guides — Eager Loading Associations (N+1): guides.rubyonrails.org
  • PostgreSQL — Using EXPLAIN: postgresql.org

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

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

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