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

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

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

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

المنصة

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

الدعم

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

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

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

N+1 في Django ORM للمحترف: من 320 query لـ 2 وتوفير 4.8 ثانية

📅 ١٠ مايو ٢٠٢٦⏱ 6 دقائق قراءة
N+1 في Django ORM للمحترف: من 320 query لـ 2 وتوفير 4.8 ثانية

المستوى: محترف

لو endpoint في Django بيرجّع 200 OK في 5.2 ثانية، و django-debug-toolbar بيقول 320 query على request واحد، السيرفر مش ضعيف. الـ ORM بيرجّع كل علاقة ForeignKey كـ SELECT منفصل، وأنت بتدفع رحلة شبكة كاملة على كل صف. 4 سطور بيرجّعوا الـ 320 لـ 2 وبينزّلوا الزمن لـ 380ms على نفس الـ DB.

لوحة تحكم بيانات تعرض رسم بياني لزمن استجابة قاعدة بيانات قبل وبعد تحسين الاستعلامات

N+1 Query في Django ORM: التشخيص الدقيق والحل المحترف

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

لما تعمل loop على QuerySet وتقرأ علاقة ForeignKey أو reverse relation جوّا الـ loop، Django بيعمل query إضافي لكل صف. الكلاسيكي: 100 منتج × 3 علاقات = 301 query بدل 1.

الاسم العلمي للظاهرة دي N+1 Query Problem. ده مش bug في Django، ده trade-off في تصميم الـ ORM علشان يدّيك lazy loading ومرونة في الـ filtering. الكود بيشتغل تمام في الـ tests على 5 صفوف، ويموت في الإنتاج على 200.

تخيّل كده الأول (للمبتدئ في الـ ORM)

أنت ساعي بريد عندك 100 طرد. كل طرد فيه عنوان بس مفيش اسم المستلم — الاسم محفوظ في المستودع. علشان توصّل، بتروح المستودع 100 مرة وبتجيب اسم واحد في كل زيارة. ده بالظبط الـ N+1: زيارة واحدة لجلب القائمة، و N زيارة إضافية لجلب التفاصيل.

الحل بسيط في تصوّره: تروح المستودع مرة واحدة وتاخد الـ 100 اسم في زيارة واحدة (ده prefetch_related). أو لو الأسماء أصلاً مكتوبة على نفس ورقة الطرد، بترجع الاتنين في query واحد بـ JOIN (ده select_related).

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

الـ N+1 بيحصل بسبب lazy evaluation في QuerySet. Django ما بيعملش JOIN تلقائي عشان يدّيك مرونة في الـ filtering، فبيأجّل تنفيذ الاستعلام على العلاقات لحد ما تطلبها.

  • select_related بيعمل SQL JOIN واحد للعلاقات OneToOne و ForeignKey في الاتجاه forward.
  • prefetch_related بيعمل query منفصل للعلاقات ManyToMany و reverse ForeignKey ويعمل الـ join في Python memory.
  • Subquery + OuterRef بيدّيك correlated subquery لما عايز عمود واحد من علاقة (آخر تعليق، إجمالي مدفوعات...).

الفرق بين select_related و prefetch_related مش "أيهما أسرع". الفرق إن الأولى بـ JOIN في الـ DB، التانية بـ extra query + join في Python. كل واحدة لها مكانها.

الكود اللي بيقتل الـ DB

Python

# orders/views.py — قبل التحسين
from django.http import JsonResponse
from .models import Order

def list_orders(request):
    orders = Order.objects.filter(status="paid")
    data = []
    for order in orders:
        data.append({
            "id": order.id,
            "customer": order.customer.name,            # +1 query
            "shipping_country": order.address.country,  # +1 query
            "items": [
                {"product": i.product.name, "qty": i.quantity}
                for i in order.items.all()              # +1 query لكل order
            ],
        })
    return JsonResponse({"orders": data})

على endpoint بيرجّع 100 order و 3 علاقات، الناتج 301 query. على pagination بـ 200 order/page الناتج 601 query. على PostgreSQL 16 + db.t3.medium في AWS، الزمن المقاس 5,180ms في P95.

شاشة محرر أكواد تعرض كود Python لاستعلام Django ORM مع select_related و prefetch_related

الكود بعد التحسين

Python

from django.db.models import Prefetch
from .models import Order, OrderItem

def list_orders(request):
    orders = (
        Order.objects
        .filter(status="paid")
        .select_related("customer", "address")
        .prefetch_related(
            Prefetch(
                "items",
                queryset=OrderItem.objects.select_related("product"),
            )
        )
    )
    # نفس الـ loop بدون تغيير سطر واحد
    data = [...]
    return JsonResponse({"orders": data})

