المستوى: متوسط. المقال ده موجّه لمطوّر بيتعامل مع ORM (زي Django أو Laravel أو Prisma) وشايف بطء غريب في صفحات بسيطة. لو لسه مبتدئ خالص، فيه مثال المطعم تحت هيوصّلك الفكرة قبل الجزء التقني.
لو صفحة بتعرض 20 صف بتاخد قرب الثانية والسيرفر مفيهوش أي حمل، المشكلة غالبًا مش في السيرفر ولا في اتصال الشبكة. المشكلة إن الكود بيبعت للقاعدة استعلام لكل صف بدل استعلام واحد. ده اسمه مشكلة N+1، ولو مسكتها هتنزّل زمن الصفحة من ثانية لأقل من 50 ملّي ثانية بسطر واحد.
مشكلة N+1 في الاستعلامات: الشرح الكامل
المشكلة باختصار
الـ ORM بيخلّي جلب البيانات يبان سهل، لدرجة إنك مبتشوفش الاستعلامات اللي بتتبعت فعلًا. بتكتب سطر بايثوني عادي، وهو ورا ضهرك بيضرب القاعدة عشرات المرات. النتيجة: صفحة تقيلة، قاعدة بيانات مضغوطة بلا داعي، وفاتورة سيرفر أعلى.
الفكرة بمثال بسيط قبل التقنية
تخيّل نادل في مطعم. جالك 10 زباين على ترابيزة. بدل ما ياخد الطلبات كلها مرة واحدة ويروح المطبخ رحلة واحدة، بيروح ياخد طلب زبون، يرجع المطبخ، يرجع تاني ياخد طلب التاني، وهكذا. عشر رحلات للمطبخ عشان حاجة كان ممكن تتعمل في رحلة أو اتنين.
الرحلة للمطبخ هنا هي الاستعلام للقاعدة. الكود اللي فيه N+1 بيعمل بالظبط كده: رحلة واحدة يجيب فيها قايمة العناصر (ده الـ 1)، وبعدين رحلة منفصلة لكل عنصر عشان يجيب تفاصيله المرتبطة (دول الـ N).
الشرح العلمي الدقيق
N+1 معناها إن عملية واحدة منطقيًا اتحوّلت لعدد استعلامات = 1 (للقايمة الأصلية) + N (استعلام لكل صف من صفوف القايمة). لو جبت 100 مقال وكل مقال عايز اسم كاتبه، الـ ORM بيعمل استعلام واحد للمقالات، وبعدين 100 استعلام منفصل لجدول الكُتّاب. الإجمالي 101 استعلام بدل 2.
كل استعلام منهم بياخد وقت شبكة ذهاب وعودة (round trip) للقاعدة، حتى لو الاستعلام نفسه أسرع من ملّي ثانية. لو الـ round trip الواحد 1 ملّي ثانية، فـ 100 استعلام زيادة = 100 ملّي ثانية ضايعة في الانتظار مش في الحساب.
الكود اللي بيسبّب المشكلة والحل
ده مثال بـ Django ORM. الكود ده بيبان بريء، لكنه بيفجّر N+1:
# الكود الغلط: استعلام لكل مقال عشان يجيب الكاتب
articles = Article.objects.all() # استعلام 1: كل المقالات
for article in articles:
print(article.author.name) # استعلام إضافي لكل مقال (N)
# الإجمالي لـ 100 مقال = 101 استعلام
الحل إنك تقول للـ ORM يجيب الكُتّاب المرتبطين في نفس الوقت باستخدام JOIN واحد: