MongoDB: مسارات التجميع المتقدمة

آخر تحديث: 2026-08-26

التجميع المتقدم في مسار المعالجة — إن إتقان التعبيرات المعقدة وتحويلات الأنواع يمكن أن يحل 90% من سيناريوهات تحليل البيانات.

مسار التعلم المتقدم لخطوط أنابيب التجميع: تبدأ هذه الدورة بالتعبيرات الشرطية ($cond/$switch/$ifNull)، ثم تنتقل إلى عمليات التاريخ، وتحويلات الأنواع، وعمليات السلاسل، وعمليات المصفوفات، لتتوج بتمرين عملي شامل. كل موضوع مستقل عن الآخر ولكنه مرتبط بالآخرين — تُستخدم التعبيرات الشرطية في «تحديد أيام الأسبوع/عطلات نهاية الأسبوع» ضمن عمليات التواريخ، وتُستخدم تحويلات الأنواع في «تنسيق التواريخ باستخدام $toString»، وتُستخدم عمليات السلاسل في «ربط النصوص وعرضها». ويساعد فهم هذه الروابط في بناء شبكة معرفية شاملة.

نظام التعبيرات في مسار التجميع: تنقسم التعبيرات في مسار التجميع في MongoDB إلى خمس فئات: 1. التعبيرات المنطقية ($cond/$switch/$ifNull/$and/$or/$not): الفحوصات الشرطية والتركيبات المنطقية؛ 2. تعبيرات المقارنة ($eq/$gt/$gte/$lt/$lte/$ne/$cmp): مقارنات القيم؛ 3. تعبيرات حسابية ($add/$subtract/$multiply/$divide/$mod/$round): الحسابات العددية؛ 4. التعبيرات السلسلية ($concat/$substr/$toUpper/$toLower/$split): معالجة النصوص؛ 5. التعبيرات الصفية ($map/$filter/$reduce/$arrayElemAt/$size): تحويلات المصفوفات. من خلال إتقان هذه الأنواع الخمسة من التعبيرات، يمكنك دمجها لإنشاء منطق تحويل البيانات بأي درجة من التعقيد.

طريقة تذكر لتصنيف التعبيرات: يمكن حفظ الأنواع الخمسة للتعبيرات باستخدام الطريقة التالية: «الشرطي → المقارنة → الحساب → النص → المصفوفة» — 1. أولاً، قم بتقييم الشرط (منطقية): هل هناك حاجة إلى عملية حسابية؟ أي فرع يجب اختياره؟ 2. بعد ذلك، قم بالمقارنة: هل القيمة تستوفي الشرط؟ 3. ثم قم بالحساب: قم بإجراء العمليات الحسابية إذا تم استيفاء الشرط؛ 4. ثم قم بالتنسيق: حوّل نتيجة الحساب إلى نص للعرض؛ 5. ثم قم بمعالجة المجموعة: قم بإجراء عمليات دفعية على المصفوفة. تتوافق هذه العملية تمامًا مع الخطوات النموذجية في حساب التقارير — تحديد شروط التجميع → مقارنة القيم الحدية → حساب القيم الإحصائية → تنسيق المخرجات → معالجة حقول المصفوفة.

قوة دمج التعبيرات: تكمن القوة الحقيقية لمسار التجميع في دمج التعبيرات — ففي حين أن التعبير الفردي يقوم بتحويل بسيط، فإن دمجها معًا يمكن أن يحل مشكلات الأعمال المعقدة. مثال: احسب "تسمية فئة المستخدم" = $switch(الشرط: $gte(totalSpent, 10000) → 'VIP') → $concat(التسمية + سلسلة totalSpent) → الناتج "VIP ¥15,800". تجمع هذه السلسلة بين أربعة أنواع من التعبيرات: $switch (الشرط)، و$gte (المقارنة)، و$concat (تسلسل السلاسل)، و$toString (تحويل النوع). إن فهم تكوين التعبيرات هو الفارق بين «معرفة كيفية استخدام مسار التجميع» و«إتقان مسار التجميع» — فالأول يعرف فقط العوامل الفردية، بينما الثاني قادر على دمجها لبناء أي مسار بيانات.

1. ما ستتعلمه


100%
graph LR
    A[Document] -->|$cond<br/>Sanyuan| B[Conditional Projection]
    A -->|$switch<br/>Multi-branch| C[Category Tags]
    A -->|$ifNull<br/>Handling Null Values| D[Default Value Replacement]
    A -->|$dateToString<br/>Date Format| E[Date Strings]
    A -->|$toInt/$toDecimal<br/>Type Conversion| F[Type Conversion]

    style B fill:#d4edda
    style C fill:#d4edda

2. التعبيرات الشرطية

شرح المفهوم: يعمل التعبير الشرطي بمثابة «تدفق التحكم المنطقي» في مسار التجميع، مما يتيح حساب كل قيمة في المستند ديناميكيًا بناءً على شروط معينة. $cond هو تعبير ثلاثي (if-then-else)، و$switch هو مطابقة متعددة الفروع (مشابهة لـ switch-case)، و$ifNull هو استبدال القيمة الفارغة (بتوفير قيمة افتراضية). وتُستخدم على نطاق واسع في $project و$addFields و$group.

شجرة القرار لاختيار التعبيرات الشرطية: يعتمد اختيار التعبير الشرطي المناسب على السيناريو — 1. الحاجة فقط إلى معالجة القيمة الفارغة (null) → $ifNull (الأبسط، على سبيل المثال، {$ifNull: ['$nickname', '$username']}); 2. الحاجة إلى خيار if-else → $cond (على سبيل المثال، {$cond: [{$gte: ['$age', 18]}, 'adult', 'minor']}); 3. الحاجة إلى 3 فروع أو أكثر → $switch (على سبيل المثال، التصنيف حسب مقدار الإنفاق: VIP/Silver/Bronze)؛ 4. الحاجة إلى «إرجاع قيمة افتراضية في حالة عدم العثور على القيمة» → $switch + default (على غرار CASE WHEN ... ELSE في لغة SQL). مبدأ اتخاذ القرار: استخدم $ifNull بدلاً من $cond كلما أمكن ذلك؛ واستخدم $cond بدلاً من $switch كلما أمكن ذلك — أعط الأولوية للبساطة ولا تلجأ إلى التعقيد إلا عند الضرورة.

كيفية العمل: يقوم $cond بتقييم شرط «if» لكل مستند ويُرجع إما قيمة «then» أو «else». يمكن استخدام $cond المتداخلة لتنفيذ شروط متعددة المستويات، لكن هذا يؤدي إلى ضعف قابلية القراءة — وفي مثل هذه الحالات، يوفر استخدام $switch مزيدًا من الوضوح. $ifNull يتحقق مما إذا كان الحقل فارغًا أو غير مُعرَّف؛ وإذا كان الأمر كذلك، فإنه يستبدله بقيمة بديلة. يتم تنفيذ جميع التعبيرات الشرطية على أساس كل مستند على حدة ولا تؤثر على المستندات الأخرى.

100%
graph TB
    A[Conditional Expression] --> B[$cond<br/>Sanyuan if-else]
    A --> C[$switch<br/>Multi-branch matching]
    A --> D[$ifNull<br/>Replacing Null Values]
    
    B --> E["price >= 1000 → 'expensive'<br/>else price >= 100 → 'medium'<br/>else → 'cheap'"]
    C --> F["status = 'paid' → 'Paid'<br/>status = 'shipped' → 'Shipped'<br/>default → 'Unknown'"]
    D --> G["nickname is null<br/>→ Replace with username"]
    
    style B fill:#d4edda
    style C fill:#cce5ff
    style D fill:#fff3cd
التعبير الصيغة حالة الاستخدام سهولة القراءة
$cond {if, then, else} حالتان من حالات التفرع متوسطة
متداخل $cond متداخل داخل "else" $cond 3+ فروع ضعيف
$switch {branches, default} 3+ فروع جيد
$ifNull { $ifNull: [expr, default] } التعامل مع القيم الفارغة جيد

النموذج الحسابي للتعبيرات الشرطية: يتم تقييم التعبيرات الشرطية ($cond/$switch/$ifNull) على أساس كل مستند على حدة ضمن مسار التجميع — حيث يحسب كل مستند نتيجته الشرطية بشكل مستقل، ولا تؤثر المستندات المختلفة على بعضها البعض. وهذا يعني أن $cond لا يقطع مسار الحسابات الخاصة بالمستندات الأخرى، ولا يوجد مشاركة للحالة بين المستندات. يساعد فهم ذلك على تجنب الأخطاء الشائعة: لا يمكن الإشارة إلى الحقول من المستندات الأخرى داخل $cond؛ يجب عليك أولاً استخدام $lookup لإنشاء علاقة قبل إجراء الحساب.

مقارنة أداء التعبيرات الشرطية: توجد اختلافات في الأداء بين التعبيرات الشرطية الثلاثة — 1. $ifNull: الأسرع (يتحقق فقط من القيمة الفارغة/غير المُعرَّفة؛ مقارنة واحدة)؛ 2. $cond: متوسط (شرط ثلاثي؛ كل فرع من فروع if/then/else ينفذ التعبير مرة واحدة)؛ 3. $switch: الأبطأ (يقوم بتقييم كل حالة من حالات الفروع بالتسلسل حتى يتم العثور على تطابق). الفرق في الأداء ضئيل جدًّا على مستوى المستند الواحد (بالميكروثانية)، لكن التأثير التراكمي كبير في مسارات التجميع التي تعالج ملايين المستندات. توصيات التحسين: 1. استخدم $ifNull بدلاً من $cond للاستبدالات البسيطة للقيمة null ({ifNull: ['$field', default]} مقابل {cond: [{eq: ['$field', null]}, default, '$field']})؛ 2. قم بفرز فروع $switch حسب احتمالية التطابق (ضع الحالة الأكثر تكرارًا في المقدمة لتقليل متوسط عدد عمليات التقييم)؛ 3. عندما يكون $cond متداخلًا لأكثر من 3 مستويات، انتقل إلى استخدام $switch (الذي يوفر قابلية قراءة وأداءً أفضل).

مبادئ العمل الداخلية لمُجمِّع $group: يحتفظ المجمع $group ($sum/$avg/$min/$max/$push/$addToSet) بمتغير حالة في الذاكرة، يتم تحديثه في كل مرة تتم فيها معالجة مستند. يبدأ $sum من 0 ويضيف قيمة الحقل لكل مستند؛ ويحتفظ $avg بمتغيرين — sum و count — ويحسب المتوسط في النهاية؛ ويقوم $push بإلحاق العناصر إلى مصفوفة؛ ويقوم $addToSet بإلحاق القيم الفريدة. يساعد فهم كيفية عمل المجمعات في تصميم عمليات $group فعالة — تجنب استخدام $push داخل $group لتجميع مصفوفات كبيرة (مما يستهلك الكثير من الذاكرة)؛ بدلاً من ذلك، استخدم $sum للعد أو قم بتطبيق $project أولاً لتقليل عدد الحقول.

التجميع مقابل MapReduce: في بداياتها، كانت MongoDB تستخدم MapReduce لتحليل البيانات المعقدة؛ ويُعد مسار التجميع بديلاً أكثر حداثة. مزايا مسار التجميع: 1. أسلوب إعلاني — ما عليك سوى وصف «ما يجب فعله» بدلاً من «كيفية فعله»، ويقوم مُحسِّن الأداء تلقائيًّا باختيار خطة التنفيذ؛ 2. قائم على مسار — يتم ربط المراحل معًا، مع تركيز كل مرحلة حصريًّا على منطقها الخاص؛ 3. الأداء — يتم تنفيذ مسارات التجميع بلغة C++، بينما يتم تفسير MapReduce وتنفيذه بلغة JavaScript، مما يؤدي إلى فرق في الأداء يتراوح بين 10 و100 ضعف. وقد أوقف MongoDB 5.0 دعم MapReduce؛ وأصبح مسار التجميع هو أداة التحليل الوحيدة الموصى بها.

مسار التعلم المتقدم لخطوط أنابيب التجميع: يغطي مسار التجميع المتقدم خمس فئات رئيسية من العمليات: التعبيرات الشرطية ($cond/$switch/$ifNull)، وعمليات التاريخ ($year/$dateToString)، وتحويلات الأنواع ($toString/$toInt/$toDecimal)، وعمليات السلاسل ($concat/$substr/$toUpper)، وعمليات المصفوفات ($arrayElemAt/$size/$map/$filter). هذه الفئات الخمس ليست منفصلة؛ بل تتشابك في مسارات العمل الفعلية — $group للتجميع → $switch للتفرع الشرطي → $dateToString للتنسيق → الإخراج. إتقان هذه العمليات المتقدمة هو الأساس لإنشاء تقارير معقدة.

أشجار القرار للتعبيرات الشرطية: تحديد التعبير الشرطي الذي يجب استخدامه — 1. هل تريد فقط التحقق من وجود قيم فارغة؟ → $ifNull (الأكثر إيجازًا، ومصمم خصيصًا لهذا الغرض)؛ 2. فرعان فقط؟ → $cond (صيغة الكائن {if, then, else} أسهل في القراءة؛ وصيغة المصفوفة [الشرط، القيمة الصحيحة، القيمة الخاطئة] أكثر إيجازًا)؛ 3. ثلاثة فروع أو أكثر؟ → $switch (يقوم بمطابقة الفروع واحدًا تلو الآخر باستخدام مصفوفة، مع خيار افتراضي؛ وقابلية القراءة أفضل بكثير من $cond المتداخلة)؛ 4. هل تحتاج إلى التحقق من عدة قيم فارغة في آن واحد؟ → قم بتسلسل استدعاءات $ifNull ($ifNull: [$a, $ifNull: [$b, 'default']]) أو استخدم $switch + $eq: [null, '$field']. يؤدي اختيار التعبير الصحيح إلى تحسين قابلية قراءة سلسلة العمليات بشكل كبير.

مقارنة أداء التعبيرات الشرطية: أداء التعبيرات الشرطية الثلاثة، من الأسرع إلى الأبطأ، هو: $ifNull > $cond > $switch. $ifNull هو الأسرع لأنه يتحقق من شرط واحد فقط (null أو undefined)؛ و$cond أبطأ قليلاً لأنه يتطلب تقييم تعبير if؛ و$switch هو الأبطأ لأنه يجب أن يقيّم كل حالة في المصفوفة branches واحدة تلو الأخرى. ومع ذلك، فإن الفرق في الأداء عادةً ما يكون أقل من 1 مللي ثانية لكل ألف مستند، وتعد قابلية القراءة أكثر أهمية من الأداء — فالتعبير $cond المتداخل الذي يحتوي على 100 فرع يصعب صيانته مقارنةً بـ $switch، وفي هذه الحالة، تفوق مزايا قابلية القراءة التي يوفرها $switch تكلفة الأداء بكثير.

(1) العامل الثلاثي $cond

