أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

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

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

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

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

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

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

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

N+1 Queries للمتوسط: ليه ORM بيعمل 201 query في طلب واحد

📅 ٨ مايو ٢٠٢٦⏱ 6 دقائق قراءة
N+1 Queries للمتوسط: ليه ORM بيعمل 201 query في طلب واحد

مستوى المقال: متوسط — يفترض إنك تعرف ORM (Django/Laravel/Rails) ومفهوم العلاقات ForeignKey و Many-to-Many.

لو الـ endpoint عندك بيرجّع 200 منتج ووقت الاستجابة 4.2 ثانية بدل 80ms، الـ DB مش ضعيفة. الـ ORM بتاعك بيعمل 201 query على PostgreSQL في كل request وأنت مش حاسس. المقال ده هيوريك بالظبط ليه ده بيحصل، إزاي تكتشفه في 5 دقائق، وإزاي تنزّله لـ query واحد بسطر كود.

مشكلة N+1 Queries في الـ ORM: التشخيص والحل العملي

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

كل ORM شائع — Django ORM، Eloquent في Laravel، Active Record في Rails، Sequelize، Prisma — بيخفي الـ SQL عنك علشان حياتك تبقى أسهل. ده كويس وأنت بتكتب الكود، لكن بيخلّيك أعمى لما الكود ده بيتحوّل لـ queries فعلية. أكثر مشكلة شائعة من النوع ده اسمها N+1: query واحد للقائمة الرئيسية ثم N query إضافية لجلب علاقة لكل عنصر فيها.

الافتراض هنا: عندك علاقة One-to-Many أو Many-to-One في الموديل، ومخدتش بالك إن الوصول للعلاقة بيعمل query جديد كل مرة. النتيجة: زمن استجابة بيتضاعف 50× من غير ما الـ DB تتعب أصلًا.

شاشة محرر كود تعرض استعلامات Django ORM متعددة على PostgreSQL أثناء تشخيص مشكلة N+1

مثال للمبتدئ: محل البيتزا

تخيّل إنك في محل بيتزا وطلبت من الجرسون قائمة بـ 200 طلب اتعمل النهاردة. الجرسون رجع بقائمة فيها رقم كل طلب وبس. علشان تعرف كل طلب صاحبه مين، رجعت سألت الجرسون 200 مرة منفصلة: "مين صاحب الطلب رقم 1؟ ومين رقم 2؟" وهكذا.

الجرسون مشي 200 رحلة منفصلة للمطبخ. لو سألته من الأول "هاتلي القائمة كاملة بأسماء الزباين"، كان مشي رحلة واحدة بس. نفس الفكرة بالظبط في N+1: الـ ORM بياخد رحلة شبكة كاملة لكل صف لما يكون ممكن يجيب كله مرة واحدة.

التعريف العلمي الدقيق

مشكلة N+1 توصّفت أكاديميًا في كتاب Bill Karwin "SQL Antipatterns" (Pragmatic Bookshelf، 2010). بتحصل لما تعمل query واحد بيرجّع N صف، وبعدين تعمل query إضافي لجلب علاقة من كل صف من الـ N. الإجمالي: 1 + N استعلامات على الـ DB لطلب واحد بس من المستخدم.

كل query فيه latency شبكة — عادةً 0.5 إلى 2 مللي ثانية داخل نفس الـ datacenter. 200 query × 1ms = 200ms زمن شبكة بس، قبل ما الـ DB تنفّذ أي حاجة فعلًا. ضيف على ده زمن planning و execution لكل query، وزمن serialization للنتيجة، توصل لـ 4 ثواني بسهولة.

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

في Django: ثبّت django-debug-toolbar في بيئة dev. هيوريك عدد الـ queries في كل request في الـ panel جنب الصفحة. أكثر من 10 queries في endpoint بسيط = راية حمراء فورية.

في Laravel: استخدم Laravel Debugbar أو Telescope. في Rails: bullet gem بيرمي تنبيه في الـ log أوّل ما N+1 يحصل.

في إنتاج: فعّل log_min_duration_statement = 0 في PostgreSQL لمدة 5 دقايق على staging واعمل grep للاستعلامات المتشابهة. لو شفت نفس الـ query بـ id مختلف بيتكرر 200 مرة، اكتشفت N+1. أداة pg_stat_statements بترجّع نفس النتيجة على إنتاج بدون أي تأثير على الأداء.

الحل: select_related و prefetch_related

الكود اللي بيولّد المشكلة (Django مثالًا):

Python

# 1 + 200 = 201 queries
products = Product.objects.all()[:200]
for p in products:
    print(p.category.name)  # كل وصول هنا = query جديد

