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

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

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

المنصة

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

الدعم

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

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

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

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

متوسط11 أغسطس 20265 دقائق قراءة
مشكلة N+1: ليه صفحة واحدة بتعمل 101 استعلام على قاعدة البيانات

مستوى القارئ المطلوب: متوسط. تحتاج أساسيات SQL وتكون تعاملت قبل كده مع ORM (مثل Django أو SQLAlchemy أو Prisma). مش لازم تكون خبير قواعد بيانات.

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

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

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

الـ N+1 معناها إنك بتعمل استعلام واحد علشان تجيب قائمة (ده الـ +1)، وبعدين استعلام إضافي لكل عنصر في القائمة (دي الـ N). لو القائمة فيها 100 صف، بتطلع 101 رحلة كاملة لقاعدة البيانات في الطلب الواحد. كل رحلة منها فيها زمن شبكة وتحليل استعلام وتنفيذ. المجموع بيتراكم بسرعة.

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

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

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

الشرح العلمي: ليه بيحصل

الـ ORM بيخفي عنك الاستعلامات، وده مريح لكنه بيخبّي المشكلة. لما تكتب حلقة (loop) بتقرأ علاقة (relationship) لكل عنصر، الـ ORM بيعمل lazy loading: بيأجّل تحميل العلاقة لحد ما تطلبها، وساعتها بيطلق استعلام جديد. الحلقة اللي شكلها بريء بتتحوّل لعشرات الاستعلامات.

الكود ده في Django بيوقع في الفخ:

Python
# N+1: استعلام واحد للمقالات، ثم استعلام لكل مقال لجلب الكاتب
posts = Post.objects.all()          # +1  (استعلام واحد)
for post in posts:                  # N   (استعلام لكل صف)
    print(post.author.name)         # كل مرة يضرب قاعدة البيانات

لو عندك 100 مقال، ده 101 استعلام. الحل إنك تقول للـ ORM يجيب الكاتب مع المقالات في استعلام واحد بـ JOIN:

Python
# الحل: JOIN واحد يجلب المقال والكاتب معًا
posts = Post.objects.select_related("author").all()   # استعلام واحد فقط
for post in posts:
    print(post.author.name)         # لا يضرب قاعدة البيانات مرة أخرى

نفس الفكرة في SQLAlchemy باستخدام joinedload:

Python
from sqlalchemy.orm import joinedload

posts = (
    session.query(Post)
    .options(joinedload(Post.author))   # JOIN بدل الاستعلامات المتكررة
    .all()
)

وعلى مستوى SQL الخام، ده الفرق. بدل حلقة من:

SQL
SELECT * FROM posts;                       -- مرة واحدة
SELECT * FROM authors WHERE id = 1;        -- تتكرر N مرة
SELECT * FROM authors WHERE id = 2;
-- ...

تعملها في استعلام واحد:

SQL
SELECT posts.*, authors.name
FROM posts
JOIN authors ON authors.id = posts.author_id;

القياس: قبل وبعد

على جدول 1000 مقال في PostgreSQL مع كاش دافئ، النمط N+1 بيعمل 1001 استعلام وياخد حوالي 1240 مللي ثانية للطلب الواحد. بعد استبداله بـ JOIN واحد، العدد بينزل لاستعلام واحد والزمن لحوالي 48 مللي ثانية. ده تحسّن حوالي 96% في الزمن. الأرقام دي تقريبية وبتختلف حسب الشبكة والفهارس، لكن ترتيب الحجم ثابت: بتشيل مئات الرحلات وتحطها في رحلة واحدة.

إزاي تكتشفها قبل ما توصل الإنتاج

متعتمدش على العين. الأدوات بتكشفها بالظبط:

  • في Django فعّل django-debug-toolbar أو اطبع len(connection.queries) في التطوير. لو رقم الاستعلامات بيزيد كل ما زودت صف، دي N+1.
  • في SQLAlchemy شغّل echo=True على الـ engine وشوف نفس الاستعلام بيتكرر بمعاملات مختلفة.
  • على مستوى قاعدة البيانات، امتداد pg_stat_statements بيوريك الاستعلام اللي بيتنفّذ آلاف المرات بصياغة واحدة.

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

الحل مش مجاني. JOIN على علاقة واحد-لكثير بيكرّر بيانات الأب في كل صف من الأبناء، فبيزيد حجم الناتج المنقول. لو المقال الواحد ليه 50 تعليق، الـ joinedload هيرجّع بيانات المقال 50 مرة. المكسب: رحلة واحدة. الخسارة: بيانات مكررة على الشبكة وذاكرة أعلى شوية في التطبيق.

البديل الأنظف للعلاقات الكبيرة هو التحميل بدفعة (batching) باستعلامين بدل JOIN ضخم: استعلام للأصل، واستعلام واحد بـ IN لكل الأبناء دفعة واحدة. ده اللي بيعمله prefetch_related في Django وselectinload في SQLAlchemy:

Python
# استعلامان فقط: واحد للمقالات، وواحد بـ IN لكل التعليقات
posts = Post.objects.prefetch_related("comments").all()

الافتراض هنا إن العلاقة واحد-لواحد أو واحد-لعدد قليل يبقى select_related/joinedload أفضل. أما واحد-لكثير، فالـ prefetch_related/selectinload أوفر لأنه بيتجنّب تضخّم الصفوف.

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

لو الحلقة بتمر على 3 أو 4 عناصر بس ومش endpoint ساخن، التحسين مش أولوية؛ الـ N+1 هنا فرقها مللي ثانية. كمان لو انت محتاج بيانات العلاقة بشرط ديناميكي مختلف لكل صف (مش نفس الاستعلام)، الـ eager loading مش هيفيد وساعتها الحل بيبقى إعادة تصميم الاستعلام نفسه. وأخيرًا، متحطّش select_related على كل علاقة بشكل أعمى؛ JOIN لعلاقات كتير في استعلام واحد ممكن يبقى أبطأ من الأصل.

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

افتح أبطأ endpoint عندك بيرجّع قائمة، وشغّل عدّاد الاستعلامات في التطوير (django-debug-toolbar أو echo=True). لو لقيت العدد بيكبر مع حجم الداتا، ضيف select_related للعلاقات واحد-لواحد وprefetch_related للعلاقات واحد-لكثير، وقيس الفرق قبل وبعد. لو العدد نزل من مئات لاستعلامين، يبقى شيلت N+1.

المصادر

  • Django Docs — QuerySet.select_related() و prefetch_related(): docs.djangoproject.com
  • SQLAlchemy Docs — Relationship Loading Techniques (joinedload / selectinload): docs.sqlalchemy.org
  • PostgreSQL Docs — pg_stat_statements: postgresql.org
  • PostgreSQL Docs — Joins Between Tables: postgresql.org

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

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

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