دليل اختيار التعبيرات الشرطية: معايير الاختيار بين الأنواع الثلاثة للتعبيرات الشرطية — يُعد $cond مناسبًا للشروط ذات الفرعين (على سبيل المثال، "price >= 1000 → expensive/cheap")؛ وصيغته موجزة، لكن التداخل يجعل قراءته صعبة؛ في حين أن عبارات $cond المتداخلة يمكنها نظريًا دعم فروع متعددة، إلا أنها تصبح صعبة الصيانة عند تجاوز ثلاثة مستويات؛ يُعد $switch الخيار الأفضل للفروع التي يزيد عددها عن ثلاثة (على سبيل المثال، ربط حالة الطلب بالملصقات الصينية)، حيث إن المصفوفة branches واضحة وسهلة القراءة، كما أن default تتعامل مع الحالات غير المطابقة؛ تم تصميم $ifNull خصيصًا للتعامل مع عمليات استبدال القيم الفارغة (على سبيل المثال، استخدام username عندما يكون nickname فارغًا)، وهو أداة أساسية للبرمجة الدفاعية.

JAVASCRIPT
// === Similar if-else ===
db.products.aggregate([
  {
    $project: {
      title: 1,
      priceLevel: {
        $cond: {
          if: { $gte: ['$price', 1000] },
          then: 'expensive',
          else: {
            $cond: {
              if: { $gte: ['$price', 100] },
              then: 'medium',
              else: 'cheap'
            }
          }
        }
      }
    }
  }
]);

(2) $ifNull: التعامل مع القيم الفارغة

حالات الاستخدام الشائعة لـ $ifNull: هناك ثلاثة سيناريوهات نموذجية لاستخدام $ifNull في تحليل البيانات — 1. الاستعاضة بالقيمة الافتراضية: $ifNull: ['$nickname', '$username'] استخدام اسم المستخدم عندما لا يكون اسم العرض موجودًا؛ 2. حماية الحسابات: $ifNull: ['$discount', 0] معاملة الخصم الذي قيمته null على أنه 0 (لمنع تضمين القيمة null في العمليات الحسابية وتسببها في نتيجة null)؛ 3. تمييز البيانات المفقودة: $ifNull: ['$lastLoginAt', 'never'] تمييز المستخدمين الذين لم يسجلوا الدخول مطلقًا بعلامة «never». القاسم المشترك بين هذه السيناريوهات هو أن null يعمل بمثابة «ثقب أسود» للبيانات — أي عملية تتضمن null تُرجع null، و$ifNull هو «طريق الهروب» الوحيد.

مشكلة انتشار القيمة الفارغة: يعتبر انتشار القيمة الفارغة في مسارات التجميع في MongoDB أمرًا ضمنيًا وخطيرًا — فإذا احتوى أي تعبير على مدخلات فارغة، فإن التعبير بأكمله يُرجع قيمة فارغة. على سبيل المثال: $add: ['$price', '$tax']. إذا كانت tax قيمة فارغة، فإن النتيجة تكون قيمة فارغة بدلاً من price + 0. هذا ليس خطأً برمجياً بل سلوك قياسي لـ SQL (القيمة الفارغة + أي شيء = قيمة فارغة)، ومع ذلك، في تحليل البيانات، غالباً ما يؤدي ذلك إلى تحول عمود كامل من البيانات إلى قيمة فارغة. استراتيجيات الدفاع: 1. استخدم $ifNull في مرحلة $project/$addFields للتعامل مع جميع الحقول التي قد تكون null؛ 2. قم بتعيين قيم افتراضية عند إدراج البيانات (على سبيل المثال، tax: {type: Number, default: 0})؛ 3. استخدم $convert مع onNull للتعامل بشكل موحد مع القيم null أثناء تحويل الأنواع.

استكشاف أخطاء انتشار القيم الفارغة: خطوات استكشاف أخطاء القيم الفارغة غير المتوقعة في مخرجات مسار التجميع — 1. افحص مرحلةً تلو الأخرى: أضف $addFields واحدًا فقط في كل مرة لمعرفة المرحلة التي تظهر فيها القيمة الفارغة لأول مرة؛ 2. افحص الحقول المصدرية: استخدم $project لإخراج الحقول المشبوهة بشكل منفصل (على سبيل المثال، {tax: 1, price: 1}) للتأكد مما إذا كانت القيمة الأصلية هي null؛ 3. تحقق من سلسلة الحسابات: في عملية حسابية متسلسلة مثل $add$multiply$divide، إذا كانت أي خطوة null، فسوف تنتشر إلى جميع الخطوات اللاحقة؛ 4. ممارسة الترميز الدفاعي: قم بتغليف جميع التعبيرات الحسابية في $ifNull — على سبيل المثال، $add: [{$ifNull: ['$price', 0]}, {$ifNull: ['$tax', 0]}]. وبمجرد أن يصبح هذا عادة، نادرًا ما ستحدث مشكلات انتشار القيمة الفارغة.

JAVASCRIPT
// === Replace null/undefined ===
db.users.aggregate([
  {
    $project: {
      name: 1,
      displayName: {
        $ifNull: ['$nickname', '$username']  // nickname Use when empty username
      }
    }
  }
]);

(3) $switch: التفرع متعدد الشروط

ترتيب الفروع في $switch وأدائها: تقوم $switch بتقييم الفروع بالترتيب الذي تم إعلانها به، وتُرجع النتيجة عند أول فرع مطابق — ولذلك، يجب وضع الفرع الأكثر احتمالاً للمطابقة في المرتبة الأولى لتقليل عمليات تقييم الشروط غير الضرورية. بالمقارنة مع عبارات $cond المتداخلة، توفر $switch قابلية قراءة أفضل بكثير — فعندما يكون هناك ثلاثة فروع أو أكثر، تؤدي عبارات $cond المتداخلة إلى تباعد مفرط، في حين أن البنية المسطحة لـ $switch تكون واضحة على الفور. لا يمكن حذف الفرع default في $switch — فإذا لم تتطابق أي فرع ولم يكن هناك default، فسوف يطلق MongoDB خطأً.

تطبيقات $switch في تنقية البيانات: توجد ثلاثة استخدامات نموذجية لـ $switch في تنقية البيانات في عملية ETL — 1. تعيين الترقيم: تحويل الرموز الداخلية لقاعدة البيانات إلى تسميات قابلة للقراءة (الحالة: 'A' → 'نشط'، 'I' → 'غير نشط'، 'D' → 'محذوف')؛ 2. التصنيف إلى فئات: تجزئة القيم المستمرة (المبلغ < 100 → «صغير»، 100–1000 → «متوسط»، > 1000 → «كبير»؛ على غرار دالة $bucket ولكنها أكثر مرونة، حيث تتيح تحديد حدود وتسميات مخصصة)؛ 3. التقييم المركب متعدد الشروط: تعيين تصنيف بناءً على مزيج من حقول متعددة (معايير VIP: إنفاق > 10,000 ومسجل منذ > 1 سنة؛ معايير Gold: إنفاق > 1,000 أو مسجل منذ > 3 سنوات؛ وإلا، Bronze). إن مرونة $switch تجعلها أداة متعددة الاستخدامات لتحويل البيانات.

تطبيقات التعبيرات الشرطية في ETL: تُعد التعبيرات الشرطية أداة أساسية لتنقية البيانات في ETL—1. تصنيف البيانات: يقوم $switch بتحويل حالات الطلبات إلى تسميات تجارية (معلقة → قيد الدفع، مدفوعة → مدفوعة، مشحونة → مشحونة)؛ 2. معالجة الاستثناءات: يقوم $ifNull باستبدال الحقول المفقودة بقيم افتراضية، ويقوم $cond بتمييز القيم غير الطبيعية بعلامة «تتطلب مراجعة يدوية»؛ 3. إخفاء البيانات: تقوم $cond باستبدال الأرقام الأربعة الوسطى من رقم الهاتف بـ ** ({$concat: [{$substr: ['$phone', 0, 3]}, '**', {$substr: ['$phone', 7, 4]}]})؛ 4. القواعد التجارية: تقوم $switch بتعيين تصنيف فئة لمبلغ إنفاق المستخدم. في مسارات ETL، تُوضع التعبيرات الشرطية عادةً في مرحلة $addFields.

تحديد التعبير الشرطي الذي يجب استخدامه: الاختيار بين $cond و$switch و$ifNull — 1. استخدم $cond في حالة الاختيار الثنائي: عندما يكون هناك فرعان فقط (صحيح أو خطأ) (على سبيل المثال، isVIP: {$cond: [{$gte: ['$spend', 10000]}, true, false]}); 2. استخدم $switch للفروع المتعددة: 3 فروع شرطية أو أكثر (على سبيل المثال، تحديد المستوى، تعيين الحالة)؛ 3. استخدم $ifNull لمعالجة القيم الفارغة: لا يحتاج إلا إلى معالجة الحالات الفارغة/غير المحددة (على سبيل المثال، {$ifNull: ['$nickname', '$username']})؛ 4. استخدم $switch للشروط المتداخلة: تجنب تداخل $cond (فالتداخل لأكثر من ثلاثة مستويات يضعف قابلية القراءة بشكل كبير)؛ وبدلاً من ذلك، استخدم البنية المسطحة لـ $switch. قاعدة عامة — استخدم $cond لفرعين، و$switch لثلاثة فروع أو أكثر، و$ifNull لمعالجة القيم الفارغة.

JAVASCRIPT
// === Similar switch-case ===
db.orders.aggregate([
  {
    $project: {
      orderId: '$_id',
      statusLabel: {
        $switch: {
          branches: [
            { case: { $eq: ['$status', 'pending'] }, then: 'Payable' },
            { case: { $eq: ['$status', 'paid'] }, then: 'Paid' },
            { case: { $eq: ['$status', 'shipped'] }, then: 'Shipped' },
            { case: { $eq: ['$status', 'delivered'] }, then: 'Delivered' }
          ],
          default: 'Unknown Status'
        }
      }
    }
  }
]);


3. العمليات المتعلقة بالتاريخ

شرح المفهوم: تشكل العمليات المتعلقة بالتاريخ أساس تحليل بيانات السلاسل الزمنية. توفر MongoDB ثلاثة أنواع من عوامل التشغيل الخاصة بالتاريخ: (1) عوامل الاستخراج ($year/$month/$dayOfMonth/$hour، إلخ)، التي تستخرج المكونات الزمنية من حقل «التاريخ»؛ (2) عوامل التنسيق ($dateToString)، التي تحول حقل Date إلى سلسلة نصية بتنسيق محدد؛ (3) العوامل الحسابية ($add/$subtract)، التي تُجري عمليات الجمع والطرح على التواريخ.

كيفية العمل: يقوم عامل الاستخراج بقراءة المكونات المقابلة مباشرةً من نوع BSON Date، كما يدعم المعلمة timezone للتعامل مع المناطق الزمنية. يُخرج $dateToString سلسلة نصية باستخدام محددات التنسيق المشابهة لـ strftime. تستند عمليات التاريخ إلى طوابع زمنية بالميلي ثانية: يضيف $add عددًا من الميلي ثانية، ويحسب $subtract الفارق الزمني، ثم يقسمه على ثابت لتحويله إلى أيام أو ساعات.

اختيار استراتيجية المنطقة الزمنية: توجد استراتيجيتان للتعامل مع المناطق الزمنية في عمليات التاريخ — 1. توحيد التوقيت العالمي المنسق (UTC) في طبقة التخزين (موصى به): يتم تخزين جميع التواريخ بالتوقيت العالمي المنسق (UTC) وتحويلها إلى التوقيت المحلي باستخدام المعلمة timezone أثناء عمليات الاستعلام؛ 2. تضمين المنطقة الزمنية في طبقة التخزين: يسجل كل مستند حقل timezone، والذي يتم الرجوع إليه أثناء عمليات الاستعلام. مزايا الاستراتيجية 1: لا تحتاج شروط الاستعلام إلى مراعاة تحويلات المناطق الزمنية، وتكون مقارنات التواريخ مباشرة ودقيقة؛ ولا تتطلب الاستعلامات عبر المناطق الزمنية تحويلات معقدة للمناطق الزمنية. الاستراتيجية 2 مناسبة لسيناريوهات التدقيق التي تتطلب تسجيلًا دقيقًا لـ «متى وفي أي منطقة زمنية قام المستخدم بإجراء عملية ما».

تحسين فهرسة التاريخ: تعد الاستعلامات حسب النطاق الزمني (مثل «الطلبات خلال الأيام السبعة الماضية») أكثر استعلامات السلاسل الزمنية شيوعًا. النقاط الرئيسية للتحسين: 1. يتم فهرسة الحقل createdAt افتراضيًّا (timestamps: true + الفهرس الافتراضي)؛ 2. استخدم $gte و$lt لاستعلامات النطاق الزمني بدلاً من $where أو عمليات التجميع؛ 3. يغطي الفهرس المركب {status: 1, createdAt: -1} كلاً من «التصفية حسب الحالة» و«الفرز حسب التاريخ»؛ 4. لا يستخدم $dateToString الفهرس — فهو يحول Date إلى سلسلة نصية قبل مقارنتها. يجب عليك أولاً استخدام $match للتحقق من نطاق التاريخ، ثم استخدام $dateToString للتنسيق.

الأنماط الشائعة للإحصاءات المجمعة حسب التاريخ: يُعد التجميع حسب التاريخ جوهر تحليل السلاسل الزمنية — أي الإحصاءات المجمعة حسب اليوم أو الأسبوع أو الشهر أو الربع أو السنة. تصميم مفاتيح التجميع: 1. التجميع حسب الشهر: {_id: {year: {$year: '$date'}, month: {$month: '$date'}}}; 2. التجميع حسب الأسبوع: {_id: {year: {$year: '$date'}, week: {$week: '$date'}}}; 3. التجميع حسب اليوم: $dateToString: {format: '%Y-%m-%d', date: '$date'} كـ _id. ملاحظة: لا تستخدم طريقة $dateToString الفهارس، لكن صيغتها هي الأكثر إيجازًا. بالنسبة للتجميع الشهري والأسبوعي، يمكن لوظائف الاستخراج استخدام الفهارس المركبة لتصفية البيانات قبل التجميع.

تقنيات ملء التواريخ الناقصة: قد تفتقد النتائج الإحصائية المجمعة حسب اليوم إلى تواريخ معينة (على سبيل المثال، إذا لم تكن هناك أي طلبات في يوم معين، فلن تتضمن النتائج قيدًا لذلك التاريخ)، مما قد يتسبب في حدوث فجوات في الرسم البياني الخطي المتواصل الذي تعرضه الواجهة الأمامية—1. ملء البيانات على مستوى طبقة التطبيق: استخدم Node.js للتكرار عبر نطاق التواريخ وملء التواريخ المفقودة بقيمة 0 (الأكثر شيوعًا؛ كود موجز)؛ 2. مرحلة $densify (MongoDB 6.1+): {$densify: {field: 'date', range: {step: 1, unit: 'day', bounds: 'full'}}} تملأ التواريخ المفقودة تلقائيًا (حل خط أنابيب خالص، لا يتطلب معالجة على مستوى طبقة التطبيق)؛ 3. المجموعة المساعدة: إنشاء مجموعة calendar لتخزين جميع التواريخ مسبقًا، ثم استخدام $lookup لملء التواريخ المفقودة عبر عملية ربط (متوافقة مع الإصدارات الأقدم ولكنها تنطوي على تكاليف صيانة عالية). الخيار 2 هو الأكثر أناقة ولكنه يتطلب MongoDB 6.1 أو أحدث؛ أما الخيار 1 فهو الأكثر تنوعًا.

