مستوى القارئ المطلوب: متوسط. تحتاج أساسيات 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 بيوقع في الفخ:
# N+1: استعلام واحد للمقالات، ثم استعلام لكل مقال لجلب الكاتب
posts = Post.objects.all() # +1 (استعلام واحد)
for post in posts: # N (استعلام لكل صف)
print(post.author.name) # كل مرة يضرب قاعدة البيانات
لو عندك 100 مقال، ده 101 استعلام. الحل إنك تقول للـ ORM يجيب الكاتب مع المقالات في استعلام واحد بـ JOIN:
# الحل: JOIN واحد يجلب المقال والكاتب معًا
posts = Post.objects.select_related("author").all() # استعلام واحد فقط
for post in posts:
print(post.author.name) # لا يضرب قاعدة البيانات مرة أخرى
نفس الفكرة في SQLAlchemy باستخدام :