المستوى: محترف
لو 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
# 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})