أفضل الممارسات لاستعلامات النطاق الزمني: الطريقة الصحيحة للاستعلام عن «آخر N أيام» — 1. استخدم $gte + new Date(Date.now() - N86400000) (يُحسب في JavaScript، ثم يتم تمرير كائن Date إلى MongoDB)؛ 2. استخدم $gte + ISODate() (صيغة Mongo shell)؛ 3. استخدم $match + $expr + $gte: ['$createdAt', {$subtract: ['$$NOW', N86400000]}] في مسار التجميع (تعبير مسار خالص؛ يشير $$NOW إلى الوقت الحالي للخادم). الخيار 3 هو الأكثر مرونة — فهو لا يعتمد على حسابات الوقت على مستوى التطبيق ويكون مستقلاً بذاته داخل مسار التجميع.

100%
graph LR
    A[Date Field<br/>2026-07-01T10:30:00Z] --> B[$year → 2026]
    A --> C[$month → 7]
    A --> D[$dayOfMonth → 1]
    A --> E[$hour → 10]
    
    A --> F["$dateToString<br/>%Y-%m-%d → '2026-07-01'"]
    
    A --> G["$add[date, 7*86400000]<br/>→ 2026-07-08"]
    A --> H["$subtract[now, date]<br/>→ Difference in the number of days"]
    
    style B fill:#d4edda
    style F fill:#cce5ff
    style G fill:#fff3cd

مشكلات الدقة في حسابات التواريخ: $subtract عند حساب الفرق بين التواريخ، تُعرض النتيجة بالميلي ثانية. وقد يؤدي قسمة هذه القيمة على 86,400,000 لتحويلها إلى أيام إلى مشكلات في الدقة — حيث قد تؤدي تغييرات التوقيت الصيفي إلى أن يكون اليوم أقل من 24 ساعة بالضبط. لإجراء حسابات دقيقة، استخدم $dateToString لاستخراج جزء التاريخ ومقارنته، أو استخدم الفرق في $dayOfYear للحصول على قيمة تقريبية. في معظم السيناريوهات التجارية، تكون دقة الميلي ثانية كافية، ولكن يتطلب الأمر اهتمامًا خاصًا في الأنظمة المالية وأنظمة الوقت والحضور.

الأخطاء الشائعة في معالجة التواريخ: 1. الخلط بين المناطق الزمنية — تخزين البيانات بتوقيت UTC مع نسيان تحويلها عند عرض التوقيت المحلي؛ 2. تبدأ الأشهر من 1، لكن JavaScript Date تبدأ من 0 (تُرجع MongoDB $month القيم من 1 إلى 12، وهو ما يختلف عن JavaScript)؛ 3. $dayOfWeek تُرجع 1 ليوم الأحد (وفقًا للمعايير الأمريكية؛ وقد يتوقع المستخدمون الصينيون أن يكون 1 هو يوم الاثنين)؛ 4. تُرجع $week رقم الأسبوع ضمن السنة، لكن الدول المختلفة لديها تعريفات مختلفة لليوم الذي يبدأ فيه الأسبوع (تحدد ISO 8601 يوم الاثنين كبداية للأسبوع، بينما يكون الإعداد الافتراضي في MongoDB هو يوم الأحد)؛ 5. $add، عند إضافة الميلي ثانية، لا تأخذ في الحسبان الثواني الكبيسة (لا يؤثر هذا على الغالبية العظمى من التطبيقات).

تحسين أداء عمليات التاريخ: لا تستخدم عمليات التاريخ الفهارس في مسار التجميع — حيث تقوم دوال الاستخراج مثل $year و$month أولاً بتحويل التاريخ إلى قيمة رقمية قبل إجراء المقارنات، وبالتالي لا يمكنها الاستفادة من فهرس شجرة B في حقل التاريخ. ولذلك: 1. بالنسبة للاستعلامات حسب النطاق الزمني، استخدم $gte/$lt (التي تقارن مباشرةً في حقل التاريخ وتستخدم الفهرس) بدلاً من $month === 7؛ 2. عند التجميع حسب الشهر، قم بالتصفية باستخدام {$gte: startOfMonth, $lt: startOfNextMonth} قبل استدعاء $group؛ 3. استخدم $dateToString فقط خلال مرحلة الإخراج النهائية (للتنسيق)، وليس خلال مرحلة التصفية.

ملخص لأفضل الممارسات المتعلقة بالتواريخ: القواعد الذهبية لعمليات التواريخ في MongoDB — 1. احرص دائمًا على تخزين التواريخ بتوقيت UTC (لتجنب الالتباس بين المناطق الزمنية؛ وقم بالتحويل إلى المنطقة الزمنية المطلوبة على مستوى التطبيق أو باستخدام المعلمة timezone في $dateToString عند العرض)؛ 2. استخدم دائمًا $gte/$lt في الاستعلامات (لاستخدام الفهارس؛ وتجنب التصفية باستخدام دوال الاستخراج مثل $month أو $year)؛ 3. استخدم $dateToString أو $dateFromParts لإنشاء مفاتيح المجموعات (على سبيل المثال، "$dateToString: {format: '%Y-%m', date: '$createdAt'}" للتجميع حسب الشهر)؛ 4. قم بالتنسيق فقط في طبقة الإخراج (كخطوة أخيرة في $project/$addFields)؛ 5. حدد المنطقة الزمنية في المعلمة timezone الخاصة بـ $dateToString (بتنسيق IANA، على سبيل المثال، 'Asia/Shanghai')؛ لا تقم بتعديل قيمة الساعة يدويًّا في طبقة التطبيق (فالتوقيت الصيفي سيؤدي إلى تغيير هذا التعديل).

(1) استخراج التاريخ

استراتيجية التعامل مع المناطق الزمنية: يخزن MongoDB التواريخ كطوابع زمنية بتوقيت UTC (دون معلومات عن المنطقة الزمنية)، وتُرجع عوامل استخراج التاريخ قيمًا بتوقيت UTC بشكل افتراضي. بالنسبة للحالات التي تتطلب التوقيت المحلي: 1. يمكن للمعلمة timezone التابعة لـ $dateToString (على سبيل المثال، 'Asia/Tokyo') إخراج سلسلة نصية للتوقيت المحلي مباشرةً؛ 2. تدعم عوامل الاستخراج مثل $hour و$month أيضًا المعلمة timezone؛ 3. يوفر إجراء تحويل المنطقة الزمنية على مستوى طبقة التطبيق مرونة أكبر، ولكنه يزيد من حجم الكود. التوصية في بيئة الإنتاج: قم بتخزين البيانات بشكل موحد بتوقيت UTC، وقم بإجراء التحويل باستخدام $dateToString أو على مستوى طبقة التطبيق عند عرضها.

المشاكل الشائعة في التعامل مع المناطق الزمنية: تُعد المناطق الزمنية المجال الأكثر عرضة للأخطاء في مسارات التجميع — 1. مشكلة التوقيت الصيفي (DST): تطبق الولايات المتحدة وأوروبا التوقيت الصيفي، لذا يختلف الفارق الزمني عن التوقيت العالمي المنسق (UTC) لنفس المدينة باختلاف الشهر (نيويورك تكون UTC-5 خلال التوقيت العادي وUTC-4 خلال التوقيت الصيفي). تتعامل المعلمة timezone في MongoDB تلقائيًا مع التوقيت الصيفي، لكن الحسابات اليدوية للفارق الزمني قد تؤدي إلى أخطاء؛ 2. مشكلة التجميع عبر الأيام: الساعة 02:00 من يوم 1 يونيو 2026 (UTC+8) تقابل الساعة 18:00 من يوم 31 مايو 2026 (UTC). استخدام $dayOfMonth دون تحديد منطقة زمنية سيؤدي إلى تجميع التاريخ على أنه اليوم 31 بدلاً من اليوم الأول؛ 3. مخاطر الثانية الكبيسة/السنة الكبيسة: عند إضافة شهر باستخدام $dateAdd، فإن 31 يناير + شهر واحد = 28 فبراير (وليس 3 مارس)؛ تتعامل MongoDB مع هذا الأمر تلقائيًا؛ 4. تحديثات قاعدة بيانات المناطق الزمنية: تتضمن MongoDB قاعدة بيانات مدمجة للمناطق الزمنية من IANA، لكن الإصدارات الأقدم قد تفتقر إلى أحدث قواعد المناطق الزمنية، لذا تحتاج إلى ترقية MongoDB بانتظام.

أفضل الممارسات لعمليات التواريخ: 1. استخدم $gte/$lt لاستعلامات نطاق التواريخ بدلاً من دوال الاستخراج مثل $dayOfMonth (حيث يمكن للأول استخدام الفهارس، بينما لا يمكن للثاني ذلك)؛ 2. عند التجميع حسب الشهر، استخدم {_id: {year: {$year: '$date'}, month: {$month: '$date'}}} بدلاً من $dateToString (الأول يمكنه الاستفادة من الفهارس)؛ 3. تكون حسابات التواريخ (إضافة أو طرح الأيام) أكثر كفاءة في مسار التجميع منها في طبقة التطبيق (لتجنب نقل كميات كبيرة من حقول التواريخ).

JAVASCRIPT
// === $year / $month / $dayOfWeek / $hour ===
db.orders.aggregate([
  {
    $project: {
      year: { $year: '$createdAt' },
      month: { $month: '$createdAt' },
      day: { $dayOfMonth: '$createdAt' },
      weekday: { $dayOfWeek: '$createdAt' },  // 1=Sunday
      hour: { $hour: '$createdAt' }
    }
  }
]);

(2) تنسيق التاريخ

مرجع سريع لمحددات التنسيق في $dateToString: تستخدم $dateToString محددات التنسيق على غرار strftime — %Y (السنة بأربعة أرقام)، %m (الشهر برقمين)، %d (اليوم برقمين)، %H (الساعة بنظام 24 ساعة)، %M (الدقائق)، %S (الثواني)، %L (الميلي ثانية)، %j (اليوم من السنة)، %U (الأسبوع من السنة). التركيبات الشائعة: '%Y-%m-%d' (مفتاح التاريخ)، '%Y-%m' (مفتاح الشهر)، '%Y-W%U' (مفتاح الأسبوع)، '%Y-%m-%d %H:00' (مفتاح الساعة). يقبل معلمة المنطقة الزمنية تنسيق أولسن (على سبيل المثال، 'Asia/Shanghai') أو فارق التوقيت عن التوقيت العالمي المنسق (UTC) (على سبيل المثال، '+08:00').

التطبيقات النموذجية لتنسيق التاريخ: الاستخدام الأكثر شيوعًا لتنسيق التاريخ هو تجميع الإحصائيات حسب الوقت — باستخدام $dateToString لإنشاء مفاتيح التاريخ، ثم استخدام $group للتجميع وفقًا لتلك المفاتيح. على سبيل المثال: تتبع اتجاهات المبيعات حسب الشهر ($dateToString$group$sort)، وتحديد ذروة حركة المرور حسب الساعة، وقياس معدل الاحتفاظ بالمستخدمين حسب الأسبوع. تحسين المفاتيح: إذا كنت بحاجة إلى التجميع حسب الشهر فقط، فإن استخدام {year: {$year}, month: {$month}} أكثر كفاءة من $dateToString (حيث يمكن للأول استخدام الفهارس، بينما يقوم الثاني بتحويل التاريخ إلى سلسلة نصية ولا يمكنه استخدام الفهارس).

JAVASCRIPT
// === $dateToString Date Format ===
db.orders.aggregate([
  {
    $project: {
      orderDate: {
        $dateToString: {
          format: '%Y-%m-%d %H:%M:%S',
          date: '$createdAt',
          timezone: 'Asia/Tokyo'
        }
      }
    }
  }
]);
// { orderDate: '2026-07-01 10:30:00' }
محدد التنسيق المعنى
%Y السنة مكونة من 4 أرقام
%m الشهر مكون من رقمين
%d تاريخ مكون من رقمين
%H نظام التوقيت على مدار 24 ساعة
%M محضر الاجتماع
%S ثوانٍ

(3) عمليات التواريخ

سيناريوهات الأعمال لحسابات التواريخ: تعد حسابات التواريخ شائعة للغاية في أنظمة التجارة الإلكترونية وأنظمة SaaS — 1. التحقق من انتهاء صلاحية الطلبات: يحسب $add[createdAt, 3086400000] الموعد النهائي للدفع؛ ويتم إلغاء الطلبات تلقائيًا إذا تم تجاوز هذا التاريخ؛ 2. تحليل نشاط المستخدم: يحسب $subtract[now, lastLoginAt] عدد الأيام منذ آخر تسجيل دخول؛ ويتم تصنيف المستخدمين الذين مر أكثر من 30 يومًا منذ آخر تسجيل دخول لهم على أنهم غير نشطين؛ 3. تذكيرات تجديد الاشتراك: $subtract[expiryDate, now] تحسب عدد الأيام المتبقية؛ إذا كان العدد أقل من 7 أيام، يتم إرسال تذكير؛ 4. نوافذ زمنية لإعداد التقارير: $match {createdAt: {$gte: $add[now, -3086400000]}} تسترد البيانات من آخر 30 يومًا.

تحويل وحدات الوقت في عمليات التاريخ: الوحدة الأساسية لعمليات التاريخ في MongoDB هي الميلي ثانية — 1 يوم = 86,400,000 ميلي ثانية، 1 ساعة = 3,600,000 ميلي ثانية. أخطاء التحويل الشائعة: 1. نسيان الضرب في 1,000 (استخدام 86,400 بدلاً من 86,400,000)، مما يؤدي إلى اختلاف بمقدار 1,000 ضعف؛ 2. استخدام 30*86,400,000 لتمثيل «30 يومًا» هو تقريب (نظرًا لأن عدد الأيام يختلف حسب الشهر)؛ ولحسابات التواريخ الدقيقة، استخدم $dateAdd (MongoDB 5.0+) بدلاً من $add؛ 3. مشكلة المنطقة الزمنية: تعمل العوامل مثل $hour و$dayOfMonth افتراضيًا وفقًا لتوقيت UTC؛ لاستخدام التوقيت المحلي، أضف معلمة المنطقة الزمنية (على سبيل المثال، {$hour: {date: '$createdAt', timezone: 'Asia/Shanghai'}}). تعد حسابات الوقت مصدرًا شائعًا للأخطاء — تأكد من التحقق من صحة منطق التواريخ المهمة باستخدام اختبارات الوحدة.

JAVASCRIPT
// === $add / $subtract Date Addition and Subtraction ===
db.orders.aggregate([
  {
    $project: {
      createdAt: 1,
      expiryDate: { $add: ['$createdAt', 7 * 24 * 60 * 60 * 1000] },  // Add 7 days
      daysSinceCreated: {
        $divide: [
          { $subtract: [new Date(), '$createdAt'] },
          1000 * 60 * 60 * 24
        ]
      }
    }
  }
]);


4. تحويل الأنواع

شرح المفهوم: MongoDB هي قاعدة بيانات ذات أنواع بيانات ضعيفة؛ فقد تحتوي الحقول داخل نفس المجموعة على أنواع بيانات مختلفة (على سبيل المثال، قد يكون price هو String أو Number). يوفر مسار التجميع عوامل تحويل مثل $toString/$toInt/$toLong/$toDouble/$toDecimal/$toDate/$toBool لحل حالات عدم اتساق أنواع البيانات. يوفر $convert طريقة تحويل أكثر أمانًا (تسمح لك بتحديد معالجة onError).

