لو الـ Aggregation Pipeline في MongoDB بياخد 5 ثواني على collection فيها 2 مليون مستند، المشكلة مش في حجم الداتا غالبًا — المشكلة في ترتيب الـ stages.
المشكلة باختصار
الـ Aggregation Pipeline بيشتغل خطوة بعد خطوة. كل stage بياخد المخرج من اللي قبله. لو بدأت بـ $lookup قبل $match، أنت بتعمل join على كل المستندات، وبعدين بتفلتر. النتيجة: 95% من الشغل ده مرمي في الزبالة، والـ DB ضربها CPU 100%.
الافتراض هنا: عندك collection بحجم متوسط لكبير (≥ 500K مستند)، و pipeline فيه $lookup أو $group أو $unwind.
ركّز: الـ Aggregation Pipeline بيشتغل إزاي فعلاً
مثال للمبتدئين قبل الكلام التقني
تخيّل عندك صندوق فيه 1000 كتاب. طلبت منك مديرتك حاجة من اتنين:
- الطريقة الأولى: "روح اكتشف اسم الكاتب لكل كتاب من الـ 1000، وبعدين قولّي مين منهم كاتبه أحمد." يعني 1000 عملية بحث، ثم فلترة.
- الطريقة التانية: "اطلع الأول الكتب اللي على غلافها اسم أحمد، لقيت 30 كتاب، وبعدين روح هات تفاصيل الكاتب لكل واحد فيهم." يعني 30 عملية بحث فقط.
الفرق بين الاتنين تقريبًا 33×. ده بالظبط الفرق بين $lookup قبل $match، و $lookup بعد $match في MongoDB.
التعريف العلمي الدقيق
الـ Aggregation Pipeline في MongoDB سلسلة من المراحل (stages)، كل مرحلة بتاخد مجموعة مستندات كدخل وتطلع مجموعة مستندات كخرج. ترتيب الـ stages بيحدد عدد المستندات اللي بيدخل كل stage. الـ MongoDB query optimizer بيعمل بعض إعادة الترتيب التلقائية (مثل دمج $match متتاليين، أو نقل $match قبل $project لو الحقول مستقلة)، لكنه مش بيعكس الترتيب لمّا يكون فيه dependency حقيقي بين stages — المسؤولية عليك.
الحل: رتّب الـ stages بهذا الترتيب
القاعدة الذهبية: قلّل عدد المستندات قبل كل عملية مكلفة. الترتيب الموصى به:
$match— فلترة بالـ index قدر المستطاع، أول حاجة في الـ pipeline.$project— شيل الحقول اللي مش هتستخدمها، يقلل الذاكرة و IO.$sort+$limit— لو محتاج أعلى N، طبّقهم قبل أي join.$lookup— الجوين بيتنفّذ على عدد مستندات أقل بكثير دلوقتي.$unwindو$group— في النهاية على الناتج المفلتر.
مثال تنفيذي بأرقام حقيقية
السيناريو: متجر إلكتروني فيه 2 مليون order و 500K user. المطلوب: آخر 100 طلب مكتمل لمستخدمين من مصر.
الـ pipeline البطيء (قياس فعلي: 5.21 ثانية):