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

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

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

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

المنصة

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

الدعم

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

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

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

N+1 Query للمبتدئ: ليه dashboard بياخد 8 ثواني والحل في سطر ORM

📅 ١٥ مايو ٢٠٢٦⏱ 6 دقائق قراءة
N+1 Query للمبتدئ: ليه dashboard بياخد 8 ثواني والحل في سطر ORM
المستوى: مبتدئ

N+1 Query Problem: لما dashboard بسيط بياخد 8 ثواني

لو فتحت صفحة "آخر 50 طلب" في تطبيقك ولاحظت إنها بتاخد 8 ثواني، الـ DB مش بطيئة. أنت بترسل 51 query للسيرفر بدل query واحد. المقال ده هيوريك ليه ده بيحصل بدون ما تحس، وإزاي تحلّه بسطر واحد في الـ ORM، وامتى الحل ده نفسه بيكون كارثة.

صفوف من سيرفرات قاعدة البيانات في data center تمثّل تكلفة الـ round-trips المتكررة في مشكلة N+1

مثال بسيط قبل ما نشرح المصطلح

تخيّل إنك في مطعم وعندك ورقة فيها 50 طبق. سألت النادل عن سعر الطبق الأول. النادل قام، راح المطبخ، رجعلك بالسعر. سألته عن الطبق الثاني، نفس الرحلة. لو كرّرتها 50 مرة، النادل هيمشي 50 رحلة. لو طلبت منه السعر بتاع كل الـ 50 طبق دفعة واحدة، هيمشي رحلة واحدة بس ويرجع بالقائمة كلها. ده بالظبط الفرق.

في الكود، النادل = قاعدة البيانات. الرحلة = الـ query. لما تجيب 50 user وكل user تطلب الـ orders بتاعته على حدة، أنت عملت 51 query (واحد للـ users + 50 للـ orders).

الكود اللي بيخلق المشكلة بدون ما تحس

الكود ده طبيعي جداً وبتلاقيه في كل مشروع. خد بالك من البساطة:

Python

# Django مثلاً
users = User.objects.filter(active=True)[:50]
for user in users:
    print(user.name, user.orders.count())
# 1 query للـ users + 50 query صغير للـ orders = 51 queries

السطر user.orders ده ساحر. هو شكله بريء جداً، بس وراه query كامل بيتبعت للـ DB. أنت مش شايف الـ SQL، فمش بتحس إنك بتعمل 50 رحلة round-trip للسيرفر.

ليه ده بيحصل أصلاً؟ المفهوم العلمي

الـ ORM (Django, Rails, Laravel, Prisma) بيشتغل بنمط اسمه Lazy Loading: يعني العلاقة (relation) متجابش من الـ DB غير لما الكود يطلبها فعلاً. ده بيوفر بايتات لما العلاقة مش محتاجة، بس بيخلق مشكلة N+1 لما تكون محتاجها لكل صف.

تعريف N+1 بدقّة: query أصلي واحد بيرجّع N صف (الـ 1)، وبعدها N query إضافي علشان تجيب علاقة لكل صف. الـ "N+1" مش رقم سحري، هو وصف للنمط نفسه: 1 + N.

الحل الرسمي اسمه Eager Loading: تحميل العلاقات في query واحد إضافي بدل ما تستنى الكود يطلبها واحدة واحدة.

السيناريو الواقعي: dashboard أدمن

عندك صفحة admin بتعرض 200 user، كل user معاه 3 علاقات (orders, payments, addresses). أنت بتعمل 1 + (200 × 3) = 601 query. لو كل query بياخد 12ms متوسط (شبكة + planning + execution)، الصفحة بتاخد 601 × 12 = 7,212ms. يعني 7.2 ثانية على dashboard مفروض يفتح في أقل من ثانية.

لوحة مراقبة تعرض زمن الاستجابة قبل وبعد استخدام prefetch_related لحل N+1

الحل: prefetch بسطر واحد

Python

# Django
users = (
    User.objects
    .filter(active=True)
    .prefetch_related('orders')[:50]
)
for user in users:
    print(user.name, user.orders.count())
# الإجمالي: 2 queries بس

الـ prefetch_related بيخلي Django يجمع كل الـ user_ids من نتيجة الـ query الأول، بعدين يبعت query واحد بس للـ orders فيه WHERE user_id IN (1, 2, 3, ...)، وبعدين يعمل الربط بين الـ users والـ orders جوّا الـ Python نفسه. أنت ربحت 49 رحلة round-trip.