كيفية العمل: يقوم عامل تحويل النوع بإجراء عمليات التحويل على قيم الحقول في كل مستند. $toInt('42') → 42، $toString(3.14) → '3.14'. إذا فشل التحويل، فإن $toXxx تُرجع القيمة null وتصدر تحذيرًا؛ ويمكن استخدام $convert لتحديد أن onError تُرجع قيمة مخصصة. استخدم التحويلات في $project أو $addFields لضمان اتساق أنواع البيانات في المراحل اللاحقة.

تحديات الأنظمة ذات الأنواع الضعيفة: يُعدّ النوع الضعيف في MongoDB سيفًا ذا حدين — فهو يوفر المرونة (لا حاجة إلى تحديد الأنواع مسبقًا) ولكنه ينطوي أيضًا على مخاطر (قد يحتوي الحقل نفسه على أنواع بيانات مختلفة). المشكلات الشائعة: 1. عند ترحيل البيانات القديمة، قد يكون price عبارة عن السلسلة "599" أو الرقم 599؛ 2. عندما يصادف $avg في $group حقلًا نصيًّا، فإنه يُرجع null بدلاً من إصدار خطأ؛ 3. نتائج الفرز التي تحتوي على أنواع مختلطة في $sort غير متوقعة (يتم تحديد الترتيب من خلال مقارنة أنواع BSON). الحل: استخدم $convert في الخطوة الأولى من مسار التجميع لتوحيد الأنواع، أو فرض قيود الأنواع بصرامة على مستوى مخطط Mongoose.

وضع التحويل الآمن في $convert: يُعد $convert أكثر أمانًا من $toXxx — فهو يدعم المعلمتين onError و onNull، مما يتيح لك إرجاع قيمة افتراضية مخصصة بدلاً من null في حالة فشل التحويل. النمط الموصى به: $convert({input: '$price', to: 'decimal', onError: NumberDecimal('0'), onNull: NumberDecimal('0')}). بهذه الطريقة، حتى لو كانت price سلسلة غير رقمية أو null، يمكن لمسار التجميع أن يستمر في التنفيذ دون التسبب في مشكلات انتشار القيمة null. يجب أن تستخدم مسارات التجميع في بيئة الإنتاج دائمًا $convert بدلاً من $toXxx.

ملخص لأفضل الممارسات في تحويل الأنواع: 1. استخدم دائمًا $convert + onError بدلاً من $toXxx وحده (لمعالجة الأخطاء)؛ 2. قم بتوحيد الأنواع في بداية مسار المعالجة ($addFields + $convert) لضمان بقاء أنواع البيانات متسقة طوال المراحل اللاحقة؛ 3. استخدم $type للتحقق من أنواع الحقول وتطبيق منطق تحويل مختلف لكل نوع ($switch + $type)؛ 4. استخدم Decimal128 لحسابات العملات، وDouble للحسابات العلمية، وInt32 للعد؛ 5. قم بتحويل التواريخ النصية إلى Date باستخدام $toDate (تنسيق ISO 8601)؛ بالنسبة للتنسيقات غير القياسية، استخدم $dateFromString أولاً أو قم بإجراء معالجة مسبقة على مستوى طبقة التطبيق.

نصائح لتصحيح أخطاء تحويل الأنواع: يعد تصحيح أخطاء الأنواع في مسارات التجميع المهمة الأكثر استهلاكًا للوقت — 1. استخدم عامل $type للتحقق من أنواع الحقول: $addFields: {priceType: {$type: '$price'}} لمعرفة نوع الحقل price في كل مستند؛ 2. استخدم $cond + $type للتحويلات الشرطية: $switch: {branches: [{case: {$eq: [{$type: '$price'}, 'string']}, then: {$toDecimal: '$price'}}, {case: {$eq: [{$type: '$price'}, 'double']}, then: {$toDecimal: '$price'}}], default: '$price'}; 3. اعرض الناتج خطوة بخطوة في Compass لتحديد مكان حدوث عدم تطابق الأنواع. الوقاية خير من العلاج — قم بفرض قيود الأنواع بصرامة في مخطط Mongoose لتجنب مشكلات الكتابة الضعيفة في المصدر.

عامل التحويل المدخلات → المخرجات السيناريوهات النموذجية
$toString أي → سلسلة تحويل القيم الرقمية إلى سلاسل لعرضها
$toInt سلسلة/رقم→Int32 تحويل السلسلة «price» إلى عدد صحيح
$toLong سلسلة/رقم → Int64 تحويل معرّف الأرقام الكبيرة
$toDouble سلسلة/رقم → Double حساب دقيق
$toDecimal سلسلة/رقم→Decimal128 حسابات العملات الدقيقة
$toDate سلسلة/رقم → تاريخ تاريخ (سلسلة) إلى تاريخ
$convert يمكن تحديد onError تحويل آمن (موصى به)

إرشادات السلامة الخاصة بتحويل الأنواع: تعني طبيعة MongoDB ذات الأنواع غير الصارمة أن حقلًا واحدًا قد يحتوي على أنواع متعددة — على سبيل المثال، قد يكون price من النوع String أو Number أو حتى Decimal128. تعمل عوامل تحويل الأنواع على حل هذا التضارب، ولكن يجب مراعاة إرشادات السلامة التالية: 1. ستُحدث عمليات التحويل الصارمة مثل $toInt و$toDouble خطأً فورًا عند مواجهة قيمة غير متوافقة؛ 2. يوفر $convert عند دمجه مع onError وonNull تحويلًا متسامحًا مع الأخطاء (موصى به)؛ 3. استخدم $type للتحقق من نوع الحقل قبل التحويل لتجنب التحويلات العشوائية؛ 4. يجب إتمام تنقية البيانات خلال مرحلة ETL؛ ويجب أن تكون عمليات التحويل في مسار التجميع بمثابة الملاذ الأخير.

مقارنة بين $convert و $toXxx:

البعد $toXxx $convert
الصيغة بسيطة معقدة إلى حد ما
معالجة الأخطاء مقاطعة الخطأ onError تُرجع قيمة مخصصة
التعامل مع القيم الفارغة إرجاع القيمة الفارغة تعيد الدالة onNull قيمة مخصصة
السيناريوهات الموصى بها البيانات التي تم التأكد من خلوها من الأخطاء التحمل للأعطال في بيئات الإنتاج

المشاكل الشائعة في تحويل الأنواع: 1. تُحدث الدالة $toInt('3.14') خطأً — يجب أولاً استخدام الدالة $toDouble، ثم $toInt؛ 2. تنجح الدالة $toDate('2026-07-01')، لكن الدالة $toDate('07/01/2026') تفشل — لا يتعرف MongoDB إلا على تنسيق ISO 8601؛ 3. تُرجع $toBool('false') القيمة true — فالسلاسل غير الفارغة تُعتبر قيمًا صحيحة؛ 4. تُرجع $toDouble(null) القيمة null بدلاً من 0 — حيث يؤدي انتشار القيمة null في العمليات الحسابية اللاحقة إلى تقييم التعبير بأكمله على أنه null. الاستراتيجية العامة لتجنب هذه الأخطاء الشائعة: استخدم أولاً $ifNull للتعامل مع القيم null، ثم استخدم $convert للتعامل مع تحويلات الأنواع.

JAVASCRIPT
// === Numeric Type Conversion ===
db.products.aggregate([
  {
    $project: {
      title: 1,
      price: 1,
      priceString: { $toString: '$price' },           // Decimal128 → string
      priceInt: { $toInt: '$price' },                  // → Int32
      priceLong: { $toLong: '$price' },                // → Int64
      priceDouble: { $toDouble: '$price' },            // → Double
      priceDecimal: { $toDecimal: '$price' }           // → Decimal128
    }
  }
]);

// === Date Conversion ===
db.products.aggregate([
  {
    $project: {
      title: 1,
      releaseDate: { $toDate: '$releaseDateStr' }     // string → Date
    }
  }
]);


5. عمليات السلاسل

اعتبارات الأداء الخاصة بعمليات السلاسل: تُنفَّذ عمليات السلاسل في مسار التجميع على أساس كل مستند على حدة، مما قد يشكل عنق زجاجة عند معالجة مجموعات البيانات الكبيرة. النقاط الرئيسية التي يجب ملاحظتها: 1. تقوم الدالة $substr باقتطاع السلاسل بالبايت؛ وقد يتم اقتطاع الأحرف متعددة البايتات بتنسيق UTF-8، مثل الأحرف الصينية — لذا تأكد من استخدام الدالة $substrCP لاقتطاع السلاسل بالحرف؛ 2. عند ربط عدد كبير من الحقول باستخدام $concat، انتبه إلى الطول الناتج (يقتصر طول سلاسل BSON على 16 ميغابايت)؛ 3. يمكن استخدام $regexMatch مع $filter لتنفيذ مرشحات المطابقة التقريبية؛ 4. يجب تنفيذ عمليات السلاسل بعد $match (قم بالتصفية أولاً لتقليل عبء المعالجة).

الأخطاء الشائعة في معالجة السلاسل: 1. $concat والقيمة null: تُرجع دالة $concat القيمة null إذا كانت أي معلمة من معلماتها null — لذا يجب تغليف كل معلمة في $ifNull ({$concat: [{$ifNull: ['$firstName', '']}, ' ', {$ifNull: ['$lastName', '']}]}); 2. $toLower/$toUpper ودعم اللغات المتعددة: تتعامل هذه العوامل مع أحرف ASCII فقط؛ ولا تتأثر اللغات الصينية واليابانية والكورية (نظرًا لعدم وجود تمييز بين الأحرف الكبيرة والصغيرة فيها)، لكن تحويل الحرف الألماني ß إلى SS غير مدعوم؛ 3. $split والسلاسل الفارغة: تُرجع دالة $split: ['', ','] مصفوفة فارغة [] (بدلاً من [''])، وهو ما يختلف عن سلوك دالة ''.split(',') في JavaScript؛ 4. $replaceOne مقابل $replaceAll: تستخدم MongoDB 4.4+ $replaceOne (يستبدل أول مطابقة) و$replaceAll (يستبدل جميع المطابقات). لاحظ أن هذه العوامل غير متوفرة في الإصدارات الأقدم.

الفرق الرئيسي بين $substr و$substrCP: تستخرج $substr النص بناءً على إزاحات البايتات، بينما تستخرج $substrCP النص بناءً على إزاحات الأحرف — وهما متكافئتان بالنسبة للنص ASCII الخالص، ولكن نظرًا لأن أحرف CJK والرموز التعبيرية والحروف المُشَدَّدة هي أحرف متعددة البايتات، فقد تقطع $substr نصف حرف وتؤدي إلى تشويه النص. على سبيل المثال: بالنسبة لكلمة «café»، عند استخدام $substr: ['$text', 0, 4] لاستخراج أول 4 بايت، ينتج عن ذلك نص مشوه (نظرًا لأن الحرف «é» يشغل بايتين في UTF-8، فإن $substr ستقسمه إلى نصفين)؛ أما عند استخدام $substrCP: ['$text', 0, 4] لاستخراج أول 4 أحرف، فإن النتيجة الصحيحة هي «café». القاعدة: إذا كان الحقل يحتوي على أحرف غير ASCII، فاستخدم دائمًا $substrCP.

كيفية استخدام $regexMatch: تقوم $regexMatch بمطابقة التعبيرات النمطية في مسار التجميع — حيث تُرجع قيمة منطقية، وغالبًا ما تُستخدم بالاقتران مع $filter لتصفية عناصر المصفوفة. على سبيل المثال: تصفية العلامات في مصفوفة تبدأ بـ "mongo" — $filter: {input: '$tags', cond: {$regexMatch: {input: '$$this', regex: '^mongo'}}}}. ملاحظة: لا تستخدم $regexMatch الفهارس — فهي تقوم بمطابقة كل قيمة على حدة، ويتناسب الأداء خطيًا مع حجم البيانات. للبحث باستخدام التعبيرات العادية في مجموعات البيانات الكبيرة، استخدم $match (التي يمكنها استخدام الفهارس) أولاً، ثم $regexMatch.

التعامل مع القيم NULL في $concat: عندما تصادف $concat قيمة إدخال NULL، فإن التعبير بأكمله يُرجع NULL — وهذا ليس خطأً، بل هو السلوك القياسي لانتشار القيمة NULL في لغة SQL. على سبيل المثال: $concat: ['$firstName', ' ', '$lastName']. إذا كانت قيمة lastName هي NULL، فإن النتيجة بأكملها تكون NULL بدلاً من "John NULL". الحل: 1. قم بتغليف كل حقل قد يكون NULL باستخدام $ifNull ($concat: [$ifNull: ['$firstName', ''], ' ', $ifNull: ['$lastName', '']]); 2. استبدل التحويل الضمني لـ $concat بـ $convert + onError؛ 3. تأكد من أن الحقول النصية لها قيمة افتراضية (الافتراضي: '') عند إدراج البيانات في قاعدة البيانات. انتشار القيمة null هو السبب الأكثر شيوعًا لـ "نتائج null غير متوقعة" في العمليات النصية.

الجمع بين $split و$arrayElemAt: تقوم الدالة $split بتقسيم سلسلة إلى مصفوفة بناءً على فاصل، بينما تسترد الدالة $arrayElemAt عنصرًا بناءً على رقمه الترتيبي — ويتيح الجمع بينهما «استخراج جزء معين من سلسلة». أمثلة شائعة: 1. استخراج نطاق البريد الإلكتروني من عنوان بريد إلكتروني — $arrayElemAt: [$split: ['$email', '@'], 1] يسترد الجزء الذي يلي الرمز @؛ 2. استخراج اسم — $arrayElemAt: [$split: ['$fullName', ' '], 0] يسترد الجزء الذي يسبق المسافة؛ 3. تحليل مسار — $arrayElemAt: [$split: ['$path', '/'], -1] يسترد الجزء الأخير من المسار. لاحظ أن $split تُرجع null في حالة المدخلات التي تساوي null (يتطلب حماية $ifNull) وتُرجع [''] في حالة السلسلة الفارغة (يحتوي المصفوف على سلسلة فارغة واحدة، وليس مصفوفًا فارغًا).

التعامل مع القيم الفارغة باستخدام $concat: من العيوب الخطيرة في $concat أنه إذا كانت أي قيمة من المدخلات فارغة، فإن النتيجة بأكملها تعود فارغة. على سبيل المثال: $concat: ['$firstName', ' ', '$lastName']. إذا كانت lastName فارغة، فإن النتيجة تكون فارغة بدلاً من "John null". الحل البديل: قم بتضمين كل مدخل قد يكون null بين $ifNull$concat: [$ifNull: ['$firstName', ''], ' ', $ifNull: ['$lastName', '']]. هذا النمط شائع للغاية عند ربط الحقول مثل العناوين والأسماء.

