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

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

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

المنصة

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

الدعم

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

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

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

مشكلة N+1: ليه صفحة واحدة بتضرب قاعدة بياناتك 100 استعلام

متوسط22 يوليو 20264 دقائق قراءة
مشكلة N+1: ليه صفحة واحدة بتضرب قاعدة بياناتك 100 استعلام

المستوى: متوسط. المقال ده موجّه لمطوّر بيتعامل مع ORM (زي Django أو Laravel أو Prisma) وشايف بطء غريب في صفحات بسيطة. لو لسه مبتدئ خالص، فيه مثال المطعم تحت هيوصّلك الفكرة قبل الجزء التقني.

لو صفحة بتعرض 20 صف بتاخد قرب الثانية والسيرفر مفيهوش أي حمل، المشكلة غالبًا مش في السيرفر ولا في اتصال الشبكة. المشكلة إن الكود بيبعت للقاعدة استعلام لكل صف بدل استعلام واحد. ده اسمه مشكلة N+1، ولو مسكتها هتنزّل زمن الصفحة من ثانية لأقل من 50 ملّي ثانية بسطر واحد.

مشكلة N+1 في الاستعلامات: الشرح الكامل

المشكلة باختصار

الـ ORM بيخلّي جلب البيانات يبان سهل، لدرجة إنك مبتشوفش الاستعلامات اللي بتتبعت فعلًا. بتكتب سطر بايثوني عادي، وهو ورا ضهرك بيضرب القاعدة عشرات المرات. النتيجة: صفحة تقيلة، قاعدة بيانات مضغوطة بلا داعي، وفاتورة سيرفر أعلى.

الفكرة بمثال بسيط قبل التقنية

تخيّل نادل في مطعم. جالك 10 زباين على ترابيزة. بدل ما ياخد الطلبات كلها مرة واحدة ويروح المطبخ رحلة واحدة، بيروح ياخد طلب زبون، يرجع المطبخ، يرجع تاني ياخد طلب التاني، وهكذا. عشر رحلات للمطبخ عشان حاجة كان ممكن تتعمل في رحلة أو اتنين.

الرحلة للمطبخ هنا هي الاستعلام للقاعدة. الكود اللي فيه N+1 بيعمل بالظبط كده: رحلة واحدة يجيب فيها قايمة العناصر (ده الـ 1)، وبعدين رحلة منفصلة لكل عنصر عشان يجيب تفاصيله المرتبطة (دول الـ N).

الشرح العلمي الدقيق

N+1 معناها إن عملية واحدة منطقيًا اتحوّلت لعدد استعلامات = 1 (للقايمة الأصلية) + N (استعلام لكل صف من صفوف القايمة). لو جبت 100 مقال وكل مقال عايز اسم كاتبه، الـ ORM بيعمل استعلام واحد للمقالات، وبعدين 100 استعلام منفصل لجدول الكُتّاب. الإجمالي 101 استعلام بدل 2.

كل استعلام منهم بياخد وقت شبكة ذهاب وعودة (round trip) للقاعدة، حتى لو الاستعلام نفسه أسرع من ملّي ثانية. لو الـ round trip الواحد 1 ملّي ثانية، فـ 100 استعلام زيادة = 100 ملّي ثانية ضايعة في الانتظار مش في الحساب.

شاشة تعرض سطور استعلامات SQL متكررة في سجل التنفيذ داخل الطرفية

الكود اللي بيسبّب المشكلة والحل

ده مثال بـ Django ORM. الكود ده بيبان بريء، لكنه بيفجّر N+1:

Python
# الكود الغلط: استعلام لكل مقال عشان يجيب الكاتب
articles = Article.objects.all()          # استعلام 1: كل المقالات
for article in articles:
    print(article.author.name)            # استعلام إضافي لكل مقال (N)
# الإجمالي لـ 100 مقال = 101 استعلام

الحل إنك تقول للـ ORM يجيب الكُتّاب المرتبطين في نفس الوقت باستخدام JOIN واحد:

Python
# الكود الصح: select_related بيعمل JOIN واحد
articles = Article.objects.select_related("author").all()  # استعلام 1 بـ JOIN
for article in articles:
    print(article.author.name)            # صفر استعلامات إضافية
# الإجمالي = استعلام واحد

لو العلاقة many-to-many أو reverse (زي مقال وله تعليقات كتير)، استخدم prefetch_related بدل select_related، وده بيعمل استعلامين إجمالًا (واحد للمقالات وواحد لكل التعليقات دفعة واحدة بـ IN).

Python
# للعلاقات المتعددة: استعلامين ثابتين مهما كان عدد الصفوف
articles = Article.objects.prefetch_related("comments").all()

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

على جدول فيه 100 مقول وقاعدة PostgreSQL على نفس الشبكة المحلية، القياس التقريبي:

  • قبل: 101 استعلام، زمن الصفحة حوالي 320 ملّي ثانية (غالبها انتظار round trips).
  • بعد select_related: استعلام واحد، زمن الصفحة حوالي 18 ملّي ثانية.

ده تحسّن قرب 17 ضعف، من غير ما تلمس السيرفر ولا تزوّد ذاكرة. الأرقام دي تقديرية وبتفرق حسب زمن الشبكة وحجم الصفوف، بس النسبة بتفضل في نفس الحجم.

الـ trade-offs وما يجب الانتباه له

الحل مش مجاني على طول. الـ JOIN في select_related بيجيب أعمدة الجدولين في نفس الصف، فلو بتعمل JOIN على 5 علاقات كبيرة، حجم البيانات المنقولة بيكبر وممكن يتكرّر. المكسب: عدد استعلامات ثابت. التكلفة: صف أعرض وذاكرة أعلى شوية لكل نتيجة.

الافتراض هنا إنك بتحتاج البيانات المرتبطة فعلًا لكل صف. لو محتاجها لصف واحد بس من المية، الأفضل تجيبها لوحدها بدل ما تحمّل الاستعلام كله بـ JOIN.

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

لو القايمة صغيرة (5 أو 10 صفوف) والعلاقة مش بتتقري في الحلقة، مش لازم تعقّد الكود بـ prefetch. كمان لو الجدول المرتبط ضخم جدًا وانت محتاج عمود واحد بس منه، أحيانًا استعلام واحد بـ values() أو annotate() بيطلع أنضف من JOIN كامل. المشكلة تستاهل الحل لما N يبقى كبير ومتغيّر مع نمو البيانات.

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

شغّل عدّاد الاستعلامات على أبطأ صفحة عندك دلوقتي. في Django استخدم django-debug-toolbar أو connection.queries، وفي Laravel استخدم DB::listen أو Telescope. لو لقيت رقم الاستعلامات بيزيد كل ما البيانات تزيد، ده N+1؛ حطّ select_related أو prefetch_related وقيس الفرق قبل وبعد.

المصادر

  • توثيق Django الرسمي — select_related وprefetch_related: docs.djangoproject.com/en/stable/ref/models/querysets/
  • توثيق Django حول تقليل عدد استعلامات قاعدة البيانات: docs.djangoproject.com/en/stable/topics/db/optimization/
  • توثيق Laravel Eloquent — Eager Loading: laravel.com/docs/eloquent-relationships#eager-loading
  • توثيق Prisma — حل مشكلة N+1 بـ relation loading: prisma.io/docs/orm/prisma-client/queries/relation-queries

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

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

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