السلسلة: هذا الدرس جزء من سلسلة MongoDB. اقرأ المقال الرئيسي.
index MongoDB يُحوّل قراءة O(n) إلى O(log n). الفرق بين طلب في 12 مللي ثانية ونفس الطلب في 4 ثوان على نفس collection. القاعدة الكونية: كل حقل مُفلتر، مُرتَّب، أو مُستخدَم كمفتاح jointure يستحق index. هذا الدرس يفصّل العائلات السبع لـ indexes MongoDB 8.0 LTS، قاعدة ESR (Equality، Sort، Range)، وقراءة explain().
المتطلبات
- MongoDB 8.0 LTS، mongosh أو Compass
- Collection اختبار بـ 100 000 وثيقة على الأقل
- 90 دقيقة
الخطوة 1 — لماذا يُغيّر index كل شيء
// بلا index — explain() يُظهر COLLSCAN
db.commandes.find({ statut: "payee" }).explain("executionStats");
// "executionTimeMillis": 1842, "totalDocsExamined": 1000000
// إنشاء index على الحقل المُفلتَر
db.commandes.createIndex({ statut: 1 });
// مع index — explain() يُظهر IXSCAN
db.commandes.find({ statut: "payee" }).explain("executionStats");
// "executionTimeMillis": 12, "totalDocsExamined": 24500
القيمة 1 في كائن index هي الترتيب الصاعد. للطلبات بالمساواة، الاتجاه بلا أهمية؛ للترتيبات، يصير حرجاً.
الخطوة 2 — Index بسيط على حقل واحد
// Index على حقل مُفلتر متكرر
db.produits.createIndex({ categorie: 1 });
// Index مع قيد فريد — يرفض الازدواجيات
db.utilisateurs.createIndex({ email: 1 }, { unique: true });
// Index على حقل متداخل
db.produits.createIndex({ "attributs.marque": 1 });
// قائمة indexes الموجودة
db.produits.getIndexes();
إنشاء index فريد على حقل موجود يفشل إن احتوت collection ازدواجيات: E11000 duplicate key error.
الخطوة 3 — Index مركّب وقاعدة ESR
ترتيب الحقول في index ليس عشوائياً: يجب اتباع قاعدة ESR — Equality first، Sort second، Range last.
// طلب نموذجي: كل مستخدمين paying من مدينة X، مُرتَّبون بالتاريخ
db.utilisateurs.find({
ville: "Riyadh", // égalité
abonnement: "actif", // égalité
inscrit_le: { $gte: ISODate("2026-01-01") } // range
}).sort({ inscrit_le: -1 });
// Index يحترم ESR: égalités → tri → range
db.utilisateurs.createIndex(
{ ville: 1, abonnement: 1, inscrit_le: -1 }
);
ترتيب ESR يُعظّم معدّل التصفية. عكسه (range قبل égalité) يُجبر المحرّك على scan الفرع كاملاً والترتيب بعد القراءة.
الخطوة 4 — Multikey index على مصفوفات
// وثيقة نموذجية بمصفوفة tags
// { _id, titre, tags: ["mongodb", "tutoriel", "performance"] }
db.articles.createIndex({ tags: 1 }); // multikey آلي
// طلبات مغطّاة
db.articles.find({ tags: "mongodb" });
db.articles.find({ tags: { $all: ["mongodb", "perf"] } });
db.articles.find({ tags: { $in: ["mongodb", "redis"] } });
القيد: index مركّب لا يستطيع فهرسة إلا حقل مصفوفة واحد بين مفاتيحه.
الخطوة 5 — Partial index
// على 10 ملايين مستخدم، 50 000 فقط premium
db.utilisateurs.createIndex(
{ expire_le: 1 },
{ partialFilterExpression: { plan: "premium" } }
);
// الطلب الذي يستخدم index — يجب أن يحوي المعيار partiel
db.utilisateurs.find({
plan: "premium",
expire_le: { $lte: new Date() }
});
// الطلب الذي لا يستخدم index — بلا "plan: premium"
db.utilisateurs.find({ expire_le: { $lte: new Date() } });
القاعدة الذهبية: الطلب يجب أن يحوي نفس clause الـ partialFilterExpression لاستخدام index. إن نسيت، COLLSCAN صامت.
الخطوة 6 — TTL index للانتهاء الآلي
// Sessions تنتهي 24 ساعة بعد الإنشاء
db.sessions.createIndex(
{ cree_le: 1 },
{ expireAfterSeconds: 86400 }
);
// Logs محفوظة 30 يوماً
db.logs.createIndex(
{ date: 1 },
{ expireAfterSeconds: 2592000 }
);
TTL monitor يُنفَّذ كل 60 ثانية. الحذف ليس فورياً عند الانتهاء لكن يصل في الدقيقة التي تلي. الحقل المُفهرس يجب أن يكون من نوع Date.
الخطوة 7 — Text index للبحث plein-texte
// Index text على العنوان والوصف
db.produits.createIndex(
{ nom: "text", description: "text" },
{ default_language: "french", weights: { nom: 5, description: 1 } }
);
// بحث plein-texte
db.produits.find(
{ $text: { $search: "ordinateur portable" } },
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } });
// بحث دقيق
db.produits.find({ $text: { $search: "\"cle USB\"" } });
// استثناء كلمة
db.produits.find({ $text: { $search: "ordinateur -reconditionne" } });
القيد: collection لا يمكنها امتلاك إلا index text واحد، حتى إن غطّى عدة حقول. لاحتياجات متقدمة (أخطاء كتابة، lemmes)، Atlas Search أو Elasticsearch.
الخطوة 8 — explain(): القراءة التي تحسم
db.commandes.find(
{ statut: "payee", cree_le: { $gte: ISODate("2026-01-01") } }
).sort({ cree_le: -1 }).explain("executionStats");
// 5 حقول للقراءة:
// 1. winningPlan.stage -> IXSCAN أو COLLSCAN (الهدف: IXSCAN)
// 2. winningPlan.inputStage.indexName -> اسم index المُستخدَم
// 3. executionStats.executionTimeMillis -> المدة الكلية
// 4. executionStats.totalKeysExamined -> عدد إدخالات index مقروءة
// 5. executionStats.totalDocsExamined -> عدد وثائق مُجسَّدة
النسبة للمراقبة: nReturned / totalDocsExamined. إن أرجعت 100 نتيجة وفحصت 100 000 وثيقة، index لا يُميّز بما يكفي.
الخطوة 9 — صيانة ونظافة
// إحصاءات استخدام indexes
db.commandes.aggregate([{ $indexStats: {} }]);
// المخرَج: { name, key, accesses: { ops, since } }
// Index بـ accesses.ops = 0 منذ أسابيع = مرشّح للحذف
// حذف index باسمه
db.commandes.dropIndex("statut_1_cree_le_-1");
// إعادة بناء (مفيد بعد فساد أو oplog طويل)
db.commandes.reIndex();
القاعدة العملية: راجع $indexStats فصلياً واحذف ما لم يخدم. كل index جديد يجب أن يُبرَّر بطلب ملموس، لا «احتياطاً».
أخطاء شائعة
| الخطأ | السبب | الحل |
|---|---|---|
| طلب بطيء رغم index | ترتيب حقول لا يتبع ESR | أعد الترتيب: égalités → tri → range |
E11000 duplicate key |
Index فريد على حقل بازدواجيات | نظّف الازدواجيات قبل الإنشاء |
| TTL لا يطهّر | الحقل ليس Date BSON | حوّل عبر سكربت migration |
| Multikey مستحيل على مصفوفتين | قيد المحرّك | denormalize: حقل scalaire رئيسي |
| Index text غير مُستخدَم | عدة indexes text غير متوافقة | collection لها index text واحد فقط |
| RAM مشبَّعة | Index أكبر من working set | Index partiel أو sharding |
أسئلة شائعة
كم index لكل collection؟ MongoDB يأذن حتى 64. عملياً 5-12 يكفي لـ 95% من الطلبات.
هل نُفهرس _id؟ MongoDB ينشئ آلياً index فريداً على _id. غير قابل للحذف.
Index صاعد أم نازل؟ بلا أهمية للطلبات بالمساواة أو $in. حرج للترتيبات.
إنشاء index في الإنتاج بلا downtime؟ MongoDB 4.2+ ينشئ indexes خلفياً افتراضياً.
متى ننتقل إلى Atlas Search؟ حين يصير index text محدوداً: تحمّل الأخطاء، مترادفات، autocompletion. Atlas Search مشمول على M0 بحدود اختبار.