الجمع بين $split و$arrayElemAt: تقوم الدالة $split بتقسيم سلسلة نصية إلى مصفوفة، بينما تسترد الدالة $arrayElemAt العنصر الموجود في الفهرس المحدد — ويتيح لك الجمع بينهما «استخراج أجزاء من السلسلة». أمثلة كلاسيكية: 1. استخراج نطاق البريد الإلكتروني من عنوان بريد إلكتروني: $arrayElemAt: [{ $split: ['$email', '@'] }, 1] → "gmail.com"؛ 2. استخراج المسار من عنوان URL: $arrayElemAt: [{ $split: ['$url', '/'], 3 }]; 3. استخراج الاسم الأخير من الاسم الكامل: $arrayElemAt: [{ $split: ['$fullName', ' '] }, 0]. لا يدعم $split محددات التعبيرات العادية — إذا لم يكن المحدد ثابتًا، فيجب عليك أولاً استخدام $trim لإزالة المسافات الزائدة.

JAVASCRIPT
// === $substr String Truncation ===
db.users.aggregate([
  {
    $project: {
      email: 1,
      emailPrefix: { $substr: ['$email', 0, 5] }  // First 5 characters of email
    }
  }
]);

// === $concat String Concatenation ===
db.users.aggregate([
  {
    $project: {
      fullName: { $concat: ['$firstName', ' ', '$lastName'] }
    }
  }
]);

// === $toUpper / $toLower Uppercase and lowercase ===
db.users.aggregate([
  {
    $project: {
      usernameUpper: { $toUpper: '$username' }
    }
  }
]);


6. عمليات المصفوفات

دور عمليات المصفوفات في مسار التجميع: تُعد عمليات المصفوفات القدرة الأساسية التي تميز مسار التجميع في MongoDB عن لغة SQL — حيث إن عمليات SQL على مستوى الصفوف لا تستطيع التعامل مع المصفوفات المتداخلة، في حين تتيح عمليات $map و$filter و$reduce تحويل العناصر داخل المصفوفات وتصفيتها وتجميعها. وينبع ذلك مباشرةً من طبيعة المصفوفات المتداخلة في نموذج المستندات في MongoDB. $map تعادل تطبيق $project على كل عنصر من عناصر المصفوفة، و$filter تعادل تطبيق $match على المصفوفة، و$reduce تعادل تطبيق $group على المصفوفة — وإتقان هذه العوامل الثلاثة يعني إتقان «قدرات التجميع» على مستوى المصفوفات.

المخاطر والحلول البديلة لوظيفة $unwind: تقوم وظيفة $unwind بتقسيم مصفوفة إلى مستندات متعددة، بحيث يحتوي كل مستند على عنصر واحد من المصفوفة. المخاطر: 1. يؤدي تقسيم مصفوفة كبيرة إلى إنشاء عدد كبير من المستندات (على سبيل المثال، مصفوفة مكونة من 1,000 عنصر → 1,000 مستند)، مما يتسبب في تضخم البيانات في مسار المعالجة؛ 2. بعد التقسيم، يلزم استخدام $group لإعادة تجميع البيانات، مما يزيد من التعقيد؛ 3. بشكل افتراضي، تؤدي المصفوفات الفارغة إلى تجاهل المستند بأكمله (يتطلب preserveNullAndEmptyArrays: true). أفضل الممارسات: تجنب استخدام $unwind إذا كان من الممكن حل المشكلة باستخدام $map أو $filter؛ وعندما يكون استخدام $unwind ضروريًا، فاستخدم $group مباشرةً بعده لإعادة تنظيم البيانات.

التوافق بين $map و$filter و$reduce وأساليب المصفوفات في JavaScript: تتوافق عوامل المصفوفات في مسار التجميع بشكل واحد إلى واحد مع طرق المصفوفات في JavaScript—$map ↔ Array.map() (تحويل كل عنصر)، $filter ↔ Array.filter() (تصفية العناصر)، $reduce ↔ Array.reduce() (الحساب التراكمي)، $concatArrays ↔ [...a, ...b] (دمج المصفوفات)، و$reverseArray ↔ Array.reverse() (عكس الترتيب)، و$arrayElemAt ↔ Array[index] (استرداد القيمة حسب الفهرس)، و$size ↔ Array.length (طول المصفوفة). تتيح هذه المطابقة لمطوري الواجهة الأمامية التعرف بسرعة على عمليات المصفوفات في مسار التجميع.

المشاكل الشائعة في عمليات المصفوفات: هناك عدة مشاكل شائعة في عمليات المصفوفات — 1. يجب أن تُرجع المعلمة cond الخاصة بـ $filter قيمة منطقية (لن تقوم التعبيرات التي تُرجع null أو undefined بتصفية العنصر؛ بل سيتم الاحتفاظ به)؛ 2. يجب تحديد المعلمة initialValue الخاصة بـ $reduce (إذا لم يتم تحديدها، يُستخدم العنصر الأول من المصفوفة كقيمة أولية، ولكن إذا كانت المصفوفة فارغة، يتم إرجاع null)؛ 3. تُحسب الفهارس السالبة في $arrayElemAt من النهاية (–1 هو العنصر الأخير، و–2 هو قبل الأخير)، ولكن إذا كان الفهرس خارج النطاق، يتم إرجاع null؛ 4. تُحدث $size خطأً في حقول null أو undefined (تتطلب الحماية باستخدام $ifNull: ['$array', []])؛ 5. المعلمة as الخاصة بـ $map تُعيَّن افتراضيًّا إلى 'this'، لكن استخدام اسم مخصص يحسّن قابلية القراءة ($map: {input: '$tags', as: 'tag', in: {$toUpper: '$$tag'}}).

نطاق $$this و$$value: تستخدم $map/$filter/$reduce المتغير $$this للإشارة إلى العنصر الحالي في المصفوفة التي يتم تكرارها، بينما تستخدم $reduce المتغير $$value للإشارة إلى القيمة المتراكمة. لاحظ علامة الدولار المزدوجة — تشير $$ إلى متغيرات النظام ($$this، $$value، $$ROOT، $$DESCEND)، بينما تشير علامة $ المفردة إلى مرجع حقل. خطأ شائع: استخدام $field بدلاً من $$this.field في شرط $filter — يشير $field إلى الحقل الأعلى مستوى في المستند، بينما يشير $$this.field إلى حقل عنصر المصفوفة الحالي. فهم هذه الاختلافات في النطاق أمر أساسي لاستخدام مشغلات المصفوفات بشكل صحيح.

المشغل المدخلات المخرجات التغير في عدد المستندات
$map N مستندات N مستندات غير قابل للتغيير (تحويل داخلي للمصفوفة)
$filter N مستندات N مستندات دون تغيير (التصفية داخل المصفوفة)
$unwind N مستندات N×M مستندات التوسيع (تقسيم المصفوفات الفرعية)

الجمع بين $map و$filter: غالبًا ما تُستخدم $map و$filter معًا — استخدم أولاً $filter لاختيار عناصر المصفوفة المطلوبة، ثم استخدم $map لتحويل تنسيقها. على سبيل المثال: استخراج أسماء المنتجات الخاصة بالسلع التي تم شحنها فقط من طلب الشراء — $map: {input: {$filter: {input: '$items', cond: {$eq: ['$$this.status', 'shipped']}}}, in: '$$this.name'}. لاحظ الترتيب: قم بتطبيق filter قبل map (لتقليل عدد العناصر التي تتم معالجتها بواسطة map). يؤدي الترتيب العكسي (تطبيق map قبل filter) إلى انخفاض الأداء وقابلية القراءة.

$reduce الحساب التراكمي: تعمل دالة $reduce على اختزال مصفوفة إلى قيمة واحدة — الصيغة {input: array, initialValue: value, in: expression}. أمثلة شائعة: 1. جمع المصفوفة $reduce: {initialValue: 0, in: {$add: ['$$value', '$$this']}}; 2. تسلسل المصفوفة $reduce: {initialValue: '', in: {$concat: ['$$value', ',', '$$this']}}; 3. الحد الأقصى للمصفوفة $reduce: {القيمة_الأولية: 0, in: {$cond: [{$gt: ['$$this', '$$value']}, '$$this', '$$value']}}. $$value هي القيمة المتراكمة، و$$this هو العنصر الحالي.

100%
graph LR
    A["tags: ['5g','amoled','fast']"] --> B["$arrayElemAt: 0<br/>→ '5g'"]
    A --> C["$size<br/>→ 3"]
    A --> D["$map: {$toUpper}<br/>→ ['5G','AMOLED','FAST']"]
    A --> E["$filter: len >= 3<br/>→ ['amoled','fast']"]
    
    style B fill:#d4edda
    style C fill:#cce5ff
    style D fill:#fff3cd
    style E fill:#e2d5f1
المشغل الوظيفة المكافئ في JavaScript
$arrayElemAt استرداد عنصر باستخدام الرقم الترتيبي arr[index]
$size طول المصفوفة arr.length
$map التحويل عنصرًا بعنصر arr.map(fn)
$filter مرشح الحالة arr.filter(fn)
$reduce الحساب التراكمي arr.reduce(fn, init)
$concatArrays دمج المصفوفات [...a, ...b]
$reverseArray عكس مصفوفة arr.reverse()

المشاكل الشائعة المتعلقة بـ $size: لا يمكن استخدام $size إلا مع الحقول التي تمثل مصفوفات — فإذا لم يكن الحقل مصفوفة (على سبيل المثال، إذا كان قيمته null أو غير موجود)، فإن $size ستُحدث خطأً. الحلول البديلة: 1. أولاً، استخدم $ifNull: ['$tags', []] لتحويل القيمة null إلى مصفوفة فارغة؛ 2. استخدم $type: '$tags' للتحقق مما إذا كان مصفوفة قبل إجراء الحساب؛ 3. قم بتعيين default: [] في المخطط (لضمان أن يكون الحقل دائمًا مصفوفة). يُرجع $size عددًا صحيحًا ويمكن استخدامه مباشرةً في شروط $cond — على سبيل المثال، $cond: [{$gte: [{$size: '$tags'}, 3]}, 'Plenty of tags', 'Not enough tags'].

المؤشرات السالبة لـ $arrayElemAt: يدعم $arrayElemAt المؤشرات السالبة — حيث يسترد -1 العنصر الأخير، و-2 يسترد العنصر قبل الأخير، وهو ما يعمل بنفس طريقة عمل arr.at(-1) في JavaScript. الاستخدامات الشائعة: 1. استرداد السجل الأحدث {$arrayElemAt: ['$orders', -1]} (بافتراض أن الطلبات مرتبة حسب الوقت)؛ 2. استرداد العلامة الأولى: {$arrayElemAt: ['$tags', 0]}؛ 3. استرداد العنوان الأخير: {$arrayElemAt: ['$addresses', -1]}. إذا كان المؤشر خارج النطاق، فإن $arrayElemAt تُرجع القيمة null (دون إصدار خطأ)؛ وستحتاج إلى استخدام $ifNull كخيار بديل.

تسلسل عمليات المصفوفات: يمكن تسلسل عمليات $map و$filter و$reduce معًا — استخدم أولاً $filter للتصفية، ثم استخدم $map للتحويل، وأخيرًا استخدم $reduce للتجميع. على سبيل المثال: حساب السعر الإجمالي للسلع المشحونة — $reduce: {input: {$map: {input: {$filter: {input: '$items', cond: {$eq: ['$$this.status', 'shipped']}}}, in: '$$this.price'}}, initialValue: 0, in: {$add: ['$$value', '$$this']}}. لاحظ ترتيب التنفيذ: أولاً التصفية (لتقليل عدد العناصر التي تعالجها map)، ثم map (لاستخراج السعر)، وأخيرًا reduce (لجمع القيم).

تحسين الأداء للعمليات المتسلسلة: على الرغم من أن العمليات المتسلسلة على المصفوفات تتمتع بفعالية كبيرة، إلا أن كل مستوى من مستويات التداخل يزيد من الحمل الحسابي — 1. تحسين الترتيب: استخدم أولاً $filter (لتقليل حجم البيانات للعمليات اللاحقة)، ثم $map (لاستخراج الحقول الضرورية فقط)، وأخيرًا $reduce (لحساب النتيجة النهائية)؛ 2. تجنب الحسابات الزائدة: إذا كانت عدة عمليات متسلسلة تتطلب نفس النتيجة الوسيطة، فاستخدم $addFields لحسابها مرة واحدة ثم إعادة استخدامها؛ 3. ملاحظة بشأن المصفوفات الكبيرة: قد يستغرق تنفيذ $map + $filter + $reduce على مصفوفات تحتوي على أكثر من 1,000 عنصر وقتًا طويلاً؛ فكر أولاً في استخدام $unwind + $group كبديل ($unwind يعالج المصفوفات الكبيرة بشكل أسرع في طبقة قاعدة البيانات مقارنةً بـ $map في طبقة التعبيرات)؛ 4. أضف $match في مقدمة مسار المعالجة: قم بتطبيق عمليات المصفوفات فقط على المستندات المطلوبة (على سبيل المثال، قم بمعالجة مصفوفة items للطلبات المدفوعة فقط).

ثلاثة أوضاع للتجميع في دالة $reduce: تحدد المعلمة in الخاصة بـ $reduce طريقة التجميع — 1. التجميع العددي: in: {$add: ['$$value', '$$this']} (المجموع)، in: {$max: ['$$value', '$$this']} (القيمة القصوى)؛ 2. التراكم السلسلي: in: {$concat: ['$$value', ',', '$$this']} (التسلسل المفصول بفاصلة)؛ 3. التراكم الكائني: in: {$mergeObjects: ['$$value', '$$this']} (دمج خصائص الكائنات). تحدد المعلمة initialValue الخاصة بـ $reduce نوع القيمة الأولية ونوع المخرجات النهائية — القيمة الأولية الرقمية 0 → تُنتج Number، والقيمة الأولية السلسلة '' → تُنتج String، والقيمة الأولية للكائن {} → تُنتج Object. يجب أن يتطابق نوع القيمة الأولية مع نوع الإخراج للمعلمة in.

JAVASCRIPT
// === $arrayElemAt Retrieve an Element by Index ===
db.products.aggregate([
  {
    $project: {
      title: 1,
      firstTag: { $arrayElemAt: ['$tags', 0] },
      lastTag: { $arrayElemAt: ['$tags', -1] }
    }
  }
]);

// === $size Array Length ===
db.products.aggregate([
  {
    $project: {
      title: 1,
      tagCount: { $size: '$tags' }
    }
  }
]);

// === $map Array Transformations ===
db.products.aggregate([
  {
    $project: {
      title: 1,
      tagsUpper: {
        $map: {
          input: '$tags',
          as: 'tag',
          in: { $toUpper: '$$tag' }
        }
      }
    }
  }
]);

// === $filter Array Filtering ===
db.products.aggregate([
  {
    $project: {
      title: 1,
      expensiveTags: {
        $filter: {
          input: '$relatedProducts',
          as: 'product',
          cond: { $gte: ['$$product.price', 1000] }
        }
      }
    }
  }
]);


7. التدريب العملي الشامل