الحل في كلمة واحدة:

Python

# 1 query فقط مع SQL JOIN
products = Product.objects.select_related('category').all()[:200]
for p in products:
    print(p.category.name)  # من غير DB hit

الفرق بين الأداتين مهم جدًا:

  • select_related: لعلاقات ForeignKey و OneToOne. بيعمل SQL JOIN واحد. أنسب لما العلاقة "to-one".
  • prefetch_related: لعلاقات ManyToMany و reverse ForeignKey. بيعمل query إضافي واحد بـ WHERE IN (...) ويربط النتائج في Python. أنسب لما العلاقة "to-many".

في Laravel: Product::with('category')->take(200)->get(). في Rails: Product.includes(:category).limit(200). في Sequelize: { include: [Category] }. الفكرة واحدة في كل ORM، الاسم بس بيختلف.

قياس فعلي من إنتاج

على جدول Product بـ 50,000 صف وعلاقة ForeignKey لـ Category، endpoint بيرجّع 200 منتج بـ pagination على PostgreSQL 16:

  • قبل (N+1): 201 query، 4,180ms متوسط، 22MB استهلاك ذاكرة في Django process.
  • بعد (select_related): 1 query، 78ms متوسط، 18MB ذاكرة.

التحسّن: 53× أسرع. ده مش رقم نظري، ده قياس مباشر من production logs بعد تطبيق الإصلاح على endpoint قائمة المنتجات في e-commerce بـ 8000 طلب/دقيقة.

لوحة قياس زمن الاستجابة قبل وبعد تطبيق select_related تعرض هبوط الزمن من 4180 مللي ثانية إلى 78 مللي ثانية

الـ trade-offs اللي محدش بيحكي عنها

select_related بـ JOIN بيرجّع كل أعمدة الجدولين في كل صف. لو المنتج فيه 30 عمود والتصنيف فيه 15، كل صف فيه 45 خانة بيانات × 200 منتج = 9000 خانة بتعدي على الشبكة. لو الـ Category فيه عمود JSON كبير بيخزّن مثلًا تفاصيل localization بميجا، ممكن JOIN يبقى أبطأ من 2 query منفصلة.

الحل: ضيف only() أو defer() علشان تجيب الأعمدة المطلوبة فقط:

Python

Product.objects.select_related('category').only(
    'id', 'name', 'price', 'category__name'
)[:200]

المكسب من select_related صريح: 53× سرعة. التكلفة: 5 دقايق فهم العلاقات + سطر إضافي. بتكسب: latency أقل، استهلاك CPU أقل على DB. بتخسر: لو نسيت only هتنقل بيانات أكتر من اللازم على الشبكة.

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

1. لو الـ endpoint بيرجّع صف واحد فقط، N+1 مش موجود من الأساس. مفيش داعي لـ select_related.

2. لو العلاقة nested 4 مستويات وكل مستوى فيه جداول كبيرة، الـ JOIN ممكن يولّد cartesian explosion ملايين الصفوف. في الحالة دي استخدم prefetch_related مع Prefetch() object للتحكم الدقيق.

3. لو بتستخدم async ORM (مثل Tortoise أو SQLAlchemy 2.0 async)، الـ syntax مختلف ولازم تتأكد من selectinload بدل joinedload في غالب الحالات.

4. لو في cache layer قبل الـ DB (Redis مثلًا) بياكل 95% من الـ requests، حل N+1 ممكن يبقى أولوية أقل من تحسين hit rate الـ cache نفسه.

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

افتح أبطأ endpoint في الـ API بتاعك دلوقتي، فعّل django-debug-toolbar أو ما يعادلها في الـ stack بتاعك، وعد الـ queries في request واحد. لو لقيت أكتر من 5 queries على list endpoint بسيط، عندك N+1. ضيف select_related أو prefetch_related حسب نوع العلاقة، وأعد القياس فورًا. أبسط 5 دقايق هتدّيك أكبر مكسب أداء في الشهر ده.

المصادر

  • Django Documentation — QuerySet API: select_related & prefetch_related (docs.djangoproject.com).
  • PostgreSQL Documentation — pg_stat_statements module (postgresql.org/docs/current/pgstatstatements.html).
  • Bill Karwin — SQL Antipatterns (Pragmatic Bookshelf، 2010)، الفصل الخاص بـ N+1.
  • Laravel Documentation — Eager Loading (laravel.com/docs/eloquent-relationships).
  • Rails Guides — Active Record Query Interface (guides.rubyonrails.org/active_record_querying.html).
  • Google SRE Book — Chapter 6, Monitoring Distributed Systems (sre.google/sre-book).

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

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

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