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

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

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

المنصة

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

الدعم

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

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

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

حلّ N+1 Query في ORMs: من 200ms إلى 20ms بأمر واحد

مبتدئ25 أبريل 20265 دقائق قراءة
حلّ N+1 Query في ORMs: من 200ms إلى 20ms بأمر واحد

لو الـ API بتاعك بياخد 218ms في endpoint بسيط بيرجّع قائمة بـ 50 منتج، المشكلة غالبًا مش في السيرفر ولا الـ DB. المشكلة اسمها N+1 query problem، والحل أمر واحد بيوفّر 90% من الوقت.

N+1 Query Problem: المشكلة الصامتة في كل ORM

رفوف خوادم قاعدة بيانات تمثل مشكلة كثرة الاستعلامات N+1 في ORM

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

لما تجيب 50 منتج من قاعدة البيانات، وكل منتج يطلب صنفه (category) في query منفصل، أنت بتعمل 51 query بدل query واحد. ده اسمه N+1: استعلام أساسي + N استعلام فرعي.

الـ ORMs (Sequelize، Prisma، Django ORM، ActiveRecord) بتخفي ده عنك. الكود بيبان نظيف وبسيط، لكن في الخلفية فيه طوفان queries بيضرب الـ DB ويأكل الـ latency.

مثال للمبتدئين: تخيّل إنك في مكتبة

عندك قائمة بـ 50 كتاب، وعايز تعرف اسم المؤلف لكل واحد. الطريقة الغلط: تروح لأمين المكتبة، تطلب الكتاب الأول، يفتح حاسوبه، يجيبلك اسم المؤلف، ترجع لمكتبك، تطلب الكتاب التاني، وهكذا. بتعمل 50 رحلة ذهاب وإياب.

الطريقة الصح: تقول للأمين "هاتلي الـ 50 كتاب بأسامي مؤلفينهم مرة واحدة". رحلة واحدة، نتيجة كاملة.

ده بالظبط الفرق بين N+1 query وeager loading. كل round trip للـ DB تكلفته شبكة + parsing + execution، فلما تعملها 51 مرة بدل مرة، الفرق بيوصل لأكتر من ضعف ونص.

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

N+1 query problem هو نمط استعلام ينتج عن استعلام أساسي يُرجع N سجل، يتبعه تنفيذ N استعلام إضافي لجلب بيانات مرتبطة لكل سجل على حدة. النتيجة الكلية N+1 round trip للـ database بدل round trip واحد. كل round trip بيضيف latency شبكي + parsing overhead + execution، حتى لو كل query منهم بسيط وسريع منفردًا.

مثال كود حقيقي بالأرقام

ده كود Sequelize بيعاني من N+1 بشكل كلاسيكي:

JavaScript
// 51 query: 1 للمنتجات + 50 للأصناف
const products = await Product.findAll({ limit: 50 });
for (const p of products) {
  const category = await p.getCategory(); // كل iteration = query جديد
  console.log(p.name, category.name);
}

الـ profiling على PostgreSQL محلي مع جدولين فيهم index على foreign key:

  • عدد queries: 51
  • زمن إجمالي: 218ms
  • زمن round trips الشبكي: 165ms
  • P95 على staging مع latency 2ms للـ DB: 410ms

الحل: eager loading عبر include بيخلي الـ ORM يبني JOIN واحد:

JavaScript
// query واحد بـ JOIN
const products = await Product.findAll({
  limit: 50,
  include: [{ model: Category }]
});
for (const p of products) {
  console.log(p.name, p.Category.name);
}

بعد التعديل على نفس البيانات ونفس الجهاز:

  • عدد queries: 1
  • زمن إجمالي: 22ms
  • تحسّن: حوالي 90%
  • الأرقام دي تقديرية لبيئة محلية، الفرق على بيئة prod مع latency أعلى بيكون أكبر.

شاشة طرفية تعرض سجل استعلامات ORM متكررة تكشف نمط N+1

نفس الفكرة في Prisma

JavaScript
// قبل: N+1
const products = await prisma.product.findMany({ take: 50 });
for (const p of products) {
  const cat = await prisma.category.findUnique({ where: { id: p.categoryId } });
}

// بعد: include
const products = await prisma.product.findMany({
  take: 50,
  include: { category: true },
});

في Django ORM الأمر اسمه select_related للـ foreign key وprefetch_related للـ many-to-many. في ActiveRecord الأمر includes.

ازاي تكتشف N+1 في كودك

  1. فعّل log الـ queries في الـ ORM. في Sequelize: { logging: console.log } في الـ config.
  2. افتح endpoint واحد، عدّ الـ queries في الـ console.
  3. لو endpoint بسيط دخل أكتر من 5 queries، فيه N+1 على الأرجح.
  4. على PostgreSQL استخدم pg_stat_statements تشوف الـ queries المتكررة بنفس الـ shape بأرقام مختلفة.
  5. أدوات APM زي New Relic أو Datadog بتظهرلك "N+1 detected" مباشرة لو مفعّلة.

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

eager loading مش مجاني. بتكسب round trips أقل، بتخسر:

  • حجم نتيجة أكبر في الـ query الواحد، خصوصًا لو الـ JOIN على one-to-many (الصفوف بتتكرّر جزئيًا).
  • memory أعلى على السيرفر لو الـ relation فيها أعمدة كبيرة (TEXT، JSON).
  • complexity في الـ query plan لو الـ JOIN بقى على 4+ جداول. ممكن الـ planner يختار خطة أسوأ.

الافتراض هنا: N صغير نسبيًا (≤ 1000 سجل في الـ batch)، وعندك indexes صح على الـ foreign keys. لو N = 100K، الحل مش include، الحل pagination + cursor + dataloader pattern.

متى لا تستخدم eager loading

مش كل N+1 لازم يتحل، وفيه حالات الـ include بيضرّ أكتر ما ينفع:

  • الـ relation بتُستخدم في 10% من الـ requests فقط: lazy أوفر للذاكرة وللـ DB.
  • الجدول المرتبط فيه أعمدة TEXT/JSON كبيرة ومش هتعرضها كلها: استخدم attributes أو select لاختيار أعمدة بعينها بدل eager loading كامل.
  • عندك caching layer قوي قدام الـ DB والـ N+1 كله بيضرب الـ cache في O(1).
  • الـ relation polymorphic (سجل ممكن يرتبط بأكتر من جدول): JOIN ساعتها معقد، dataloader أنضف.

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

افتح أبطأ endpoint عندك دلوقتي، فعّل query logging على staging لمدة 5 دقائق، وعدّ كم query بيتنفّذ في request واحد. لو الرقم أكبر من 10، عندك N+1 شغّال. ابدأ بأكبر استعلام لـ list endpoint وطبّق include أو select_related حسب الـ ORM بتاعك، وقيس قبل وبعد. لو الفرق أقل من 30% راجع الـ indexes على الـ foreign key قبل ما تكمل تحسينات تانية.

المصادر

  • Sequelize Eager Loading: sequelize.org/docs/v6/advanced-association-concepts/eager-loading
  • Prisma Query Optimization & N+1: prisma.io/docs/orm/prisma-client/queries/query-optimization-performance
  • Django select_related & prefetch_related: docs.djangoproject.com/en/stable/ref/models/querysets
  • Active Record Eager Loading Guide: guides.rubyonrails.org/active_record_querying
  • PostgreSQL pg_stat_statements: postgresql.org/docs/current/pgstatstatements.html
  • DataLoader pattern (GraphQL Foundation): github.com/graphql/dataloader

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

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

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