قائمة مراجعة لتحسين أداء مسارات التجميع: تتطلب مسارات التجميع في بيئات الإنتاج تحسينًا منهجيًّا — 1. ضع $match في البداية لتصفية البيانات مبكرًا وتقليل حجمها؛ 2. استخدم $project بعد $group لتبسيط الحقول وتقليل استهلاك الذاكرة؛ 3. استخدم $sort + $limit بدلاً من الفرز الكامل (وضع Top N)؛ 4. تجنب استخدام تعبيرات معقدة لـ _id في $group (مما يؤثر على كفاءة التجميع)؛ 5. اضبط allowDiskUse: true لمجموعات البيانات الكبيرة؛ 6. استخدم hint() لفرض استخدام الفهرس وتجنب المسح الكامل للمجموعات؛ 7. اتبع $unwind مباشرةً بـ $group لمنع تضخم البيانات؛ 8. تشترك خطوط الأنابيب الفرعية لـ $facet في المدخلات ولكنها تعمل بشكل مستقل؛ تحكم في عدد خطوط الأنابيب الفرعية.

نصائح لتصحيح أخطاء مسارات التجميع: تستخدم مسارات التجميع استدعاءات متسلسلة، مما يجعل النتائج الوسيطة غير مرئية ويصعب عملية تصحيح الأخطاء. ثلاث نصائح عملية: 1. قم بالتنفيذ مرحلةً تلو الأخرى — أضف مرحلة واحدة فقط في كل مرة وتحقق مما إذا كان الناتج يتطابق مع التوقعات؛ 2. استخدم $project للاحتفاظ بالحقول الرئيسية فقط، مما يقلل من التشويش في الناتج لتسهيل التحليل؛ 3. استخدم أداة إنشاء مسارات التجميع في Compass للتصحيح البصري. إذا كانت نتائج $group لا تتطابق مع التوقعات، فتحقق مما إذا كان _id صحيحًا — حيث تتضمن الأخطاء الأكثر شيوعًا أخطاء إملائية في أسماء الحقول أو علامات اقتباس مفقودة في _id.

التطبيقات التجارية لتقسيم المستخدمين وفق نموذج RFM: يُعد نموذج RFM (الحداثة/التكرار/القيمة المالية) النموذج الأكثر استخدامًا لتقسيم المستخدمين في مجال التجارة الإلكترونية — 1. الحداثة (الوقت المنقضي منذ آخر عملية شراء): كلما انخفضت قيمة daysSinceLastOrder، كان ذلك أفضل (فالمستخدمون الذين أجروا عملية شراء مؤخرًا هم أكثر عرضة للشراء مرة أخرى)؛ 2. التكرار (تواتر الشراء): كلما ارتفعت قيمة orderCount، كان ذلك أفضل (فالمستخدمون الذين يشترون بشكل متكرر هم عملاء مخلصون)؛ 3. القيمة المالية (مبلغ الإنفاق): كلما ارتفعت قيمة totalSpent، كان ذلك أفضل (فالمستخدمون ذوو الإنفاق المرتفع يساهمون بأغلبية الإيرادات). يتم تصنيف كل بُعد من أبعاد RFM الثلاثة إلى مستويات عالية ومنخفضة، مما ينتج عنه 8 أنواع من المستخدمين — حيث يمثل «R عالي، F عالي، M عالي» المستخدمين VIP الأكثر قيمة، بينما يمثل «R منخفض، F منخفض، M منخفض» المستخدمين الذين توقفوا عن الاستخدام والذين يحتاجون إلى إعادة تنشيط حساباتهم أو شطبهم. يقوم $switch بربط درجات RFM بعلامات المستخدمين ويعمل كمسار تجميع لتحليل RFM.

إعداد بيانات التقارير والتحقق من صحتها: يجب تصميم بيانات الاختبار الخاصة بتقارير المبيعات بعناية — 1. أن تغطي فترة زمنية طويلة بما يكفي (3 أشهر على الأقل؛ حيث تُعد بيانات الشهر السابق ضرورية لإجراء مقارنات شهرية)؛ 2. أن تتضمن قيمًا متطرفة (طلبات بقيمة 0 يوان، وطلبات تم استرداد قيمتها، لاختبار متانة تصفية $match)؛ 3. أن تكون موزعة بشكل معقول (العديد من الطلبات ذات القيمة الصغيرة + عدد قليل من الطلبات ذات القيمة الكبيرة، لمحاكاة توزيع الطلبات الحقيقية)؛ 4. أن تتضمن مجموعة متنوعة من الحالات (مدفوعة/معلقة/تم استرداد قيمتها، لاختبار فعالية تصفية $match: {status: 'paid'}). بعد إدراج البيانات الاختبارية بشكل مجمَّع باستخدام insertMany، تحقق أولاً من صحة البيانات باستخدام استعلام بسيط بـ find، ثم قم بتنفيذ مسار التجميع — فمشاكل البيانات أكثر شيوعًا من مشاكل مسار التجميع.

القيمة التجارية لتقارير المبيعات: تُعد تقارير المبيعات الشهرية البيانات الأساسية لعمليات التجارة الإلكترونية — فهي تجيب على ثلاثة أسئلة رئيسية: 1. ما هي الاتجاهات (هل الإيرادات في تزايد أم في انخفاض)؟؛ 2. ما هي الأسباب (ما هي الفئة أو المنتج الذي يدفع الأداء إلى الأمام أو يعيقه)؟؛ 3. كيف ينبغي لنا التعديل (في العروض الترويجية، أو المخزون، أو استراتيجيات اختيار المنتجات)؟ يُعد استخدام $setWindowFields + $shift لحساب النمو على أساس سنوي خطوة حاسمة في الارتقاء بالتقرير من مجرد «عرض البيانات» إلى «تقديم رؤى» — حيث إن معرفة أن «إيرادات هذا الشهر تبلغ 3,000» أقل قيمة من معرفة أن «الإيرادات زادت بنسبة 100% مقارنة بالشهر السابق».

مقارنة بين «Aggregation Pipes» وأدوات ذكاء الأعمال: «Aggregation Pipes» مقابل أدوات ذكاء الأعمال مثل Tableau وMetabase — «Aggregation Pipes» هي واجهات برمجية (مرنة، آلية، قابلة للتضمين في التطبيقات)، في حين أن أدوات ذكاء الأعمال هي واجهات مرئية (تعمل بالسحب والإفلات، ومناسبة للمستخدمين غير التقنيين، وتتيح الاستكشاف التفاعلي). أفضل الممارسات: 1. استخدم أدوات ذكاء الأعمال (المتصلة عبر موصل MongoDB BI Connector) للاستعلامات المخصصة التي يجريها موظفو العمليات؛ 2. استخدم خط أنابيب التجميع (Aggregate Pipeline) للتقارير الثابتة المدمجة في التطبيقات (أداء قابل للتحكم، ونتائج قابلة للتخزين المؤقت)؛ 3. استخدم Jupyter + PyMongo للتحليل المتعمق الذي يجريه علماء البيانات (نظام Python أكثر قوة). يُعد Aggregate Pipeline مناسبًا للاحتياجات التحليلية «المعروفة والمتكررة»، بينما تُعد أدوات BI مناسبة للاحتياجات التحليلية «المجهولة والاستكشافية».

(1) سيناريوهات الأعمال المعقدة

نموذج RFM لتقسيم المستخدمين: يُعد نموذج RFM (الحداثة/التكرار/القيمة المالية) نموذجًا كلاسيكيًّا لتقسيم مستخدمي التجارة الإلكترونية — حيث تشير «الحداثة» إلى عدد الأيام المنقضية منذ آخر عملية شراء، ويشير «التكرار» إلى وتيرة الشراء، بينما تشير «القيمة المالية» إلى المبلغ التراكمي للإنفاق. يتم تصنيف كل بعد من هذه الأبعاد الثلاثة إلى مستويات عالية ومتوسطة ومنخفضة، مما ينتج عنه 27 نوعًا مختلفًا من المستخدمين. الرؤى الرئيسية: R عالية، تكرار مرتفع، قيمة مرتفعة = مستخدمو VIP الأساسيون (يتطلبون أولوية في الاحتفاظ بهم)؛ حداثة منخفضة، تكرار مرتفع، قيمة مرتفعة = مستخدمون معرضون لخطر الانسحاب (يتطلبون إعادة التفاعل)؛ حداثة مرتفعة، تكرار منخفض، قيمة منخفضة = مستخدمون جدد (يتطلبون رعاية). تعد مسارات التجميع في MongoDB مناسبة تمامًا لحسابات RFM — حيث تقوم $group بتجميع الأبعاد الثلاثة حسب userId، وتقوم $switch بتعيين القيم إلى المستويات العالية أو المتوسطة أو المنخفضة.

التطبيقات التجارية لتقسيم المستخدمين: تؤدي نتائج تقسيم المستخدمين وفق نموذج RFM إلى وضع استراتيجيات تشغيلية متمايزة — 1. المستخدمون من فئة VIP: خصومات حصرية + شحن ذو أولوية + خدمة عملاء مخصصة لفئة VIP؛ 2. المستخدمون المعرضون لخطر الانسحاب: قسائم شراء موجهة + رسائل نصية لإعادة التفاعل + عروض خاصة لفترة محدودة؛ 3. المستخدمون الجدد: خصومات على الطلب الأول + إرشادات التمهيد + توصيات بالمنتجات؛ 4. المستخدمون غير النشطون: إعادة تنشيط منخفضة التكلفة (إشعارات فورية بدلاً من الرسائل النصية). تتوافق كل استراتيجية مع تكاليف تسويقية مختلفة — حيث يحقق المستخدمون من فئة VIP أعلى عائد على الاستثمار (تكاليف احتفاظ منخفضة)، يليهم المستخدمون الجدد، بينما يحقق المستخدمون غير النشطون أقل عائد.

JAVASCRIPT
// === Scene:User Segmentation Analysis ===
db.orders.aggregate([
  { $match: { status: 'paid' } },
  {
    $group: {
      _id: '$userId',
      totalSpent: { $sum: '$total' },
      orderCount: { $sum: 1 },
      avgOrderValue: { $avg: '$total' },
      firstOrderAt: { $min: '$createdAt' },
      lastOrderAt: { $max: '$createdAt' }
    }
  },
  {
    $addFields: {
      userLevel: {
        $switch: {
          branches: [
            { case: { $gte: ['$totalSpent', 10000] }, then: 'VIP' },
            { case: { $gte: ['$totalSpent', 1000] }, then: 'Gold' },
            { case: { $gte: ['$totalSpent', 100] }, then: 'Silver' }
          ],
          default: 'Bronze'
        }
      },
      daysSinceLastOrder: {
        $divide: [
          { $subtract: [new Date(), '$lastOrderAt'] },
          1000 * 60 * 60 * 24
        ]
      }
    }
  },
  { $sort: { totalSpent: -1 } },
  { $limit: 100 }
]);

(2) تقارير المبيعات

كيفية عمل دالة النافذة $setWindowFields: $setWindowFields (MongoDB 5.0+) هي دالة نافذة في مسار التجميع، تشبه جملة OVER() في لغة SQL. وهي تحسب القيم المجمعة المستندة إلى النافذة — مثل المتوسطات المتحركة، والمجاميع التراكمية، والقيم من الصف السابق أو التالي — لكل مستند دون تغيير عدد المستندات. $shift هي عملية الإزاحة في دوال النافذة؛ حيث يسترد $shift: {output: '$revenue', by: -1} قيمة الإيرادات من الصف السابق لحساب معدل النمو الشهري.

حساب النمو الشهري: لحساب النمو السنوي في التقارير الشهرية، تحتاج إلى بيانات من الشهر الحالي والشهر السابق — يستخدم SQL التقليدي دالة النافذة LAG()، بينما يحقق MongoDB الوظيفة المماثلة باستخدام $setWindowFields + $shift. صيغة الحساب: (الشهر الحالي - الشهر السابق) / الشهر السابق × 100%. لاحظ الحماية من القسمة على الصفر: عندما تكون قيمة الشهر السابق 0، يتم تعيين معدل النمو على 0 (يتم التحقق من ذلك باستخدام $cond).

الحالات الحدية لدوال النوافذ: عندما تستدعي دالة $shift الصف السابق للصف الأول، فإنها تُرجع القيمة null (نظرًا لعدم وجود صف سابق) — ولهذا السبب يتطلب حساب معدل النمو (growthRate) استخدام دالة $cond لمعالجة الحالات التي تكون فيها قيمة prevMonthRevenue هي null. وبالمثل، فإن استدعاء الصف التالي للصف الأخير يُرجع أيضًا القيمة null. شروط حدية أخرى لدوال النافذة: 1. النافذة الفارغة تُرجع القيمة null؛ 2. بالنسبة لنافذة مكونة من صف واحد، فإن $sum هي قيمة ذلك الصف؛ 3. يتصرف كل من $rank و$denseRank بشكل مختلف عند تعادل القيم (تتخطى دالة rank رقمًا، بينما لا تفعل دالة denseRank ذلك). إن فهم هذه الشروط الحدية أمر ضروري لاستخدام دوال النافذة بشكل صحيح.

البنية المتدرجة لنظام إعداد التقارير: يتألف نظام إعداد التقارير الكامل من ثلاث طبقات — 1. طبقة البيانات (مسار التجميع الذي يحسب المقاييس الأولية)؛ 2. طبقة التحليل (المقاييس المشتقة مثل المقارنة الشهرية والمقارنة السنوية والتصنيفات)؛ 3. طبقة العرض (التنسيق والرسوم البيانية والتصدير). في هذا المثال، يتم دمج طبقة البيانات وطبقة التحليل في مسار تجميع واحد، بينما تتولى الواجهة الأمامية إدارة طبقة العرض. وعادةً ما تتطلب التقارير في بيئة الإنتاج أيضًا: طبقة التخزين المؤقت (يتم تخزين بيانات التقارير مؤقتًا كل ساعة في Redis لتجنب الحسابات المكررة)، وطبقة الأذونات (تتيح الأدوار المختلفة عرض البيانات عبر أبعاد مختلفة)، وطبقة التدقيق (تسجيل من قام بعرض أي تقارير ومتى).

JAVASCRIPT
// === Monthly Sales Report(Including year-over-year)===
db.orders.aggregate([
  {
    $group: {
      _id: {
        year: { $year: '$createdAt' },
        month: { $month: '$createdAt' }
      },
      revenue: { $sum: '$total' },
      orderCount: { $sum: 1 },
      avgOrderValue: { $avg: '$total' }
    }
  },
  { $sort: { '_id.year': 1, '_id.month': 1 } },
  {
    $setWindowFields: {
      sortBy: { '_id.year': 1, '_id.month': 1 },
      output: {
        prevMonthRevenue: {
          $shift: {
            output: '$revenue',
            by: -1
          }
        }
      }
    }
  },
  {
    $addFields: {
      growthRate: {
        $cond: {
          if: { $gt: ['$prevMonthRevenue', 0] },
          then: {
            $divide: [
              { $subtract: ['$revenue', '$prevMonthRevenue'] },
              '$prevMonthRevenue'
            ]
          },
          else: 0
        }
      }
    }
  }
]);

