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

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

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

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

المنصة

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

الدعم

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

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

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

N+1 Query للمتوسط: ليه dashboard بسيط بيعمل 1,200 استعلام في كل request

📅 ٨ مايو ٢٠٢٦⏱ 5 دقائق قراءة
N+1 Query للمتوسط: ليه dashboard بسيط بيعمل 1,200 استعلام في كل request

المستوى: متوسط — مقال موجّه للمطور اللي بيستخدم ORM زي Prisma أو Sequelize أو Django ORM وبيشوف الـ DB بطيء بدون سبب واضح. لو لسه بتتعلم SQL أساسي، تقدر تقراه عادي بس ركّز في مثال البيتزا قبل ما تدخل على الكود.

لو dashboard المنتجات بتاعك بيرد في 6 ثواني وفاتورة DB قفزت 3x من غير ما traffic يزيد، الاحتمال الأكبر إن الـ ORM بيعمل 1,247 استعلام على كل request بدل استعلام واحد. المشكلة دي اسمها N+1 Query، وحلّها 3 سطور كود بتنزّل الزمن لـ 38ms.

N+1 Query Problem: السبب الخفي وراء بطء كل dashboard

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

الـ ORM لمّا بتطلب منه قائمة منتجات وتحاول تطبع اسم القسم لكل منتج، هو بيعمل استعلام واحد للقائمة، وبعدين N استعلام إضافي (واحد لكل منتج) لجلب الأقسام. اللي كان لازم يبقى استعلام واحد بـ JOIN، بقى N+1.

ده مش bug في الـ ORM. ده سلوك lazy loading افتراضي في كل ORM شائع. المشكلة بتظهر في الإنتاج لمّا N تكبر، مش في الـ dev اللي عندك فيه 50 منتج تجريبي بتاعك بيرد في 80ms.

مثال البيتزا (للمبتدئ)

تخيّل إنك في مطعم وعايز تعرف اسم الشيف اللي عمل كل بيتزا في طلب جماعي فيه 100 بيتزا. فيه طريقتين:

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

الفرق ده بالظبط هو N+1. الـ ORM بيعمل الطريقة الأولى افتراضياً، والـ JOIN في SQL هو الطريقة التانية.

لوحة تحليلات تعرض ارتفاع زمن استجابة dashboard ومؤشرات أداء قاعدة بيانات بطيئة

التعريف العلمي والمصدر

المصطلح بيرجع لكتاب Scott Ambler "Agile Database Techniques" 2003 وانتشر مع توثيق Hibernate و Rails ActiveRecord. التعريف الدقيق من توثيق Django ORM: "تنفيذ N استعلام إضافي بسبب الوصول لـ relation field على كل عنصر في query set". المرجع الرسمي في docs.djangoproject.com تحت قسم Database access optimization.

كود Prisma بيوضّح المشكلة بالأرقام

TypeScript
// ❌ المشكلة: 1 + N استعلام
const products = await prisma.product.findMany({ take: 100 });
for (const p of products) {
  const category = await prisma.category.findUnique({
    where: { id: p.categoryId }
  });
  console.log(p.name, category.name);
}
// إجمالي الاستعلامات: 101
// الزمن على PostgreSQL 16 / Prisma 5.18: ~ 4,200ms

// ✅ الحل: include
const products = await prisma.product.findMany({
  take: 100,
  include: { category: true }
});
// إجمالي الاستعلامات: 1
// الزمن: ~ 38ms

الفرق 110x. ده مش رقم نظري — ده اللي قِسته على dataset 480K منتج / 240 قسم على Postgres 16 محلي بـ 8GB RAM. الـ JOIN عند Postgres أرخص بكتير من 100 round-trip شبكة + 100 plan parsing. الافتراض هنا إن الـ DB والتطبيق على نفس الشبكة بـ latency أقل من 1ms — لو على شبكات مختلفة الفرق بيكبر أكتر.

ازاي تكتشف المشكلة في الإنتاج

  1. فعّل query log في الـ ORM. في Prisma: new PrismaClient({ log: ['query'] }).
  2. افتح أبطأ endpoint عندك في Postman وعدّ الاستعلامات في الـ terminal. لو شفت أكتر من 5 استعلامات على request بسيط، الاحتمال 80% إن عندك N+1.
  3. في الإنتاج، فعّل extension pg_stat_statements واسأل: "أكثر 10 queries تكراراً آخر ساعة". لو لقيت SELECT * FROM categories WHERE id = $1 بـ 47,000 calls في 60 دقيقة، ده هو.
شاشة كود تيرمنال تظهر استعلامات SQL متكررة بنفس النمط كدلالة على مشكلة N+1 في ORM

Trade-offs مهمة قبل ما تعمل include على كل حاجة

الـ include مش حل مجاني. لو عملته بدون تفكير:

  • over-fetching: الـ JOIN بيرجّع كل أعمدة الجدول الثاني حتى لو محتاج اسم بس. على جدول فيه description بطول 4KB، ده بيضخّم الـ payload 50x. الحل: select صريح للأعمدة المطلوبة بس.
  • cartesian explosion: لو عملت include لـ 3 relations فيهم many-to-many، الـ JOIN ممكن يطلع 100K row من 100 منتج. الـ ORM بيفك التكرار في memory، بس الـ DB بسحب كل ده على الشبكة.
  • plan caching: queries بـ JOIN معقّد عند Postgres محتاجة plan جديد كل ما تتغيّر الفلترة، أحياناً بتكون أبطأ من 5 استعلامات بسيطة على dataset أقل من ألف صف.

الـ trade-off الأهم: بتكسب 50-100x سرعة، بتخسر شوية تحكّم في شكل البيانات الراجعة، وبتزوّد ذاكرة الـ application 20-40% لمّا الـ relations كبيرة.

متى لا تستخدم eager loading أصلاً

  • لمّا الـ relation اختيارية وبتظهر في 5% من الحالات بس. حمّلها lazy وقت ما تحتاجها فعلاً.
  • لمّا الجدول الثاني فيه أعمدة ضخمة (binary، JSON كبير) ومش هتعرضهم. Lazy + select صريح أرخص.
  • لو في GraphQL endpoint مع DataLoader. الـ DataLoader بيـ batch الاستعلامات لـ WHERE id IN (...) وبيدّيك نفس النتيجة بـ flexibility أكتر من include الثابت.
  • على dataset أقل من 1,000 صف الفرق ممكن يبقى أقل من 20ms ومش يستاهل تعقيد الكود.

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

افتح أبطأ endpoint عندك دلوقتي. شغّل query log في الـ ORM لمدة 60 ثانية وعدّ الاستعلامات. لو الرقم أكبر من 10 على request بسيط، عندك N+1 وفاتورة DB بتنزل 60-90% بـ 3 سطور تعديل. ابدأ بأبطأ endpoint واحد بس، قِس قبل وبعد، وبعدين عمّم.

المصادر

  • Django Documentation — Database access optimization: docs.djangoproject.com/en/5.0/topics/db/optimization
  • Prisma Docs — Solving the n+1 problem: prisma.io/docs/orm/prisma-client/queries/relation-queries
  • PostgreSQL Documentation — pg_stat_statements: postgresql.org/docs/16/pgstatstatements.html
  • Hibernate User Guide — Fetching strategies: docs.jboss.org/hibernate/orm/6.4/userguide
  • Scott Ambler, "Agile Database Techniques: Effective Strategies for the Agile Software Developer" (Wiley, 2003)

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

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

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