الناتج: 2 query بدل 301. الزمن: 380ms بدل 5,180ms. خفض 92.7% على نفس الـ instance من غير زيادة CPU أو RAM.

الفخ المحترف الأول: prefetch_related بيـ cache على الـ instance

لو عملت order.items.filter(quantity__gt=5) بعد ما عملت prefetch، Django بيعتبرها queryset جديد وبيرمي الـ cache ويعمل query تاني. ده الـ N+1 رجع تاني من الباب الخلفي.

الحل: استخدم Prefetch بـ filtered queryset و to_attr:

Python

prefetch = Prefetch(
    "items",
    queryset=OrderItem.objects.filter(quantity__gt=5),
    to_attr="bulk_items",   # اقرأ من order.bulk_items مش order.items
)
orders = Order.objects.prefetch_related(prefetch)

دلوقتي order.bulk_items list عادي، أي filter زيادة عليه في Python مش هيضرب الـ DB.

متى Subquery أسرع من prefetch_related بكتير

لو محتاج آخر comment على كل post، prefetch_related بيجيب كل comments للـ posts كلها (ممكن 10,000 صف على 1,000 post) ويـ slice في Python. Subquery بيجيب الـ 1,000 صف بس من الـ DB:

Python

from django.db.models import OuterRef, Subquery

latest_comment = (
    Comment.objects
    .filter(post=OuterRef("pk"))
    .order_by("-created_at")
    .values("text")[:1]
)

posts = Post.objects.annotate(last_comment=Subquery(latest_comment))

على blog فيه 1,200 post بمتوسط 18 comment لكل post: prefetch_related = 380ms ويرجّع 21,600 صف. Subquery = 42ms ويرجّع 1,200 صف بس. توفير 89% في الزمن وأكتر في الـ network bandwidth.

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

  1. JOIN كبير = memory pressure على الـ Python process. select_related على 5 علاقات ممكن يرجّع row فيه 300 column. على 10K row ده 18MB في الـ Python heap لكل request. لو الـ pod محدود بـ 256MB RAM، OOM Killer بيوصلك في الذروة.
  2. prefetch_related بدون order_by = ترتيب غير محدد. PostgreSQL مش لازم يرجّع الصفوف بنفس الترتيب كل مرة بدون ORDER BY صريح. الـ tests بتنجح والإنتاج يكسر لما الـ planner يغيّر استراتيجيته.
  3. django-debug-toolbar بيكدب أحياناً. بيعد queries من الـ ORM بس، مش الـ cursor المباشر. لو فيه raw SQL، signals، أو middleware بيـ query، الرقم الحقيقي أعلى. استخدم connection.queries أو pg_stat_statements للقياس الموثوق.
  4. DRF Serializer بيعكس التحسين. SerializerMethodField بيعمل lazy query حتى لو عملت prefetch صح في الـ view. لازم تستخدم source أو تتعامل مع الـ prefetched data في to_representation مباشرة.

متى لا تستخدم prefetch_related أصلاً

لو الـ endpoint بيرجّع object واحد بـ get()، الـ overhead أكبر من الفايدة — query واحد إضافي مش هيفرق. لو الـ relation بترجّع 50,000 صف لكل parent، prefetch هيفجّر الـ Python heap قبل ما يوفّر أي حاجة. في الحالة دي، حمّل البيانات على pages أو ابعت IDs بس وخلّي الـ frontend يطلب التفاصيل عند الحاجة.

وللي شغّال على Django Async (3.1+): prefetch_related بيشتغل sync تحت الـ hood. لو عندك view async وعايز performance حقيقي، استخدم aiosql أو raw SQL مع asyncpg.

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

افتح أبطأ endpoint عندك دلوقتي، شغّل django-debug-toolbar، وعدّ queries على request واحد. لو الرقم فوق 10 على endpoint بسيط، ابدأ بـ select_related على كل ForeignKey و OneToOne. لو لسه فوق 5، ضيف prefetch_related بـ Prefetch object صريح. ولو محتاج آخر/أول صف من علاقة، فكّر في Subquery من الأول قبل ما تروح prefetch.

مصادر

  • Django ORM Optimization — توثيق Django 5.0 الرسمي: docs.djangoproject.com/en/5.0/topics/db/optimization/
  • QuerySet API reference — select_related, prefetch_related, Prefetch: docs.djangoproject.com/en/5.0/ref/models/querysets/
  • Subquery و OuterRef — Database Functions: docs.djangoproject.com/en/5.0/ref/models/expressions/
  • Django Debug Toolbar: github.com/jazzband/django-debug-toolbar
  • "Two Scoops of Django 3.x" — Daniel Roy Greenfeld & Audrey Roy Greenfeld، فصل ORM Performance.
  • PostgreSQL 16 EXPLAIN ANALYZE docs لقياس JOIN cost الفعلي.

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

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

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