تحليل متعمق لإدارة ذاكرة مسار المعالجة: تحتفظ كل مرحلة من مراحل مسار التجميع بتيار من المستندات في الذاكرة، مع حد افتراضي يبلغ 100 ميغابايت لكل مرحلة. إذا تم تجاوز هذا الحد، فإن MongoDB تُصدر خطأً وتُنهي مسار المعالجة، ما لم يتم تعيين allowDiskUse: true (مما يسمح بعمليات الكتابة المؤقتة على القرص). ومع ذلك، فإن هذا يطرح مشكلة جديدة — حيث تكون عمليات الإدخال/الإخراج (I/O) على القرص أبطأ بـ 100 مرة من الذاكرة، مما يتسبب في انخفاض أداء خط الأنابيب بشكل كبير. النهج الصحيح هو تحسين خط الأنابيب لمنع التجاوز: 1. استخدم $match في البداية لتقليل حجم المدخلات؛ 2. استخدم $project لتبسيط الحقول وتقليل المساحة التي يشغلها كل مستند؛ 3. تجنب العدد المفرط للقيم الفريدة لـ _id في $group؛ 4. انتبه إلى حدود حجم المصفوفات في $push/$addToSet.

قائمة مرجعية عملية لضبط أداء التجميع: 1. تأكد من أن $match الأول يمكنه استخدام فهرس (تحقق من ناتج explain)؛ 2. الترتيب المثالي هو وضع $match قبل $group و$project بعد $group؛ 3. يمكن تحسين $sort + $limit إلى وضع Top N (لا يتطلب سوى الاحتفاظ بمكدس مكون من N عنصرًا، بدلاً من إجراء فرز كامل)؛ 4. يمكن تقديم $match الذي يلي $group (تحسين يدوي — لا يقوم MongoDB بذلك تلقائيًا)؛ 5. يجب فهرسة foreignField الموجود في $lookup؛ 6. بالنسبة لمجموعات البيانات الكبيرة، أضف maxTimeMS لمنع تشغيل خط الأنابيب إلى ما لا نهاية.

الحالات الحدية لدالة النافذة $setWindowFields: $setWindowFields هي دالة نافذة أُضيفت في الإصدار 5.0 من MongoDB وتقوم بإجراء حسابات (مثل المتوسطات المتحركة، والمجاميع التراكمية، والترتيبات) على المستندات داخل «نافذة». الحالات الحدية: 1. حدود النافذة: تشمل خيارات «unbounded preceding» و«unbounded following» جميع المستندات في المجموعة، بينما تشمل خيارات «1 preceding» و«1 following» المستندات المجاورة فقط؛ 2. النافذة الفارغة: عندما تحتوي المجموعة على مستند واحد فقط، تُرجع $shift و$first و$last القيمة null؛ 3. تعارضات الفرز: يجب أن تتطابق المعلمة sortBy مع ترتيب الفرز للمجموعة؛ وإلا، يصبح نطاق النافذة غير متوقع؛ 4. الأداء: يجب أن تحافظ دوال النافذة على حالة النافذة في الذاكرة؛ وقد تتجاوز المجموعات الكبيرة (> 100,000 مستند) حدود الذاكرة.

حالات الاستخدام العملية لـ $setWindowFields: تعمل $setWindowFields على سد الفجوة الناتجة عن عدم توفر وظائف النوافذ في MongoDB — 1. الحسابات الشهرية/السنوية: تسترد $shift البيانات من الفترة السابقة، وتحسب $subtract التغير، وتحسب $divide معدل النمو؛ 2. المجاميع التراكمية: تمتد نافذة $sum من «الفترة السابقة غير المحدودة» إلى الصف الحالي لحساب المبيعات التراكمية؛ 3. المتوسطات المتحركة: تأخذ نافذة $avg البيانات من أحدث N فترات (على سبيل المثال، 7 فترات سابقة للصف الحالي) لتخفيف التقلبات قصيرة المدى؛ 4. الترتيب/تقسيم الصفحات: تنفذ $rank/$denseRank عملية الترتيب، بينما تنفذ $rowNumber عملية تقسيم الصفحات (وهي أكثر مرونة من skip/limit)؛ 5. أفضل N سجلات مجمعة: بعد التجميع باستخدام partitionBy، استخدم $rank لترتيب النتائج، ثم استخدم $match مع rank <= N لاسترداد أفضل N سجلات من كل مجموعة. تمثل هذه السيناريوهات الاستخدامات القياسية لوظائف النوافذ في SQL؛ وتكون صيغة MongoDB أكثر تفصيلاً ولكنها مكافئة من الناحية الوظيفية.

البنية ثلاثية المستويات لنظام إعداد التقارير: يتألف نظام إعداد التقارير المخصص للإنتاج من ثلاث مستويات: 1. مستوى البيانات (خط أنابيب التجميع في MongoDB): يحسب النتائج الإحصائية من البيانات الأولية ويقوم بإخراجها إلى مجموعة بيانات وسيطة؛ 2. مستوى الخدمة (Node.js + ذاكرة التخزين المؤقت): يستدعي خط أنابيب التجميع، ويخزن النتائج مؤقتًا (Redis TTL 5 دقائق)، ويوفر واجهات برمجة تطبيقات REST؛ 3. طبقة العرض (مكتبة الرسوم البيانية للواجهة الأمامية): تسترد البيانات من واجهة برمجة التطبيقات، وتعرض الرسوم البيانية (ECharts/Chart.js)، وتتيح التصفية التفاعلية. وتتمثل مزايا فصل هذه الطبقات الثلاث — حيث تركز طبقة البيانات على الحساب، وتركز طبقة الخدمة على الأداء، وتركز طبقة العرض على تجربة المستخدم — في إمكانية تحسين كل طبقة وتوسيع نطاقها بشكل مستقل.

مسارات التجميع مقابل أدوات ذكاء الأعمال: متى يجب استخدام مسارات التجميع لإنشاء التقارير، ومتى يجب استخدام أدوات ذكاء الأعمال المتخصصة (Metabase/Superset/Tableau)؟ تُعد مسارات التجميع مناسبة لما يلي: 1. التقارير ذات المنطق البسيط (مسارات تجميع تتألف من 5 إلى 10 مراحل)؛ 2. التطبيقات التي تتطلب التضمين (تقوم واجهة برمجة التطبيقات (API) بإرجاع بيانات التقرير، وتقوم الواجهة الأمامية بعرض المخططات من تلقاء نفسها)؛ 3. حجم البيانات المتوسط (أقل من 10 ملايين سجل)؛ 4. المتطلبات العالية في الوقت الفعلي (الحساب في الوقت الفعلي لكل طلب). تعد أدوات ذكاء الأعمال (BI) مناسبة لما يلي: 1. استعلامات الخدمة الذاتية من قبل المستخدمين غير التقنيين (واجهة السحب والإفلات)؛ 2. التحليل متعدد الأبعاد المعقد (مكعبات OLAP، والتعمق/التوسع)؛ 3. مصادر البيانات المتنوعة (MongoDB + MySQL + CSV)؛ 4. إرسال التقارير عبر البريد الإلكتروني وفقًا لجدول زمني محدد. بالنسبة للفرق الصغيرة، يكفي استخدام مسار التجميع مع ECharts؛ أما بالنسبة للفرق الأكبر حجمًا، فإن أدوات BI توفر كفاءة أكبر.

▶ المثال 1: التطبيق العملي المتقدم لخطوط أنابيب التجميع - تحليل تقسيم المستخدمين

JAVASCRIPT
// Scene:Classify users based on their spending VIP Layering
db.orders.insertMany([
  { userId: 'user_001', total: NumberDecimal('15000'), createdAt: new Date('2026-06-01'), status: 'paid' },
  { userId: 'user_002', total: NumberDecimal('500'),   createdAt: new Date('2026-06-05'), status: 'paid' },
  { userId: 'user_003', total: NumberDecimal('50'),    createdAt: new Date('2026-06-10'), status: 'paid' },
  { userId: 'user_001', total: NumberDecimal('800'),   createdAt: new Date('2026-06-15'), status: 'paid' }
]);

// Complete Pipeline:User Segmentation + Tag Conversion + Monthly Statistics
db.orders.aggregate([
  // Step 1: Count only paid orders
  { $match: { status: 'paid' } },

  // Step 2: Group by User
  {
    $group: {
      _id: '$userId',
      totalSpent: { $sum: '$total' },
      orderCount: { $sum: 1 },
      avgOrderValue: { $avg: '$total' },
      lastOrderAt: { $max: '$createdAt' }
    }
  },

  // Step 3: Use $switch to Segment Users
  {
    $addFields: {
      userLevel: {
        $switch: {
          branches: [
            { case: { $gte: ['$totalSpent', 10000] }, then: 'VIP' },
            { case: { $gte: ['$totalSpent', 1000] },  then: 'Gold' },
            { case: { $gte: ['$totalSpent', 100] },   then: 'Silver' }
          ],
          default: 'Bronze'
        }
      },
      // Days until the last order
      daysSinceLastOrder: {
        $divide: [
          { $subtract: [new Date(), '$lastOrderAt'] },
          1000 * 60 * 60 * 24
        ]
      }
    }
  },

  // Step 4: Date Format
  {
    $project: {
      userId: '$_id',
      totalSpent: 1,
      avgOrderValue: { $toString: '$avgOrderValue' },  // Decimal128 -> String
      userLevel: 1,
      daysSinceLastOrder: { $round: ['$daysSinceLastOrder', 0] },  // Round to the nearest whole number
      lastOrderDate: {
        $dateToString: {
          format: '%Y-%m-%d',
          date: '$lastOrderAt',
          timezone: 'Asia/Tokyo'
        }
      }
    }
  },

  // Step 5: Sort by Amount Spent
  { $sort: { totalSpent: -1 } }
]);

الإخراج:

TEXT 📖 للعرض فقط
[
  { userId: 'user_001', totalSpent: '15800', avgOrderValue: '7900', userLevel: 'VIP', daysSinceLastOrder: 16, lastOrderDate: '2026-06-15' },
  { userId: 'user_002', totalSpent: '500',   avgOrderValue: '500',  userLevel: 'Silver', daysSinceLastOrder: 26, lastOrderDate: '2026-06-05' },
  { userId: 'user_003', totalSpent: '50',    avgOrderValue: '50',   userLevel: 'Bronze', daysSinceLastOrder: 21, lastOrderDate: '2026-06-10' }
]

النتيجة: تم تصنيف المستخدمين الثلاثة تلقائيًا حسب مبلغ الإنفاق. يبلغ إجمالي إنفاق user_001 15,800، وقد تم تصنيفه كعميل VIP؛ وقد مر 16 يومًا على آخر طلب له، وتم تنسيق التاريخ وفقًا لمنطقة توكيو الزمنية.

▶ المثال 2: تقارير مبيعات ShopHub + تنسيق التاريخ

تحضير البيانات للتقارير: يجب أن تغطي بيانات الاختبار الخاصة بتقارير المبيعات عدة أشهر — وإلا فلن يمكن حساب النمو الشهري (نظرًا لعدم توفر بيانات الشهر السابق في الشهر الأول). في هذا المثال، قمنا بإعداد بيانات الطلبات للفترة من مايو إلى يوليو: طلب واحد في مايو، وطلبان في يونيو، وطلب واحد في يوليو. النقاط الرئيسية لتصميم البيانات: 1. طلب واحد على الأقل كل شهر (وإلا فإن $bucket ستنشئ مجموعات فارغة)؛ 2. توزيع المبالغ معقول (بما في ذلك مبلغ صغير قدره 300 ومبلغ كبير قدره 2200)؛ 3. تم تعيين الحالة بشكل موحد على paid ($match تستبعد الطلبات غير المدفوعة).

تفسير النمو الشهري: معدل النمو الشهري = (هذا الشهر - الشهر الماضي) / الشهر الماضي. تمثل الإيرادات البالغة 3,000 في يونيو زيادة بنسبة 100% عن 1,500 في مايو — وهذا ما يُعرف بـ«تضاعف» النمو. ومع ذلك، يجب الانتباه إلى تأثير القاعدة — فالارتفاع من 100 إلى 200 يمثل أيضًا نموًا بنسبة 100٪، لكن الزيادة المطلقة تبلغ 100 فقط؛ بينما يمثل الارتفاع من 10,000 إلى 15,000 نموًا بنسبة 50٪ فقط، لكن الزيادة المطلقة تبلغ 5,000. عند اتخاذ القرارات التجارية، من الضروري مراعاة كل من معدل النمو والزيادة المطلقة؛ فالاعتماد على مقياس واحد فقط غير كافٍ.

طرق التحقق من صحة البيانات للتقارير: يجب التحقق من صحة مخرجات مسار التجميع — 1. التحقق المتبادل: مقارنة نتيجة find().count() بنتيجة $sum: 1 في $group؛ يجب أن تتطابق الأعداد؛ 2. التحقق عن طريق العينات: اختيار 2–3 صفوف عشوائية من البيانات الأولية وحسابها يدويًّا للتأكد من صحة النتائج المجمعة؛ 3. التحقق من الحدود: في حالة وجود مجموعة بيانات فارغة (تُرجع الدالة $match عدم وجود تطابقات) → تظهر نتيجة فارغة بدلاً من ظهور خطأ؛ صف بيانات واحد (تحتوي الدالة $group على مجموعة واحدة فقط) → يكون معدل النمو الشهري 0 (لا توجد بيانات من الشهر السابق)؛ 4. التحقق من الاتساق: الإجمالي الشهري = الإجمالي السنوي؛ إجماليات كل فئة = الإجمالي الكلي. أي عدم اتساق يشير إلى وجود خطأ في منطق مسار التجميع.

استراتيجية التخزين المؤقت للتقارير: تستغرق مسارات التجميع في الوقت الفعلي وقتًا طويلاً لمعالجة مجموعات البيانات الكبيرة (بالثواني) — 1. التجميع المجدول: استخدم $merge لكتابة نتائج التجميع إلى مجموعة تقارير (على سبيل المثال، monthly_reports) كل ساعة؛ وتتم قراءة الاستعلامات من مجموعة التقارير (بالميلي ثانية)؛ 2. التحديثات التراكمية: قم بتجميع البيانات الجديدة فقط ($match مع نطاق زمني تراكمي)، ثم استخدم $merge لدمجها في التقارير الحالية؛ 3. طبقة التخزين المؤقت: استخدم Node.js لتخزين نتائج التجميع مؤقتًا في Redis (TTL 5–30 دقيقة)، وهو مناسب لصفحات التقارير التي تتضمن عمليات قراءة كثيرة وعمليات كتابة قليلة؛ 4. استراتيجية انتهاء الصلاحية: وضع علامة «غير صالح» على ذاكرة التخزين المؤقت عند تغير البيانات المصدر (باستخدام أرقام الإصدارات أو الطوابع الزمنية)، وإعادة الحساب عند الاستعلام التالي. معايير اختيار الاستراتيجية: تواتر تغير البيانات (متطلبات الوقت الفعلي) × تواتر الاستعلامات (متطلبات الأداء).

JAVASCRIPT
// Scene:ShopHub The operations team generates monthly sales reports.,Includes formatted dates and year-over-year growth
db.orders.insertMany([
  { orderId: 'ORD-001', userId: 'user_001', total: NumberDecimal('1500'), status: 'paid', createdAt: new Date('2026-05-15') },
  { orderId: 'ORD-002', userId: 'user_002', total: NumberDecimal('800'),  status: 'paid', createdAt: new Date('2026-06-01') },
  { orderId: 'ORD-003', userId: 'user_001', total: NumberDecimal('2200'), status: 'paid', createdAt: new Date('2026-06-20') },
  { orderId: 'ORD-004', userId: 'user_003', total: NumberDecimal('300'),  status: 'paid', createdAt: new Date('2026-07-05') }
]);

