> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# MongoDB: قاعدة بيانات المستندات ومتى تناسبك
- URL: https://arabroot.io/articles/mongodb-قاعدة-بيانات-المستندات-ومتى-تناسبك/
- Published: 2026-10-12T10:15:00.000Z
- Updated: 2026-10-11T11:18:17.000Z
- Description: كيف تصمم مستندات MongoDB دون أن تكبر حتى تتوقف، وكيف تشغله في Replica Set، وماذا تعني رخصته لك.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, هندسة البرمجيات وتصميم الأنظمة, قواعد البيانات, الاستضافة الذاتية

في [مقال أنواع قواعد البيانات](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%88%D9%85%D8%AA%D9%89-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D9%83%D9%84-%D9%86%D9%88%D8%B9/) ذكرنا أن قواعد المستندات Document Databases تحفظ كل سجل كمستند JSON كامل بدل صفوف موزعة على جداول، وأن MongoDB أشهرها. والسؤال الذي يأتي بعد ذلك عملي جداً: متى يكون هذا الشكل أفضل من جدول في PostgreSQL أو MySQL، ومتى يتحول إلى مشكلة بعد سنة من الإنتاج؟ والمشكلة الحقيقية مع MongoDB ليست في تثبيته، فهو يعمل بأمر واحد، وإنما في تصميم المستندات، لأن الخطأ في التصميم لا يظهر في الأسبوع الأول، وإنما عندما يكبر مستند واحد حتى يرفضه السيرفر.

وسوف نناقش في هذا المقال ما يلي:

- نموذج المستندات Document Model، والتضمين Embedding أم الإشارة Referencing.
- الفهارس Indexes والتجميع Aggregation والمعاملات Transactions.
- مجموعة النسخ Replica Set والانتخاب عند توقف السيرفر الرئيسي.
- رخصة SSPL وماذا تعني لك إذا كنت تستضيف MongoDB بنفسك، وFerretDB كبديل مفتوح وما حالته اليوم.
- تجربة عملية: Replica Set بثلاثة سيرفرات، ومليون طلب، وانتقال تلقائي، ثم خطأ التصميم المشهور: المصفوفة التي تكبر بلا حد Unbounded Array، بأرقام حقيقية.

## نموذج المستندات: الطلب كله في مكان واحد

في قاعدة علائقية Relational يكون الطلب الواحد صفاً في جدول الطلبات، ومنتجاته صفوفاً في جدول آخر، والعميل صفاً في جدول ثالث، وعندما تقرأ الطلب تجمعها بـ JOIN. أما في MongoDB فالطلب مستند واحد بصيغة BSON (وهي JSON ثنائي فيه أنواع إضافية مثل التاريخ والأرقام العشرية Decimal128)، وفيه المنتجات كمصفوفة، وقد يكون فيه جزء من بيانات العميل. والمستندات تحفظ في مجموعات Collections بدل الجداول، ولا يوجد مخطط Schema يفرضه السيرفر إلا إذا طلبت أنت ذلك بقواعد التحقق Schema Validation. والصورة التالية تبين الفرق:

![على اليسار ثلاثة جداول customers وorders وorder_items بأسهم بينها ومكتوب تحتها أن قراءة الطلب تحتاج JOIN، وعلى اليمين مستند MongoDB واحد فيه _id 501 والحالة paid وكائن customer بداخله ومصفوفة items بثلاثة منتجات، ومكتوب تحته قراءة واحدة وكتابة واحدة ذرية، وحد 16 MB لكل مستند](https://arabroot.io/content/images/2026/10/mongodb-01-document-model.webp)

نفس الطلب كصفوف في ثلاثة جداول، وكمستند واحد في MongoDB

والفائدة هنا واضحة: قراءة واحدة تعطيك الطلب كله، وكتابة واحدة على المستند ذرية Atomic دائماً، فإما أن يتغير كله أو لا شيء، ولذلك يقول [توثيق المعاملات](https://www.mongodb.com/docs/manual/core/transactions/?ref=arabroot.io) نفسه إن التصميم الجيد للمستندات يقلل حاجتك للمعاملات على أكثر من مستند. وأما الثمن فهو أن نفس المعلومة قد تتكرر في أكثر من مستند (اسم العميل في كل طلب)، وأن السيرفر لا يمنعك من حفظ مستند ناقص أو بحقل مكتوب بشكل خاطئ إلا إذا أضفت قواعد التحقق، وأن للمستند الواحد حداً أقصى 16 MB بحسب [صفحة الحدود](https://www.mongodb.com/docs/manual/reference/limits/?ref=arabroot.io)، وهذا الحد بالذات هو موضوع الجزء الأخير من التجربة.

وقد يتساءل البعض: إذا كان PostgreSQL يحفظ JSON في عمود JSONB، فلماذا MongoDB أصلاً؟ والإجابة أن PostgreSQL يكفيك في حالات كثيرة كما بينا في [قسم JSONB في مقال PostgreSQL لكل شيء](https://arabroot.io/articles/postgresql-%D9%84%D9%83%D9%84-%D8%B4%D9%8A%D8%A1-%D9%85%D8%AA%D9%89-%D8%AA%D9%83%D9%81%D9%8A%D9%83-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%88%D8%A7%D8%AD%D8%AF%D8%A9/#jsonb)، وMongoDB يتميز عندما يكون التطبيق كله مبنياً على مستندات تتغير حقولها كثيراً، ويحتاج التوزيع على عدة سيرفرات Sharding مدمجاً، وفريقك يكتب بالفعل بلغة المستندات.

## التضمين Embedding أم الإشارة Referencing؟

هذا هو القرار الأهم في تصميم أي مجموعة، والتوثيق يشرح الطريقتين في صفحتي [التضمين](https://www.mongodb.com/docs/manual/data-modeling/embedding/?ref=arabroot.io) و[الإشارة](https://www.mongodb.com/docs/manual/data-modeling/referencing/?ref=arabroot.io):

- **التضمين Embedding:** تضع البيانات المرتبطة داخل المستند نفسه، مثل منتجات الطلب أو عنوان الشحن. ويناسبك عندما تقرأ البيانات معاً دائماً، وعندما يكون عددها محدوداً ومعروفاً (طلب فيه عشرات المنتجات وليس ملايين).
- **الإشارة Referencing:** تضع في المستند رقم المستند الآخر فقط، مثل `customer_id`، وتقرأ الآخر باستعلام آخر أو بالمرحلة [$lookup](https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/?ref=arabroot.io). ويناسبك عندما يكون الطرف الآخر كبيراً أو يتغير وحده أو يكبر بلا حد، مثل تعليقات منشور أو قراءات جهاز.

والقاعدة العملية سؤال واحد: هل لهذه المصفوفة حد أعلى تعرفه؟ إذا كانت الإجابة لا، فلا تقم بتضمينها، والسبب أن المستند سوف يكبر مع الوقت حتى يصل إلى 16 MB، وقبل ذلك بكثير سوف تصبح قراءته وكتابته أبطأ في كل مرة، وهذا ما يسميه التوثيق نمطاً سيئاً Anti-pattern في صفحة [المصفوفات بلا حد Unbounded Arrays](https://www.mongodb.com/docs/manual/data-modeling/design-antipatterns/unbounded-arrays/?ref=arabroot.io)، وسوف نراه بالأرقام في التجربة.

## الفهارس والتجميع Aggregation

الفهارس في MongoDB تعمل بنفس الفكرة التي شرحناها في [مقال الفهارس](https://arabroot.io/articles/%D8%A7%D9%84%D9%81%D9%87%D8%A7%D8%B1%D8%B3-indexes-%D9%88%D9%82%D8%B1%D8%A7%D8%A1%D8%A9-explain/): شجرة B-tree على حقل أو أكثر، وبدونها يقرأ السيرفر المجموعة كلها COLLSCAN، ومعها يقرأ الفهرس IXSCAN ثم المستندات المطابقة فقط. والفرق أنك تستطيع أن تفهرس حقلاً داخل مستند متداخل مثل `customer.id` أو حقلاً داخل مصفوفة، وأن الأمر `explain("executionStats")` يعطيك عدد المستندات التي قرأها السيرفر، وهو الرقم الذي تراقبه دائماً.

أما التقارير فتكتب بخط التجميع [Aggregation Pipeline](https://www.mongodb.com/docs/manual/aggregation/?ref=arabroot.io)، وهو سلسلة مراحل كل واحدة تأخذ مخرج التي قبلها: `$match` للتصفية، و`$group` للتجميع، و`$sort` للترتيب، و`$lookup` للربط بمجموعة أخرى. ومن يعرف SQL سوف يجد أن `$match` هو WHERE و`$group` هو GROUP BY، مع فرق مهم: ضع `$match` في أول الخط دائماً، لأن الفهرس لا يستخدم إلا في المراحل الأولى.

## المعاملات Transactions

كل كتابة على مستند واحد ذرية دائماً. وإذا احتجت أن تغير أكثر من مستند معاً، مثل إنقاص المخزون وإنشاء الطلب، فMongoDB يدعم المعاملات على أكثر من مستند منذ الإصدار 4.0 في Replica Set ومنذ 4.2 في العنقود الموزع Sharded Cluster، ولكن التوثيق يقول بوضوح إن المعاملة الموزعة أغلى من الكتابة على مستند واحد، وإنها ليست بديلاً عن التصميم الجيد. ولاحظ أن المعاملات تحتاج Replica Set ولا تعمل على سيرفر منفرد، وهذا أحد أسباب أن تجربتنا تبدأ بثلاثة سيرفرات حتى على جهاز واحد. وأما مشاكل الأقفال والتعارض بين المعاملات فشرحناها في [مقال الأقفال](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%82%D9%81%D8%A7%D9%84-locks-%D9%88%D8%A7%D9%84%D8%AC%D9%85%D9%88%D8%AF-deadlock-%D9%81%D9%8A-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA/).

## مجموعة النسخ Replica Set

شرحنا أشكال العناقيد بشكل عام في [مقال عناقيد قواعد البيانات](https://arabroot.io/articles/%D8%B9%D9%86%D8%A7%D9%82%D9%8A%D8%AF-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#engine-mongodb)، وMongoDB من القواعد التي يأتي فيها النسخ والانتقال مدمجين: ثلاثة سيرفرات (أو أكثر بعدد فردي)، واحد منها رئيسي Primary يستقبل الكتابة، والباقي ثانوية Secondary تنسخ سجل العمليات oplog منه. وعندما يتوقف الرئيسي يصوت الباقون وينتخبون رئيساً جديداً، والمكتبة Driver في التطبيق تكتشف ذلك وترسل الكتابة إلى الجديد، والتوثيق يقول إن الوقت المتوسط حتى ينتخب رئيس جديد لا يتجاوز في العادة 12 ثانية بالإعدادات الافتراضية، كما في [صفحة الانتخاب](https://www.mongodb.com/docs/manual/core/replica-set-elections/?ref=arabroot.io). والصورة التالية تبين ما حدث في التجربة:

![لوحتان: في الأولى التطبيق يكتب إلى mongo3 وهو PRIMARY، وسجل oplog ينسخ إلى mongo1 وmongo2، وفي الثانية mongo3 عليه علامة X ومكتوب أنه not reachable، وmongo2 يصوت لـ mongo1 الذي أصبح PRIMARY، والتطبيق يتبعه](https://arabroot.io/content/images/2026/10/mongodb-03-replica-set.webp)

توقف الرئيسي، فانتخب السيرفران الباقيان mongo1، ووجدته المكتبة دون تغيير في التطبيق

ولاحظ أن الكتابة الافتراضية Write Concern هي `w: "majority"` في أغلب الحالات بحسب [صفحة Write Concern](https://www.mongodb.com/docs/manual/reference/write-concern/?ref=arabroot.io)، أي أن السيرفر لا يؤكد الكتابة إلا بعد أن تصل إلى أغلب الأعضاء، ولذلك ما يؤكد لك لا يضيع عندما يتوقف الرئيسي. وإذا وضعت حكماً Arbiter بدل العضو الثالث فقد تتغير هذه القيمة الافتراضية إلى `w: 1` كما في جدول الصفحة نفسها، فلا تقم بذلك إلا إذا كنت تعرف ما تخسره.

## رخصة SSPL: ماذا تعني لمن يستضيف بنفسه؟

منذ 16 أكتوبر 2018 أصبح MongoDB Community Server تحت رخصة [Server Side Public License](https://www.mongodb.com/legal/licensing/server-side-public-license/faq?ref=arabroot.io) بدل AGPL، والإصدارات التي قبل ذلك التاريخ بقيت تحت AGPL. والشرط الذي صنعت الرخصة من أجله هو القسم 13: إذا قدمت وظيفة MongoDB نفسها كخدمة للآخرين، أي MongoDB كخدمة مدارة، فعليك أن تنشر كود الخدمة كلها، من واجهات الإدارة إلى النسخ الاحتياطي والمراقبة والاستضافة. والسبب المعلن أن الشركات السحابية كانت تبيع MongoDB كخدمة دون أن تساهم فيه.

فهل يؤثر هذا عليك؟ بحسب نفس صفحة الأسئلة: إذا استخدمت MongoDB قاعدة بيانات لتطبيقك، حتى لو كان تطبيقاً تبيعه كخدمة SaaS، فلا يوجد عليك شرط القسم 13، واستخدامه داخل شركتك أو لشركاتك التابعة لا يعتبر تقديماً لطرف ثالث. ولكن هناك ثلاث نتائج عملية يجب أن تعرفها:

- منظمة OSI لا تعتبر SSPL رخصة مفتوحة المصدر كما في [بيانها](https://opensource.org/blog/the-sspl-is-not-an-open-source-license?ref=arabroot.io)، ولذلك نصنفها في [رخص المصدر المتاح](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%8A-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A7%D8%AA-%D9%85%D9%81%D8%AA%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1-%D9%88%D8%AA%D8%B1%D8%A7%D8%AE%D9%8A%D8%B5%D9%87%D8%A7/#source-available).
- لن تجد حزمة `mongodb-server` في [مستودعات Debian](https://packages.debian.org/search?keywords=mongodb-server&ref=arabroot.io)، فالتثبيت يكون من مستودع MongoDB نفسه أو من ال Docker Image الرسمي، وأنت تعتمد على الشركة في التحديثات الأمنية.
- إذا كانت شركتك تبيع استضافة قواعد بيانات لعملائها، فهذه بالضبط الحالة التي كتبت الرخصة من أجلها، واسأل محامياً قبل أن تبدأ.

## FerretDB: بديل مفتوح، وما حالته اليوم؟

[FerretDB](https://github.com/FerretDB/FerretDB?ref=arabroot.io) مشروع تحت رخصة Apache 2.0 يستقبل اتصالات بروتوكول MongoDB ويحولها إلى أوامر على PostgreSQL، ومنذ الإصدار 2.0 يعتمد على إضافة [DocumentDB](https://github.com/documentdb/documentdb?ref=arabroot.io)، وهي إضافة تحت رخصة MIT بدأتها Microsoft ثم [انضمت إلى Linux Foundation](https://www.linuxfoundation.org/press/linux-foundation-welcomes-documentdb-to-advance-open-developer-first-nosql-innovation?ref=arabroot.io) في 25 أغسطس 2025 بمشاركة شركات منها AWS وGoogle وSupabase. والفكرة جذابة: تطبيقك يتكلم بلغة MongoDB، والبيانات في PostgreSQL الذي تعرف نسخه ومراقبته.

أما الحالة اليوم كما رأيناها في المستودع في أكتوبر 2026: آخر إصدار من FerretDB هو 2.7.0 في 10 نوفمبر 2025، وبعده لا توجد في الفرع الرئيسي إلا تحديثات للمكتبات حتى فبراير 2026، أما DocumentDB نفسه فنشط جداً ووصل إلى الإصدار المرشح v1.0-RC2 في 9 أكتوبر 2026\. لذلك إذا أردت هذا الطريق فراقب المشروعين قبل أن تعتمد عليه، وشغلنا عليه نفس سكربتات التجربة كما سوف ترى في [قسم FerretDB في التجربة](#lab-ferretdb).

## تجربة عملية: Replica Set ومليون طلب

سوف نشغل MongoDB 8.0.32 (الإصدار 8.0 مدعوم حتى أكتوبر 2029، وصدر 9.0 في نهاية سبتمبر 2026) بثلاثة أعضاء على Docker Engine 29.8.0، ونحمل نفس مليون طلب الذي حملناه في [تجربة MySQL وMariaDB](https://arabroot.io/articles/mysql-%D8%A7%D9%85-mariadb-%D9%88%D9%85%D8%AA%D9%89-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D8%A7%D9%8A%D8%A7-%D9%85%D9%86%D9%87%D9%85%D8%A7/#lab)، ونشغل نفس الأسئلة، ثم نوقف الرئيسي، ثم نكسر التصميم عن قصد. والأرقام للتوضيح فقط على جهاز تطوير بثمانية أنوية و32 GB، وليست قياس أداء.

### تشغيل الأعضاء الثلاثة

الملف `compose.yaml`:

```yaml
# A three-member MongoDB replica set (ArabRoot lab). Only the first member is published, on 127.0.0.1.
#   docker compose -p lab-mongo up -d
name: lab-mongo
x-mongo: &mongo
  image: mongo:8.0.32
  command: ["mongod", "--replSet", "rs0", "--bind_ip_all"]
services:
  mongo1:
    <<: *mongo
    container_name: lab-mongo-1
    hostname: mongo1
    ports: ["127.0.0.1:27117:27017"]
    volumes: ["./:/lab:ro", "m1:/data/db"]
  mongo2:
    <<: *mongo
    container_name: lab-mongo-2
    hostname: mongo2
    volumes: ["m2:/data/db"]
  mongo3:
    <<: *mongo
    container_name: lab-mongo-3
    hostname: mongo3
    volumes: ["m3:/data/db"]
volumes:
  m1:
  m2:
  m3:
```

⚠️

هذا الإعداد للمختبر فقط، فلا يوجد فيه مستخدمون ولا كلمات مرور، وأي شخص يصل إلى المنفذ يقرأ ويحذف كل شيء. في الإنتاج قم بتفعيل المصادقة بين الأعضاء بملف مفتاح Keyfile وأنشئ مستخدمين كما في [دليل Replica Set مع التحكم بالوصول](https://www.mongodb.com/docs/manual/tutorial/deploy-replica-set-with-keyfile-access-control/?ref=arabroot.io)، ولا تقم بنشر المنفذ 27017 على الإنترنت أبداً.

الآن نقوم بتشغيلها وربطها في مجموعة واحدة:

```bash
docker compose -p lab-mongo up -d
docker exec lab-mongo-1 mongosh --quiet --eval 'rs.initiate({_id:"rs0",members:[{_id:0,host:"mongo1:27017"},{_id:1,host:"mongo2:27017"},{_id:2,host:"mongo3:27017"}]})'
docker exec lab-mongo-1 mongosh --quiet --eval 'rs.status().members.map(m=>({name:m.name,state:m.stateStr}))'
```

والمخرج سوف يكون كما يلي:

```text
[
  { name: 'mongo1:27017', state: 'SECONDARY' },
  { name: 'mongo2:27017', state: 'SECONDARY' },
  { name: 'mongo3:27017', state: 'PRIMARY' }
]
```

ولاحظ أن الرئيسي هنا mongo3 وليس الأول، فالانتخاب يقرر ذلك، ولهذا يتصل التطبيق دائماً بسلسلة اتصال فيها كل الأعضاء واسم المجموعة: `mongodb://mongo1,mongo2,mongo3/shop?replicaSet=rs0`، وليس بعنوان سيرفر واحد.

### تحميل مليون طلب

نستخدم نفس المولد `gen_orders.py` من تجربة MySQL، ونحمل الملف بالأداة `mongoimport` مع تحديد نوع كل حقل:

```bash
python gen_orders.py
time docker exec lab-mongo-1 mongoimport --uri "mongodb://mongo1,mongo2,mongo3/shop?replicaSet=rs0" \
  --collection orders --type csv --columnsHaveTypes \
  --fields "_id.int64(),customer_id.int32(),country.string(),status.string(),amount.decimal(),created_at.date_go(2006-01-02 15:04:05)" \
  --file /lab/orders.csv --numInsertionWorkers 4
```

والمخرج:

```text
2026-10-11T05:25:59.336+0000	1000000 document(s) imported successfully. 0 document(s) failed to import.

real	0m59.424s
```

### نفس الأسئلة بلغة MongoDB

الملف `queries.js` يسأل نفس الأسئلة الثلاثة: المبيعات المدفوعة لكل دولة، وطلبات عميل واحد قبل الفهرس وبعده، وطلبات يوم واحد، ثم حجم المجموعة:

```javascript
// The same three questions as the SQL labs, timed three times each (ArabRoot lab).
//   docker exec lab-mongo-1 mongosh --quiet "mongodb://mongo1,mongo2,mongo3/shop?replicaSet=rs0" /lab/queries.js
const t = (label, fn) => { for (let i = 0; i < 3; i++) { const s = Date.now(); fn(); print(`${label}: ${Date.now() - s} ms`); } };
const byCountry = () => db.orders.aggregate([
  { $match: { status: 'paid' } },
  { $group: { _id: '$country', orders: { $sum: 1 }, revenue: { $sum: '$amount' } } },
  { $sort: { revenue: -1 } }
]).toArray();
t('revenue by country', byCountry);
printjson(byCountry().slice(0, 3));
const one = () => db.orders.find({ customer_id: 4242 }).toArray();
const ex = (q) => { const e = db.orders.find(q).explain('executionStats'); return { stage: e.queryPlanner.winningPlan.stage === 'FETCH' ? 'FETCH+' + e.queryPlanner.winningPlan.inputStage.stage : e.queryPlanner.winningPlan.stage, docsExamined: e.executionStats.totalDocsExamined, ms: e.executionStats.executionTimeMillis }; };
print('before index:'); printjson(ex({ customer_id: 4242 }));
db.orders.createIndex({ customer_id: 1 }); db.orders.createIndex({ created_at: 1 });
print('after index:'); printjson(ex({ customer_id: 4242 }));
t('one customer', one);
t('one day', () => db.orders.countDocuments({ created_at: { $gte: ISODate('2025-07-01'), $lt: ISODate('2025-07-02') } }));
const s = db.orders.stats();
print(`count=${s.count} dataSize=${(s.size / 1048576).toFixed(0)}MB storageSize=${(s.storageSize / 1048576).toFixed(0)}MB indexes=${(s.totalIndexSize / 1048576).toFixed(0)}MB`);
```

والمخرج سوف يكون كما يلي (اختصرنا نتيجة التجميع إلى أول دولة):

```text
revenue by country: 2472 ms
revenue by country: 2140 ms
revenue by country: 2252 ms
  { _id: 'SA', orders: 87916, revenue: Decimal128('39867208.77') },
before index:
{ stage: 'COLLSCAN', docsExamined: 1000000, ms: 1077 }
after index:
{ stage: 'FETCH+IXSCAN', docsExamined: 12, ms: 31 }
one customer: 4 ms
one customer: 14 ms
one customer: 3 ms
one day: 25 ms
one day: 20 ms
one day: 13 ms
count=1000000 dataSize=108MB storageSize=36MB indexes=46MB
```

في المخرج أعلاه لاحظ التالي:

- نتيجة التجميع مطابقة لـ MySQL وMariaDB حتى آخر هللة: 87916 طلباً و39867208.77 للسعودية، والسبب أننا حفظنا المبلغ كـ Decimal128 وليس كرقم عشري تقريبي Double.
- بدون فهرس قرأ السيرفر مليون مستند ليجد 12، وبالفهرس قرأ 12 فقط، وهذا هو الرقم `docsExamined` الذي تراقبه في كل استعلام بطيء.
- حجم البيانات قبل الضغط 108 MB، وعلى القرص 36 MB فقط، والسبب أن محرك WiredTiger يضغط المجموعات افتراضياً بـ snappy كما في [صفحة WiredTiger](https://www.mongodb.com/docs/manual/core/wiredtiger/?ref=arabroot.io).

### معاملة على مجموعتين

الملف `transaction.js` يشتري آخر قطعة في المخزون مرتين: الأولى يجب أن تنجح، والثانية يجب أن تلغى كلها دون أن يبقى طلب بلا مخزون:

```javascript
// Two collections changed together or not at all (ArabRoot lab).
const s = db.getMongo().startSession();
const inv = s.getDatabase('shop').inventory, ord = s.getDatabase('shop').orders;
db.inventory.drop(); db.inventory.insertOne({ _id: 'sku-1', stock: 1 });
const buy = (id) => {
  s.startTransaction({ writeConcern: { w: 'majority' } });
  try {
    const r = inv.updateOne({ _id: 'sku-1', stock: { $gte: 1 } }, { $inc: { stock: -1 } });
    if (r.modifiedCount !== 1) throw new Error('out of stock');
    ord.insertOne({ _id: id, customer_id: 7, country: 'SA', status: 'paid', amount: NumberDecimal('50'), created_at: new Date() });
    s.commitTransaction(); print(`order ${id}: committed`);
  } catch (e) { s.abortTransaction(); print(`order ${id}: aborted (${e.message})`); }
};
buy(2000001); buy(2000002);
printjson({ stock: db.inventory.findOne().stock, order2000002: db.orders.countDocuments({ _id: 2000002 }) });
```

والمخرج:

```text
order 2000001: committed
order 2000002: aborted (out of stock)
{
  stock: 0,
  order2000002: 0
}
```

### نوقف الرئيسي

الآن نوقف mongo3 وهو الرئيسي، ونكتب كل بضع ثوان بنفس سلسلة الاتصال:

```bash
U="mongodb://mongo1,mongo2,mongo3/shop?replicaSet=rs0"
docker stop lab-mongo-3
for i in 1 2 3 4 5 6; do sleep 2; docker exec lab-mongo-1 mongosh --quiet $U --eval 'const s=Date.now(); db.orders.insertOne({_id: 3000000+'$i', status:"paid"}); print(new Date().toISOString().slice(11,19)+" insert ok, primary="+db.hello().primary+" ("+(Date.now()-s)+" ms)")'; done
docker exec lab-mongo-1 mongosh --quiet $U --eval 'rs.status().members.map(m=>m.name+" "+m.stateStr).join("\n")'
```

والمخرج سوف يكون كما يلي (توقف mongo3 الساعة 05:27:14 بتوقيت UTC):

```text
05:27:18 insert ok, primary=mongo1:27017 (308 ms)
05:27:24 insert ok, primary=mongo1:27017 (47 ms)
05:27:33 insert ok, primary=mongo1:27017 (123 ms)
05:27:39 insert ok, primary=mongo1:27017 (145 ms)
05:27:46 insert ok, primary=mongo1:27017 (39 ms)
05:27:52 insert ok, primary=mongo1:27017 (26 ms)
mongo1:27017 PRIMARY
mongo2:27017 SECONDARY
mongo3:27017 (not reachable/healthy)
```

أول كتابة بعد أربع ثوان نجحت على الرئيسي الجديد mongo1\. ولاحظ أن `docker stop` إيقاف مرتب، فالرئيسي يتنحى Step Down بنفسه قبل أن يتوقف، أما إذا انقطعت الكهرباء عنه فالأعضاء ينتظرون مهلة الانتخاب (10 ثوان افتراضياً) قبل أن يبدؤوا، ولذلك خطط لتطبيقك على أن الكتابة قد تتوقف عشر ثوان أو أكثر، والمكتبات الحديثة تعيد المحاولة مرة واحدة تلقائياً Retryable Writes.

### نكسرها: تعليقات بلا حد داخل المنشور

لنفرض أن لدينا مدونة، وقمنا بالحل البسيط: كل منشور مستند، وتعليقاته مصفوفة داخله، فهذا يبدو منطقياً لأننا نعرض المنشور مع تعليقاته دائماً. والملف `unbounded.js` يضيف تعليقات بحجم 1 KB تقريباً إلى منشور واحد، ويقيس بعد كل دفعة حجم المستند، ووقت إضافة تعليق واحد، ووقت قراءة المنشور، ويفعل نفس الشيء مع التصميم الآخر: مجموعة مستقلة للتعليقات مع فهرس على `post_id` والوقت:

```javascript
// Break it: comments embedded in one post document until the 16 MB limit, against comments in their own collection.
//   docker exec lab-mongo-1 mongosh --quiet "mongodb://mongo1,mongo2,mongo3/blog?replicaSet=rs0" /lab/unbounded.js
db.posts.drop(); db.comments.drop();
db.comments.createIndex({ post_id: 1, at: -1 });
const text = 'x'.repeat(1000);                       // a 1 KB comment
const comment = (i) => ({ user: 'user' + (i % 500), text, at: new Date(), likes: 0 });
db.posts.insertOne({ _id: 1, title: 'popular post', comments: [] });
const ms = (fn, n = 20) => { const s = Date.now(); for (let i = 0; i < n; i++) fn(); return ((Date.now() - s) / n).toFixed(1); };
let total = 0;
print('comments | post size | push 1 (ms) | read post (ms) | separate: insert 1 (ms) | separate: last 20 (ms)');
for (const target of [1000, 4000, 8000, 12000, 15000, 16000, 17000]) {
  const add = target - total, batch = [];
  for (let i = 0; i < add; i++) batch.push(comment(total + i));
  try { db.posts.updateOne({ _id: 1 }, { $push: { comments: { $each: batch } } }); }
  catch (e) { print(`${target}: push failed: ${e.codeName || ''} ${e.message.slice(0, 120)}`); break; }
  db.comments.insertMany(batch.map(c => ({ ...c, post_id: 1 })));
  total = target;
  const size = db.posts.aggregate([{ $match: { _id: 1 } }, { $project: { s: { $bsonSize: '$$ROOT' } } }]).next().s;
  const push1 = ms(() => db.posts.updateOne({ _id: 1 }, { $push: { comments: comment(0) } }), 5); total += 5;
  db.comments.insertMany([0, 1, 2, 3, 4].map(i => ({ ...comment(i), post_id: 1 })));
  const read = ms(() => db.posts.findOne({ _id: 1 }), 5);
  const ins = ms(() => db.comments.insertOne({ ...comment(0), post_id: 2 }), 5);
  const last = ms(() => db.comments.find({ post_id: 1 }).sort({ at: -1 }).limit(20).toArray(), 5);
  print(`${total} | ${(size / 1048576).toFixed(2)} MB | ${push1} | ${read} | ${ins} | ${last}`);
}
```

والمخرج سوف يكون كما يلي:

```text
comments | post size | push 1 (ms) | read post (ms) | separate: insert 1 (ms) | separate: last 20 (ms)
1005 | 1.01 MB | 8.2 | 14.0 | 5.6 | 3.0
4005 | 4.05 MB | 24.4 | 65.8 | 8.0 | 3.6
8005 | 8.11 MB | 56.8 | 73.4 | 6.2 | 3.0
12005 | 12.16 MB | 64.2 | 151.4 | 6.0 | 3.8
15005 | 15.21 MB | 74.4 | 336.4 | 24.4 | 4.0
16000: push failed:  BSONObj size: 17009423 (0x1038B0F) is invalid. Size must be between 0 and 16793600(16MB) First element: _id: 1
```

والصورة التالية تبين نفس الأرقام:

![رسم أعمدة لوقت قراءة المنشور بالملي ثانية حسب عدد التعليقات المضمنة: 14 عند 1005 تعليقات بحجم 1.01 MB، و66 عند 4005، و73 عند 8005، و151 عند 12005، و336 عند 15005 بحجم 15.21 MB بلون برتقالي، وبجانبه صندوق أحمر مكتوب فيه أن إضافة الدفعة حتى 16000 فشلت بخطأ BSONObj size 17009423 invalid، وصندوق أخضر لمجموعة التعليقات المستقلة بفهرس post_id وat وقراءة آخر 20 تعليقاً في 3 إلى 4 ملي ثانية في كل الأحجام](https://arabroot.io/content/images/2026/10/mongodb-02-unbounded.webp)

كل تعليق يجعل قراءة المنشور أثقل، وعند 16 ألف تعليق يرفض MongoDB الكتابة، أما المجموعة المستقلة فثابتة

في المخرج أعلاه لاحظ التالي:

- قراءة المنشور كبرت من 14 ملي ثانية إلى 336، أي 24 ضعفاً، والسبب أن كل قراءة للمنشور تنقل كل التعليقات حتى لو كانت الصفحة تعرض آخر عشرين فقط.
- إضافة تعليق واحد كبرت من 8.2 إلى 74.4 ملي ثانية، أي أن الكتابة الصغيرة أصبحت أثقل كلما كبر المستند.
- عند الدفعة التي كانت سترفع المستند إلى 17009423 بايت رفض السيرفر الكتابة كلها، وهذا يعني في الإنتاج أن أشهر منشور عندك هو الذي يتوقف عن قبول التعليقات، وفي أسوأ وقت.
- في التصميم الثاني بقيت قراءة آخر عشرين تعليقاً بين 3 و4 ملي ثانية مهما كبر العدد، لأن الفهرس يقرأ عشرين مستنداً فقط.

والحل الصحيح إذن مجموعة مستقلة للتعليقات كما في التجربة، أو نمط المجموعة الجزئية Subset Pattern الذي تذكره صفحة المصفوفات بلا حد: تضع في المنشور عدداً صغيراً ثابتاً من التعليقات للعرض السريع، والباقي في مجموعة مستقلة، مع ثمن تكرار البيانات عند تعديلها.

### نفس السكربتات على FerretDB

للمقارنة شغلنا FerretDB 2.7.0 على PostgreSQL 17 مع إضافة DocumentDB، وهو نفس الإعداد الموجود في [توثيق FerretDB](https://docs.ferretdb.io/installation/ferretdb/docker/?ref=arabroot.io) مع ربط المنفذ بالعنوان 127.0.0.1، وحملنا نفس مليون طلب بنفس أمر `mongoimport`:

```yaml
# FerretDB 2 on PostgreSQL with the DocumentDB extension, to run the same scripts (ArabRoot lab).
#   docker compose -p lab-mongo-ferret -f compose-ferretdb.yaml up -d
name: lab-mongo-ferret
services:
  postgres:
    image: ghcr.io/ferretdb/postgres-documentdb:17-0.107.0-ferretdb-2.7.0
    container_name: lab-mongo-ferret-pg
    environment:
      POSTGRES_USER: lab
      POSTGRES_PASSWORD: lab-pass
      POSTGRES_DB: postgres
    volumes: ["pg:/var/lib/postgresql/data"]
  ferretdb:
    image: ghcr.io/ferretdb/ferretdb:2.7.0
    container_name: lab-mongo-ferret-app
    environment:
      FERRETDB_POSTGRESQL_URL: postgres://lab:lab-pass@postgres:5432/postgres
    ports: ["127.0.0.1:27217:27017"]
  tools:
    image: mongo:8.0.32
    container_name: lab-mongo-ferret-tools
    command: ["sleep", "infinity"]
    volumes: ["./:/lab:ro"]
volumes:
  pg:
```

والنتائج كانت كما يلي:

```text
1000000 document(s) imported successfully. 0 document(s) failed to import.
real	0m28.966s
revenue by country: 1765 ms
revenue by country: 1894 ms
revenue by country: 1797 ms
one customer, no index: 855 ms
plan: Bitmap Heap Scan Bitmap Index Scan customer_id_1
one customer: 5 ms
one day: 19 ms
storageSize=166MB indexes=124MB
```

ولكن المعاملة من الملف `transaction.js` لم تعمل، والمخرج كان:

```text
CommandNotFound 59 no such command: 'commitTransaction'
```

في المخرج أعلاه لاحظ التالي:

- الاستعلامات العادية والتجميع والفهارس عملت دون أي تغيير في السكربت، والأمر `explain` أعاد خطة PostgreSQL نفسها (Bitmap Index Scan) بدل خطة MongoDB، ولذلك تعطل الجزء من `queries.js` الذي يقرأ شكل خطة MongoDB.
- المعاملات على أكثر من مستند غير مدعومة في هذا الإصدار، فإذا كان تطبيقك يعتمد عليها فFerretDB لا يناسبه اليوم.
- الحجم على القرص 166 MB للبيانات و124 MB للفهارس، مقابل 36 MB و46 MB في MongoDB مع الضغط.

### حذف المختبر

```bash
docker compose -p lab-mongo down -v
docker compose -p lab-mongo-ferret -f compose-ferretdb.yaml down -v
rm orders.csv
```

## النسخ الاحتياطي والمراقبة

- **النسخ الاحتياطي:** الأداة [mongodump](https://www.mongodb.com/docs/database-tools/mongodump/?ref=arabroot.io) مع الخيار `--oplog` تعطيك نسخة متسقة من Replica Set، وهي مناسبة لقواعد صغيرة ومتوسطة، وللكبيرة خذ Snapshot من قرص عضو ثانوي. وفي كل الحالات طبق قاعدة [3-2-1 واختبار الاستعادة](https://arabroot.io/articles/%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D8%A7%D9%84%D8%A7%D8%AD%D8%AA%D9%8A%D8%A7%D8%B7%D9%8A-%D8%A8%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-3-2-1-%D9%88%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B9%D8%A7%D8%AF%D8%A9/)، فالعضوان الثانويان ليسا نسخة احتياطية، لأن الحذف بالخطأ ينسخ إليهما خلال ثانية.
- **الذاكرة:** ذاكرة WiredTiger الافتراضية هي الأكبر بين 50% من (الذاكرة ناقص 1 GB) و256 MB، فإذا شغلت أكثر من عضو على نفس الجهاز كما في المختبر فحدد `--wiredTigerCacheSizeGB` لكل واحد، والسبب أن كل عضو يحسب الذاكرة كأنه وحده. وفي التجربة استهلك كل عضو قرابة 0.9 إلى 1 GB بعد التحميل.
- **المراقبة:** راقب تأخر الأعضاء الثانوية Replication Lag من `rs.printSecondaryReplicationInfo()`، والاستعلامات البطيئة من سجل Profiler، وعدد `docsExamined` في الاستعلامات المهمة.

## متى يناسبك MongoDB ومتى لا يناسبك؟

**نقاط القوة:** مستندات تطابق كائنات الكود دون طبقة تحويل، وحقول تتغير دون Migration، وReplica Set وانتقال تلقائي مدمجان ويعملان من أول يوم كما رأينا، وتوزيع على عدة سيرفرات Sharding مدمج عندما تحتاجه، وضغط جيد على القرص.

**نقاط الضعف:** الرخصة SSPL ليست مفتوحة المصدر ولا تجده في مستودعات التوزيعات، والعلاقات الكثيرة بين البيانات أصعب من SQL، والمعاملات موجودة ولكنها أغلى، والسيرفر لا يحمي شكل البيانات إلا إذا أضفت قواعد التحقق، وأخطاء التصميم مثل المصفوفة بلا حد لا تظهر إلا متأخراً.

- **يناسبك:** كتالوج منتجات بخصائص تختلف من منتج لآخر، ومحتوى وصفحات CMS، وبيانات أحداث وسجلات بأشكال متنوعة، وتطبيق فريقه يعرف MongoDB جيداً.
- **لا يناسبك:** نظام مالي أو محاسبي فيه علاقات ومعاملات كثيرة كما في [مقال تصميم قواعد البيانات](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D8%B9-%D9%88%D8%A7%D9%84%D8%AA%D9%88%D8%B3%D8%B9/)، وتقارير تحليلية ثقيلة (ClickHouse أنسب كما في [مقال ClickHouse](https://arabroot.io/articles/clickhouse-%D9%84%D9%84%D8%AA%D8%AD%D9%84%D9%8A%D9%84%D8%A7%D8%AA-%D8%B9%D9%84%D9%89-%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D8%B6%D8%AE%D9%85%D8%A9/))، وأي حالة يكفيك فيها عمود JSONB في PostgreSQL الذي تشغله أصلاً.

والمقارنة الكاملة مع PostgreSQL وMySQL على نفس البيانات في مقال [PostgreSQL أم MySQL أم MongoDB؟](https://arabroot.io/articles/postgresql-%D8%A7%D9%85-mysql-%D8%A7%D9%85-mongodb/).

## أخطاء شائعة

- **مصفوفة بلا حد داخل مستند:** تعليقات أو قراءات أو سجلات، والنتيجة رأيناها: قراءة أبطأ 24 مرة ثم رفض الكتابة عند 16 MB.
- **سيرفر منفرد في الإنتاج:** بلا انتقال تلقائي ولا معاملات، فابدأ بـ Replica Set من ثلاثة أعضاء حتى لو كانت البيانات صغيرة.
- **الاتصال بعنوان عضو واحد:** عندما يتغير الرئيسي يفشل التطبيق، فاستخدم سلسلة الاتصال بكل الأعضاء واسم المجموعة.
- **تشغيل MongoDB دون مصادقة على منفذ مفتوح:** قم بتفعيل المصادقة وملف المفتاح، واربط المنفذ بالشبكة الداخلية فقط.
- **حفظ المبالغ كرقم Double:** احفظها Decimal128 كما في التجربة، حتى تطابق المجاميع ما في نظام المحاسبة.
- **اعتبار الأعضاء الثانوية نسخة احتياطية:** الحذف ينسخ إليها فوراً.

## الخلاصة

- MongoDB يحفظ كل سجل مستنداً كاملاً، والكتابة على المستند الواحد ذرية، والحد الأقصى للمستند 16 MB.
- ضع داخل المستند ما تقرؤه معاً وله حد معروف، واستخدم الإشارة لما يكبر بلا حد، وفي التجربة كبرت قراءة المنشور من 14 إلى 336 ملي ثانية ثم رفض السيرفر الكتابة عند قرابة 16 ألف تعليق، وبقيت المجموعة المستقلة بين 3 و4 ملي ثانية.
- على مليون طلب أعطى التجميع نفس أرقام MySQL، والفهرس خفض المستندات المقروءة من مليون إلى 12، والضغط جعل 108 MB على القرص 36 MB.
- Replica Set بثلاثة أعضاء انتخب رئيساً جديداً وكتبنا عليه بعد أربع ثوان من الإيقاف المرتب.
- SSPL لا تؤثر على استخدامه قاعدة بيانات لتطبيقك، ولكنها ليست رخصة مفتوحة، وFerretDB بديل مفتوح على PostgreSQL ما زال دون معاملات وتطويره بطيء منذ نهاية 2025.

[![](https://arabroot.io/content/images/2026/10/db-types-cover-cover.webp)دليلأنواع قواعد البيانات ومتى تختار كل نوعالفرق بين قواعد البيانات العلائقية والمستندات والمفتاح والقيمة وغيرها، وأي الأسئلة تجيب عنها كل عائلة بسهولة.](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%88%D9%85%D8%AA%D9%89-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D9%83%D9%84-%D9%86%D9%88%D8%B9/)[![](https://arabroot.io/content/images/2026/10/postgres-for-everything-cover-cover.webp)دليلPostgreSQL لكل شيء: متى يكفيك ومتى تحتاج غيره؟كم عملاً يستطيع PostgreSQL واحد أن يقوم به في شركتك الناشئة قبل أن تحتاج Redis أو Elasticsearch، وما الإشارة التي تقول لك إن الوقت قد حان.](https://arabroot.io/articles/postgresql-%D9%84%D9%83%D9%84-%D8%B4%D9%8A%D8%A1-%D9%85%D8%AA%D9%89-%D8%AA%D9%83%D9%81%D9%8A%D9%83-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%88%D8%A7%D8%AD%D8%AF%D8%A9/)[![](https://arabroot.io/content/images/2026/10/db-clustering-cover-cover.webp)دليلعناقيد قواعد البيانات Database Clustering ومتى تحتاجهاماذا تعني كلمة Cluster في كل قاعدة بيانات، ومتى تحتاج نسخاً أو انتقالاً تلقائياً، ومتى يكفيك سيرفر واحد مع نسخ احتياطية جيدة.](https://arabroot.io/articles/%D8%B9%D9%86%D8%A7%D9%82%D9%8A%D8%AF-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/postgres-mysql-mongodb-cover-cover.webp)دليلPostgreSQL أم MySQL أم MongoDB؟مقارنة عادلة بين أشهر ثلاث قواعد بيانات على نفس البيانات، وأسئلة بسيطة تساعدك على اختيار ما يناسب مشروعك.](https://arabroot.io/articles/postgresql-%D8%A7%D9%85-mysql-%D8%A7%D9%85-mongodb/)[![](https://arabroot.io/content/images/2026/10/db-admin-ui-cover-cover.webp)دليلواجهات إدارة قواعد البيانات: pgAdmin وAdminer وCloudBeaverكيف تختار واجهة ويب لإدارة قاعدة بياناتك وتشغلها، ولماذا يجب ألا تصل إليها من الإنترنت إلا عبر باب واحد محمي.](https://arabroot.io/articles/%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-pgadmin-%D9%88-adminer-%D9%88-cloudbeaver/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، والتجربة على MongoDB 8.0.32 وFerretDB 2.7.0 مع postgres-documentdb 17-0.107.0 في Docker Engine 29.8.0، والمعلومات من توثيق MongoDB وصفحة أسئلة SSPL ومستودعي FerretDB وDocumentDB كما قرأناها في 11 أكتوبر 2026.