الفهارس في MongoDB: createIndex و explain
مجموعة فيها سبع استعارات تُفحص في لحظة. لكن مكتبة حقيقية فيها ملايين السجلات، وكل بحث عن استعارات عضو واحد يجبر MongoDB على قراءة المجموعة كلها. الحل هو الفهرس (Index): بنية صغيرة مرتبة تدل MongoDB على مكان المستندات المطلوبة مباشرة. في هذا الدرس ستتعلم إنشاء الفهارس، والتأكد من أنها تُستخدم فعلاً.
المشكلة: فحص المجموعة كلها
Section titled “المشكلة: فحص المجموعة كلها”بدون فهرس، لتنفيذ find({ member: 'هدى' }) تقرأ MongoDB كل مستند وتسأل: هل العضو هدى؟ هذا اسمه فحص المجموعة (Collection Scan).
الفهرس مثل فهرس آخر الكتاب: قائمة مرتبة بالكلمات وبجانب كل كلمة أرقام صفحاتها. بدل تقليب الكتاب كله، تبحث في القائمة المرتبة وتقفز للصفحة.
explain: ماذا فعلت MongoDB؟
Section titled “explain: ماذا فعلت MongoDB؟”أضف .explain('executionStats') لأي استعلام، فتحصل على تقرير تنفيذه بدل نتيجته. التقرير طويل، وهذه أهم حقوله:
db.loans.find({ member: 'هدى' }).explain('executionStats')stage: 'COLLSCAN'nReturned: 3totalKeysExamined: 0totalDocsExamined: 7COLLSCAN: فحص كامل للمجموعة.nReturned: 3: عدد النتائج.totalDocsExamined: 7: عدد المستندات التي قُرئت.
قرأنا سبعة مستندات لنُرجع ثلاثة. القاعدة: كلما اقترب totalDocsExamined من nReturned كان الاستعلام أكفأ.
إنشاء فهرس
Section titled “إنشاء فهرس”db.loans.createIndex({ member: 1 })member_1{ member: 1 } تعني فهرساً على الحقل member بترتيب تصاعدي، و -1 للتنازلي. والرد هو الاسم الذي أعطته MongoDB للفهرس. نعيد الاستعلام نفسه:
stage: 'IXSCAN'indexName: 'member_1'nReturned: 3totalKeysExamined: 3totalDocsExamined: 3IXSCAN تعني بحثاً في الفهرس. قُرئت ثلاثة مستندات فقط لإرجاع ثلاثة: لا مجهود ضائعاً. والاستعلام نفسه لم يتغير: MongoDB تختار الفهرس المناسب وحدها.
عرض الفهارس
Section titled “عرض الفهارس”db.loans.getIndexes()[ { v: 2, key: { _id: 1 }, name: '_id_' }, { v: 2, key: { member: 1 }, name: 'member_1' }]الفهرس الأول _id_ موجود في كل مجموعة منذ إنشائها. لهذا البحث بـ _id سريع دائماً، ولهذا لا يتكرر _id أبداً.
الفهرس الفريد
Section titled “الفهرس الفريد”الخيار unique يجعل الفهرس يمنع تكرار القيمة، مثل البريد الإلكتروني للأعضاء:
db.members.createIndex({ email: 1 }, { unique: true })E11000 duplicate key error collection: library.members index: email_1 dup key: { email: "[email protected]" }البريد مسجّل من قبل، فرُفضت الإضافة بالخطأ E11000 الذي رأيته في درس الإضافة مع _id المكرر. تذكّر أن MongoDB لا تفرض مخططاً على المستندات، فالفهرس الفريد من الطرق القليلة لفرض قاعدة على البيانات.
الفهرس المركّب
Section titled “الفهرس المركّب”استعلام شائع في مكتبتنا: استعارات مدينة معيّنة مدتها أكثر من حد معيّن. فهرس واحد على الحقلين يخدمه:
db.loans.createIndex({ city: 1, days: -1 })db.loans.find({ city: 'القاهرة', days: { $gte: 10 } }).explain('executionStats')stage: 'IXSCAN'indexName: 'city_1_days_-1'nReturned: 3totalDocsExamined: 3لكن ترتيب الحقول مهم. الفهرس مرتب بالمدينة أولاً، ثم بالأيام داخل كل مدينة، مثل دليل هاتف مرتب باسم العائلة ثم الاسم الأول. البحث بالأيام وحدها لا يستفيد منه:
db.loans.find({ days: { $gte: 10 } }).explain('executionStats')stage: 'COLLSCAN'nReturned: 5totalDocsExamined: 7القاعدة: الفهرس المركّب يخدم الاستعلامات التي تبدأ بحقوله من اليسار: city وحدها، أو city مع days.
والفهرس يسرّع الترتيب أيضاً. الاستعلام find({ city: 'القاهرة' }).sort({ days: -1 }) يقرأ النتائج من الفهرس مرتبة جاهزة، دون مرحلة ترتيب إضافية في الذاكرة.
الفهرس النصي
Section titled “الفهرس النصي”للبحث عن كلمة داخل نص طويل يوجد نوع خاص:
db.books.createIndex({ title: 'text' })db.books.find({ $text: { $search: 'البرمجة' } }, { title: 1, _id: 0 })[ { title: 'البرمجة للمبتدئين' } ]الفهرس النصي يقسّم العناوين إلى كلمات ويفهرس كل كلمة. والمعامل $text يبحث فيها. مناسب لمربع بحث بسيط في موقعك.
حذف الفهرس
Section titled “حذف الفهرس”db.loans.dropIndex('city_1_days_-1'){ nIndexesWas: 3, ok: 1 }الحذف بالاسم، ولا يمس المستندات. والفهرس الوحيد الذي لا يُحذف هو _id_: المحاولة تعطي cannot drop _id index.
الفهارس ليست مجانية
Section titled “الفهارس ليست مجانية”لماذا لا نفهرس كل الحقول؟
- الكتابة تبطؤ: مع كل
insertوupdateوdeleteتحدّث MongoDB المجموعة وكل فهارسها. - الذاكرة: الفهارس تعمل بأفضل أداء حين تتسع لها الذاكرة، وكل فهرس يأخذ نصيبه.
| فهرِس | لا تفهرِس |
|---|---|
| حقول تبحث بها كثيراً | المجموعات الصغيرة |
| حقول الترتيب المتكرر | حقول قليلة التنوع مثل true/false |
| حقول يجب ألا تتكرر (فريد) | حقول نادراً ما تُستخدم في البحث |
جرّب بنفسك
Section titled “جرّب بنفسك”- افحص الاستعلام
db.loans.find({ fee: { $gt: 0 } })بـexplain('executionStats')وسجّل قيمةtotalDocsExamined. - أنشئ فهرساً على
fee، وأعد الفحص وقارن. - اعرض الفهارس بـ
getIndexes()، ثم احذف فهرسfee. - تحدٍّ: أنشئ فهرساً مركّباً على
{ member: 1, days: -1 }، واكتب استعلاماً يستفيد منه وآخر لا يستفيد، وتحقق بـexplain.
أخطاء شائعة
Section titled “أخطاء شائعة”- فهرسة كل شيء: تبطئ الكتابة وتملأ الذاكرة. فهرِس ما تحتاجه استعلاماتك فقط.
- ترتيب خاطئ في الفهرس المركّب:
{ city: 1, days: -1 }لا يخدم البحث بـdaysوحدها. - فهرس فريد على بيانات مكررة: إذا كانت المجموعة تحتوي قيماً مكررة أصلاً يفشل إنشاء الفهرس بالخطأ
E11000. نظّف البيانات أولاً. - افتراض أن الفهرس مستخدَم: تحقق بـ
explainدائماً.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ماذا تعني COLLSCAN في ناتج explain؟
COLLSCAN فحص كامل للمجموعة، وهو علامة على أن الاستعلام يحتاج فهرساً.
سؤال
أي فهرس موجود تلقائياً في كل مجموعة؟
كل مجموعة لها فهرس فريد على _id لا يمكن حذفه.
الخلاصة
Section titled “الخلاصة”- بدون فهرس تقرأ MongoDB المجموعة كلها (
COLLSCAN)، ومع الفهرس تقفز للمطلوب (IXSCAN). createIndex({ field: 1 })ينشئ فهرساً، وgetIndexes()يعرض الفهارس، وdropIndexيحذف.explain('executionStats')يكشف عدد المستندات المقروءة مقابل المُرجعة.uniqueيمنع التكرار، والفهرس المركّب يُستخدم من اليسار، وtextللبحث عن الكلمات.- الفهارس تسرّع القراءة وتبطئ الكتابة: أضفها عند الحاجة وتحقق منها.