// Monthly Report:Format Month、Calculate the month-over-month growth、User Segmentation Labels
db.orders.aggregate([
  { $match: { status: 'paid' } },
  {
    $group: {
      _id: {
        year: { $year: '$createdAt' },
        month: { $month: '$createdAt' }
      },
      revenue: { $sum: '$total' },
      orderCount: { $sum: 1 },
      avgOrderValue: { $avg: '$total' }
    }
  },
  { $sort: { '_id.year': 1, '_id.month': 1 } },
  {
    $addFields: {
      monthLabel: {
        $dateToString: {
          format: '%Y-%m',
          date: { $dateFromParts: { year: '$_id.year', month: '$_id.month' } }
        }
      },
      revenueStr: { $toString: '$revenue' },
      performance: {
        $switch: {
          branches: [
            { case: { $gte: ['$revenue', 2000] }, then: 'Excellent' },
            { case: { $gte: ['$revenue', 1000] }, then: 'Good' },
            { case: { $gte: ['$revenue', 500] }, then: 'Average' }
          ],
          default: 'Below Target'
        }
      }
    }
  }
]);

الإخراج:

TEXT 📖 للعرض فقط
[
  { _id: {year: 2026, month: 5}, monthLabel: '2026-05', revenue: 1500, performance: 'Good', ... },
  { _id: {year: 2026, month: 6}, monthLabel: '2026-06', revenue: 3000, performance: 'Excellent', ... },
  { _id: {year: 2026, month: 7}, monthLabel: '2026-07', revenue: 300, performance: 'Below Target', ... }
]

// Output: // [ // { _id: {year:2026,month:5}, monthLabel:'2026-05', revenue:1500, performance:'Good', ... }, // { _id: {year:2026,month:6}, monthLabel:'2026-06', revenue:3000, performance:'Excellent', ... }, // { _id: {year:2026,month:7}, monthLabel:'2026-07', revenue:300, performance:'Below Target', ... } // ]


> الناتج: تتضمن التقارير الشهرية تسميات الأشهر بتنسيق محدد (2026-05)، وسلاسل الإيرادات المحولة، وعلامات تقييم الأداء التلقائية باستخدام $switch.

**تكييف نظام إعداد التقارير ليكون جاهزًا للإنتاج**: خط أنابيب التجميع في هذا المثال هو نسخة تدريبية — وهناك حاجة إلى مزيد من التعديلات لتكييفه مع بيئة الإنتاج — 1. الاستعلامات المعلمة: يجب تمرير نطاقات الأشهر، وفلاتر الفئات، وفلاتر معرّفات المستخدمين كمعلمات لواجهة برمجة التطبيقات (بدلاً من ترميزها بشكل ثابت في خط الأنابيب)؛ 2. معالجة الأخطاء: قد يفشل تنفيذ مسار التجميع بسبب حدود الذاكرة أو انتهاء المهلة؛ وهذا يتطلب تغليف الكود في كتلة `try-catch`، وتعيين حد `maxTimeMS`، واستخدام `allowDiskUse` كخطة بديلة؛ 3. طبقة التخزين المؤقت: لا تتغير بيانات التقارير الشهرية إلا نادرًا (بضعة طلبات جديدة فقط يوميًا)؛ استخدم Redis لتخزين نتائج التجميع مؤقتًا (TTL ساعة واحدة)، بحيث تصل 90% من طلبات التقارير إلى ذاكرة التخزين المؤقت ولا تتطلب تنفيذ مسار البيانات؛ 4. التفعيل المجدول: استخدم Change Stream لمراقبة تغييرات الطلبات وتحديث مجموعة التقارير بشكل تدريجي (بدلاً من إجراء تجميع كامل في كل مرة)؛ 5. تكييف تنسيق الإخراج: تتطلب الواجهة الأمامية تنسيق Chart.js ({labels: [...], datasets: [...]})، لذا يتم تحويل النتائج المجمعة إلى تنسيق المخطط البياني في الخلفية.

**قائمة مراجعة استكشاف الأخطاء وإصلاحها في مسار التجميع**: خطوات استكشاف الأخطاء وإصلاحها في مسار التجميع — 1. تصنيف نوع الخطأ: «تجاوز المخزن المؤقت الحد الأقصى» → تجاوز حد الذاكرة (زيادة `allowDiskUse` أو تحسين المسار)؛ «تجاوز الحد الزمني» → انتهاء مهلة التنفيذ (زيادة `maxTimeMS` أو تحسين الفهارس)؛ «يجب أن يبدأ مسار الحقل بـ '$'» → خطأ في مرجع الحقل (تحقق من وجود البادئة `$`)؛ «عامل غير معروف» → خطأ إملائي في العامل أو أنه غير مدعوم في الإصدار؛ 2. تصحيح الأخطاء مرحلةً تلو الأخرى: قم بتنفيذ مرحلة واحدة فقط في كل مرة؛ وبعد التأكد من صحة الناتج، انتقل إلى المرحلة التالية؛ 3. التحقق من حجم البيانات: تحقق من أن عدد المستندات قبل وبعد `$group` يتطابق مع التوقعات (كم عدد المستندات التي تمت تصفيتها بواسطة `$match` وكم عدد المستندات التي تم توسيعها بواسطة `$unwind`)؛ 4. فحص الفهرس: استخدم `explain()` للتأكد من أن `$match` يستخدم فهرسًا (`IXSCAN` بدلاً من `COLLSCAN`)؛ 5. توافق الإصدارات: تتطلب العوامل مثل `$dateAdd` (5.0+)، و`$setWindowFields` (5.0+)، و`$densify` (6.1+) إصدار MongoDB المطابق.

---

### ▶ المثال 3:تحليل سلوك المستخدم وتصنيفهم باستخدام $switch(الصعوبة ⭐⭐)

```javascript
// Scene:ShopHub User behavior analysis and automatic tier classification
db.user_activity.insertMany([
  { userId: 'U001', sessions: 45, pageViews: 320, purchases: 12, totalSpent: 5800, lastActive: new Date('2026-07-01') },
  { userId: 'U002', sessions: 8, pageViews: 42, purchases: 1, totalSpent: 150, lastActive: new Date('2026-06-15') },
  { userId: 'U003', sessions: 120, pageViews: 890, purchases: 28, totalSpent: 12500, lastActive: new Date('2026-07-18') },
  { userId: 'U004', sessions: 3, pageViews: 15, purchases: 0, totalSpent: 0, lastActive: new Date('2026-05-20') }
]);

// تصنيف المستخدمين وحساب المقاييس المتقدمة
db.user_activity.aggregate([
  {
    $addFields: {
      // تصنيف المستخدم حسب إجمالي الإنفاق
      tier: {
        $switch: {
          branches: [
            { case: { $gte: ['$totalSpent', 10000] }, then: 'Platinum' },
            { case: { $gte: ['$totalSpent', 5000] }, then: 'Gold' },
            { case: { $gte: ['$totalSpent', 1000] }, then: 'Silver' },
            { case: { $gt: ['$totalSpent', 0] }, then: 'Bronze' }
          ],
          default: 'New'
        }
      },
      // حساب متوسط قيمة الجلسة
      avgSessionValue: {
        $cond: {
          if: { $gt: ['$sessions', 0] },
          then: { $round: [{ $divide: ['$totalSpent', '$sessions'] }, 2] },
          else: 0
        }
      },
      // حساب معدل التحويل
      conversionRate: {
        $cond: {
          if: { $gt: ['$sessions', 0] },
          then: { $round: [{ $multiply: [{ $divide: ['$purchases', '$sessions'] }, 100] }, 1] },
          else: 0
        }
      },
      // حساب أيام النشاط الأخير
      daysSinceActive: {
        $round: [
          { $divide: [{ $subtract: [new Date(), '$lastActive'] }, 86400000] },
          0
        ]
      }
    }
  },
  // تصفية المستخدمين النشطين خلال آخر 30 يومًا
  { $match: { daysSinceActive: { $lte: 30 } } },
  // تجميع حسب المستوى
  {
    $group: {
      _id: '$tier',
      userCount: { $sum: 1 },
      totalRevenue: { $sum: '$totalSpent' },
      avgPurchases: { $avg: '$purchases' },
      avgConversionRate: { $avg: '$conversionRate' }
    }
  },
  { $sort: { totalRevenue: -1 } }
]);

الإخراج:

TEXT 📖 للعرض فقط
[
  {_id: 'Platinum', userCount: 1, totalRevenue: 12500, avgPurchases: 28, avgConversionRate: 23.3},
  {_id: 'Gold', userCount: 1, totalRevenue: 5800, avgPurchases: 12, avgConversionRate: 26.7},
  {_id: 'Bronze', userCount: 1, totalRevenue: 150, avgPurchases: 1, avgConversionRate: 12.5}
]

❓ أسئلة شائعة

الهدف التصميمي وراء الأسئلة الشائعة: هذه الأسئلة ليست مجرد أسئلة متكررة؛ بل هي امتداد لقرارات التصميم — فالاختيار بين $cond و$switch ينطوي على مفاضلة بين سهولة القراءة والأداء؛ وتستلزم المناطق الزمنية اختيار استراتيجية تخزين؛ كما ينطوي تحويل الأنواع على إدارة المخاطر في نظام ذي أنواع ضعيفة. وفهم «لماذا» أهم من تذكر «ماذا».

س أيهما أفضل من حيث الأداء، $cond أم $switch؟
ج $cond أسرع قليلاً (يستخدم عددًا أقل من تعليمات وحدة المعالجة المركزية). استخدم $switch فقط في حالة وجود فروع متعددة.
س هل تدعم دالة $dateToString المناطق الزمنية؟
ج إنها تدعم المعلمة timezone (أسماء المناطق الزمنية وفقًا لـ IANA، مثل 'Asia/Tokyo').
س ماذا يحدث إذا فشل تحويل النوع؟
ج بشكل افتراضي، يتم إرجاع القيمة null. يمكنك استخدام $convert لتحديد معالج onError.

📖 ملخص

ربط المفاهيم: تشكل الموضوعات الخمسة المتقدمة في مسار التجميع مجموعة قدرات معالجة البيانات — حيث تشكل التعبيرات الشرطية «الطبقة المنطقية» (اتخاذ القرارات بناءً على البيانات)، بينما تشكل عمليات التاريخ والنوع والسلسلة والمصفوفة «طبقة التحويل» (تحويل البيانات إلى التنسيق المطلوب). في مسارات المعالجة الفعلية، تُستخدم هذه العمليات مجتمعةً: تُحسب الإحصائيات بواسطة $group → يُطبق $addFields الطبقات الشرطية باستخدام $switch → يُنسق $project التواريخ باستخدام $dateToString → الناتج. إتقان «طبقة التحويل» هو السبيل الأساسي لإتقان مسارات التجميع.

نظرة عامة شاملة على تحسين أداء مسار التجميع: يعتمد أداء مسار التجميع على ثلاثة عوامل — 1. حجم عمليات الإدخال/الإخراج (عدد المستندات التي تم مسحها ضوئيًا/عدد إدخالات الفهرس التي تمت قراءتها): يمكن تحسين ذلك عن طريق إضافة مرشح أولي قبل مرحلة $match واستخدام hint() لاختيار الفهارس؛ 2. استخدام الذاكرة (كمية الذاكرة التي تستهلكها النتائج الوسيطة لمسار التجميع): يمكن تحقيق التحسين عن طريق تبسيط الحقول في $project واستخدام allowDiskUse لتفريغ الفائض إلى القرص؛ 3. الحساب على وحدة المعالجة المركزية (CPU) (التعقيد الحسابي لـ $group/$sort): يمكن تحقيق التحسين عن طريق تقليل تعقيد _id والاستفادة من الفهارس للفرز. لكل بُعد مبادئ تصميم وتقنيات ضبط خاصة به؛ والفهم المنهجي أكثر فعالية من حفظ حيل التحسين واحدة تلو الأخرى.

من مسارات التجميع إلى ETL: مسار التجميع هو في الأساس أداة ETL (استخراج - تحويل - تحميل) خفيفة الوزن — حيث يمثل $match مرحلة «الاستخراج» (استخراج البيانات من مجموعة)، ويمثل $project/$addFields/$convert مرحلة «التحويل» (تنقية البيانات وتحويلها)، ويمثل $out/$merge مرحلة «التحميل» (الكتابة إلى المجموعة المستهدفة). بالنسبة لمسارات البيانات البسيطة (مصدر واحد → تحويل → وجهة)، فإن مسار التجميع أخف وزنًا وأسرع من Spark أو Airflow. ومع ذلك، عندما يتضمن ETL مصادر بيانات متعددة (MongoDB + MySQL + S3) أو جدولة معقدة (سلاسل التبعية، وإعادة المحاولة، والتنبيهات)، يجب عليك استخدام أداة ETL مخصصة بدلاً من مسار التجميع.


📝 تمارين

مستوى المهارة: تتوافق التمارين الخمسة مع 3 مستويات من المهارة — التمارين الأساسية (⭐) تختبر قدرتك على استخدام عامل حسابي واحد؛ والتمارين المتوسطة (⭐⭐) تختبر قدرتك على الجمع بين العوامل الحسابية؛ والتمارين التحديية (⭐⭐⭐) تختبر قدرتك على التصميم بشكل مستقل. نوصي بإكمالها بالترتيب. بالنسبة لكل تمرين، قم أولاً بوصف ما يجب أن تفعله كل خطوة في مسار المعالجة باللغة الصينية (مسار معالجة افتراضي)، ثم قم بترجمة ذلك إلى كود برمجي.

  1. السؤال الأساسي (⭐): استخدم عبارة $switch لإضافة تسميات باللغة الصينية لحالات الطلبات.
  2. السؤال الأساسي (⭐): استخدم الدالة $dateToString لتنسيق تاريخ الطلب.
  3. مشكلة متقدمة (⭐⭐): تقسيم المستخدمين ($switch للتمييز بين فئات VIP و«Gold» و«Silver»).
  4. مشكلة متقدمة (⭐⭐): استخدم الدالة $map لتحويل جميع التسميات إلى أحرف كبيرة.
  5. سؤال التحدي (⭐⭐⭐): تقرير المبيعات الشهري + معدل النمو السنوي ($setWindowFields + $shift).

دليل التحدي: يُعد تحدي «تقرير المبيعات الشهري + معدل النمو السنوي» الأقرب إلى سيناريوهات الأعمال الواقعية. خطوات التنفيذ: 1. استخدم $match لتصفية الطلبات المدفوعة؛ 2. استخدم $group للتجميع حسب {السنة، الشهر} وحساب الإيرادات وعدد الطلبات ومتوسط قيمة الطلب؛ 3. استخدم $sort للفرز حسب السنة والشهر؛ 4. استخدم $setWindowFields و$shift لاسترداد إيرادات الشهر الماضي؛ 5. استخدم $addFields لحساب معدل النمو مقارنة بالشهر السابق. لاحظ أن by: -1 في $shift تشير إلى «الصف السابق» (الشهر الماضي)، وby: 1 تشير إلى «الصف التالي» (الشهر التالي).

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%