مستوى المقال: متوسط. لو بتشتغل على تطبيق فيه ORM (Django أو Rails أو Laravel أو Prisma) وصفحة بتبطّأ فجأة أول ما البيانات تكبر، المقال ده يخصّك.
مشكلة N+1: ليه صفحة واحدة تضرب قاعدة بياناتك بمئات الاستعلامات
هتطلع من هنا عارف تكتشف مشكلة N+1 في دقيقتين، وتصلّحها بسطر واحد ينزّل من 201 استعلام لـ2. ركز في المثال الأول، بعدين تعالَ للكود.
المشكلة باختصار
عندك جدول مقالات، وكل مقال ليه كاتب. بتعرض 200 مقال في صفحة، وجنب كل واحد اسم الكاتب. الكود شغّال تمام على جهازك بـ10 صفوف. بتنشر، البيانات تكبر، والصفحة تاخد تلت ثانية بس على الاستعلامات. السبب مش السيرفر — السبب إنك بتعمل استعلام لكل كاتب على حدة.
مثال يقرّبها: النادل اللي بيروح المطبخ 200 مرة
تخيّل مطعم فيه 200 طاولة. النادل بياخد أوردر السلطة من كل طاولة، وبدل ما يجمّع الطلبات ويروح المطبخ مرة واحدة، بيروح ويرجع لكل طاولة لوحدها. يبقى 200 رحلة رايح جاي. الأكل نفسه اتعمل بسرعة، بس المشي بين الصالة والمطبخ هو اللي بلع الوقت.
ده بالظبط اللي بيحصل في N+1: كل "رحلة" هي استعلام (round-trip) لقاعدة البيانات. الاستعلام نفسه سريع، لكن عدد الرحلات هو المشكلة.
الشرح العلمي: منين جت الـ N+1
الاسم معناه: استعلام واحد بيجيب قائمة المقالات (ده الـ 1)، وبعدين لكل مقال بيتعمل استعلام تاني يجيب الكاتب (دي الـ N). عندك 200 مقال؟ يبقى 1 + 200 = 201 استعلام. كل استعلام فيه زمن ذهاب وإياب للشبكة بينك وبين الداتابيز؛ حتى لو 1.5 مللي ثانية للواحد، 201 استعلام يعني تأخير تراكمي واضح، وبيزيد خطّيًا كل ما البيانات تكبر.
الافتراض اللي بيخلّي ده يحصل إن الـ ORM بيحمّل العلاقة "كسول" (lazy loading): مبيجيبش الكاتب إلا لحظة ما تطلبه جوّه اللوب.
الحل: eager loading بسطر واحد
الفكرة إنك تقول للـ ORM: هات المقالات والكتّاب في استعلام أو اتنين بس، مش 201.
# المشكلة: N+1
posts = Post.objects.all() # استعلام 1
for p in posts:
print(p.author.name) # استعلام لكل مقال (N)
# الحل: JOIN في استعلام واحد
posts = Post.objects.select_related("author") # الإجمالي: استعلام واحد
for p in posts:
print(p.author.name) # من غير أي استعلام إضافي
# لعلاقة متعددة (tags): استعلامين بدل N
posts = Post.objects.prefetch_related("tags")
في Django: select_related بيعمل JOIN، وده مناسب لعلاقة foreign key واحدة. وprefetch_related بيعمل استعلام تاني بـ IN (...)، وده مناسب للعلاقات المتعددة. المكافئ في Rails هو includes، في SQLAlchemy joinedload وselectinload، وفي Prisma include.