المستوى: متوسط — مقال موجّه للمطور اللي بيستخدم ORM زي Prisma أو Sequelize أو Django ORM وبيشوف الـ DB بطيء بدون سبب واضح. لو لسه بتتعلم SQL أساسي، تقدر تقراه عادي بس ركّز في مثال البيتزا قبل ما تدخل على الكود.
لو dashboard المنتجات بتاعك بيرد في 6 ثواني وفاتورة DB قفزت 3x من غير ما traffic يزيد، الاحتمال الأكبر إن الـ ORM بيعمل 1,247 استعلام على كل request بدل استعلام واحد. المشكلة دي اسمها N+1 Query، وحلّها 3 سطور كود بتنزّل الزمن لـ 38ms.
N+1 Query Problem: السبب الخفي وراء بطء كل dashboard
المشكلة باختصار
الـ ORM لمّا بتطلب منه قائمة منتجات وتحاول تطبع اسم القسم لكل منتج، هو بيعمل استعلام واحد للقائمة، وبعدين N استعلام إضافي (واحد لكل منتج) لجلب الأقسام. اللي كان لازم يبقى استعلام واحد بـ JOIN، بقى N+1.
ده مش bug في الـ ORM. ده سلوك lazy loading افتراضي في كل ORM شائع. المشكلة بتظهر في الإنتاج لمّا N تكبر، مش في الـ dev اللي عندك فيه 50 منتج تجريبي بتاعك بيرد في 80ms.
مثال البيتزا (للمبتدئ)
تخيّل إنك في مطعم وعايز تعرف اسم الشيف اللي عمل كل بيتزا في طلب جماعي فيه 100 بيتزا. فيه طريقتين:
- الطريقة الغلط: تروح للجرسون وتسأله "إيه الـ 100 بيتزا اللي اتعملت؟"، يجيبلك القائمة. بعدين تروح ليه 100 مرة تاني، كل مرة تقوله "البيتزا دي عملها مين؟". إجمالي: 101 رحلة للمطبخ.
- الطريقة الصح: تقول للجرسون من الأول "هاتلي الـ 100 بيتزا واسم شيف كل واحدة فيهم". رحلة واحدة بس.
الفرق ده بالظبط هو N+1. الـ ORM بيعمل الطريقة الأولى افتراضياً، والـ JOIN في SQL هو الطريقة التانية.
التعريف العلمي والمصدر
المصطلح بيرجع لكتاب Scott Ambler "Agile Database Techniques" 2003 وانتشر مع توثيق Hibernate و Rails ActiveRecord. التعريف الدقيق من توثيق Django ORM: "تنفيذ N استعلام إضافي بسبب الوصول لـ relation field على كل عنصر في query set". المرجع الرسمي في docs.djangoproject.com تحت قسم Database access optimization.
كود Prisma بيوضّح المشكلة بالأرقام
// ❌ المشكلة: 1 + N استعلام
const products = await prisma.product.findMany({ take: 100 });
for (const p of products) {
const category = await prisma.category.findUnique({
where: { id: p.categoryId }
});
console.log(p.name, category.name);
}
// إجمالي الاستعلامات: 101
// الزمن على PostgreSQL 16 / Prisma 5.18: ~ 4,200ms
// ✅ الحل: include
const products = await prisma.product.findMany({
take: 100,
include: { category: true }
});
// إجمالي الاستعلامات: 1
// الزمن: ~ 38ms