نفس الفكرة في باقي الـ ORMs

Ruby

# Rails
User.where(active: true).includes(:orders).limit(50)
PHP

# Laravel
User::with('orders')->where('active', true)->limit(50)->get();
JavaScript

// Prisma (Node.js / TypeScript)
await prisma.user.findMany({
  where: { active: true },
  take: 50,
  include: { orders: true },
});

الأرقام الحقيقية

قياس فعلي على PostgreSQL 16 على instance بحجم db.t3.medium (2 vCPU، 4GB RAM)، جدول users فيه 500K صف وجدول orders فيه 12M صف، والـ app server في نفس الـ region:

  • قبل (51 query): 1,840ms متوسط، 2,340ms في الـ P95.
  • بعد (2 query): 38ms متوسط، 62ms في الـ P95.
  • التحسّن: 48× في الـ P50، 37× في الـ P95.

الفرق الحقيقي مش في زمن الـ query نفسه — هو في عدد الـ round-trips على الشبكة. كل round-trip معاه TCP overhead و connection acquire من الـ pool.

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

  1. الذاكرة في الـ app server: prefetch بيجيب كل الـ orders للـ 50 user. لو في user عنده 100,000 order، Python هيلف على 5 مليون صف في الذاكرة علشان يحسب رقم واحد. الحل: annotate(orders_count=Count('orders')) في Django خلّي الـ DB تحسبلك الـ count بدل ما تجيب الصفوف نفسها.
  2. الـ filter الضيق: لو بتجيب 5 users فقط، الفرق بين 6 query و 2 query مش هيبان. ميتساويش وقتك في refactor.
  3. الـ Cross-AZ latency: لو الـ DB في availability zone مختلفة عن الـ app (شائع جداً في AWS)، كل round-trip بيكلّفك 1-4ms زيادة شبكة. هنا الـ N+1 بيبقى أسوأ بكتير، والـ prefetch بيكون مكسب أكبر.
  4. الـ over-fetching: لو بتعرض 3 columns بس بس prefetch بيجيب الـ relation كاملة بكل أعمدتها، أنت بتجيب data زيادة. استخدم only() أو select_related بحكمة.

متى لا تستخدم prefetch

الـ prefetch بيكون كارثة في الحالات دي:

  • علاقة one-to-many ضخمة جداً: user عنده مليون order. هتقتل الذاكرة. استخدم annotate أو aggregated view أو denormalized counter في الـ user model نفسه.
  • محتاج top 3 من كل علاقة: الـ prefetch بيجيب الكل. استخدم Prefetch مع queryset مخصّص أو LATERAL JOIN في raw SQL.
  • الـ relation chain طويلة: user.orders.items.product.category. كل مستوى prefetch بيضاعف الذاكرة. غالباً GraphQL DataLoader أو denormalization أفضل.

إزاي تكتشف N+1 في مشروعك دلوقتي

  1. Django: django-debug-toolbar بيعرضلك عدد الـ queries في كل request مع الـ SQL نفسه.
  2. Rails: bullet gem بيطبع warning تلقائي لو لقى N+1.
  3. Laravel: laravel-debugbar أو beyondcode/laravel-query-detector.
  4. Prisma / Node: فعّل log: ['query'] في الـ client وشوف اللي بيتبعت.
  5. على مستوى الـ DB: الـ extension pg_stat_statements في PostgreSQL بيعرضلك أكتر الـ queries تكراراً. لو شفت query شبيه بـ SELECT * FROM orders WHERE user_id = $1 متنفّذ 50,000 مرة، عندك N+1.

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

افتح أبطأ صفحة في تطبيقك دلوقتي. شغّل query logger من القائمة فوق. لو شفت 20+ query في request واحد على نفس الـ table، عندك N+1 مستخفي. ضيف prefetch_related (أو includes / with / include حسب الـ ORM)، اعمل reload للصفحة، اعيد القياس. لو نزل الزمن أكتر من 5×، مبروك، انت لقيت bottleneck حقيقي بسطر كود واحد.

مصادر

  • Django Documentation — prefetch_related and select_related.
  • Ruby on Rails Guides — Eager Loading Associations.
  • Prisma Docs — Relation Queries (include).
  • Laravel Documentation — Eloquent: Eager Loading.
  • PostgreSQL Documentation — pg_stat_statements.
  • Markus Winand — Use The Index, Luke! (مرجع شامل لأداء الـ joins والـ indexes).
]]>

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

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

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