تخطَّ إلى المحتوى

الفهارس في MongoDB: createIndex و explain

الدرس 7 من 8 · MongoDB

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

المشكلة: فحص المجموعة كلها

Section titled “المشكلة: فحص المجموعة كلها”

بدون فهرس، لتنفيذ find({ member: 'هدى' }) تقرأ MongoDB كل مستند وتسأل: هل العضو هدى؟ هذا اسمه فحص المجموعة (Collection Scan).

الفهرس مثل فهرس آخر الكتاب: قائمة مرتبة بالكلمات وبجانب كل كلمة أرقام صفحاتها. بدل تقليب الكتاب كله، تبحث في القائمة المرتبة وتقفز للصفحة.

أضف .explain('executionStats') لأي استعلام، فتحصل على تقرير تنفيذه بدل نتيجته. التقرير طويل، وهذه أهم حقوله:

db.loans.find({ member: 'هدى' }).explain('executionStats')
الناتج (أهم الحقول)
stage: 'COLLSCAN'
nReturned: 3
totalKeysExamined: 0
totalDocsExamined: 7
  • COLLSCAN: فحص كامل للمجموعة.
  • nReturned: 3: عدد النتائج.
  • totalDocsExamined: 7: عدد المستندات التي قُرئت.

قرأنا سبعة مستندات لنُرجع ثلاثة. القاعدة: كلما اقترب totalDocsExamined من nReturned كان الاستعلام أكفأ.

db.loans.createIndex({ member: 1 })
الناتج
member_1

{ member: 1 } تعني فهرساً على الحقل member بترتيب تصاعدي، و -1 للتنازلي. والرد هو الاسم الذي أعطته MongoDB للفهرس. نعيد الاستعلام نفسه:

الناتج (أهم الحقول)
stage: 'IXSCAN'
indexName: 'member_1'
nReturned: 3
totalKeysExamined: 3
totalDocsExamined: 3

IXSCAN تعني بحثاً في الفهرس. قُرئت ثلاثة مستندات فقط لإرجاع ثلاثة: لا مجهود ضائعاً. والاستعلام نفسه لم يتغير: MongoDB تختار الفهرس المناسب وحدها.

db.loans.getIndexes()
الناتج
[
{ v: 2, key: { _id: 1 }, name: '_id_' },
{ v: 2, key: { member: 1 }, name: 'member_1' }
]

الفهرس الأول _id_ موجود في كل مجموعة منذ إنشائها. لهذا البحث بـ _id سريع دائماً، ولهذا لا يتكرر _id أبداً.

الخيار unique يجعل الفهرس يمنع تكرار القيمة، مثل البريد الإلكتروني للأعضاء:

db.members.createIndex({ email: 1 }, { unique: true })
db.members.insertOne({ name: 'هدى الثانية', email: '[email protected]' })
الناتج
E11000 duplicate key error collection: library.members index: email_1 dup key: { email: "[email protected]" }

البريد مسجّل من قبل، فرُفضت الإضافة بالخطأ E11000 الذي رأيته في درس الإضافة مع _id المكرر. تذكّر أن MongoDB لا تفرض مخططاً على المستندات، فالفهرس الفريد من الطرق القليلة لفرض قاعدة على البيانات.

استعلام شائع في مكتبتنا: استعارات مدينة معيّنة مدتها أكثر من حد معيّن. فهرس واحد على الحقلين يخدمه:

db.loans.createIndex({ city: 1, days: -1 })
db.loans.find({ city: 'القاهرة', days: { $gte: 10 } }).explain('executionStats')
الناتج (أهم الحقول)
stage: 'IXSCAN'
indexName: 'city_1_days_-1'
nReturned: 3
totalDocsExamined: 3

لكن ترتيب الحقول مهم. الفهرس مرتب بالمدينة أولاً، ثم بالأيام داخل كل مدينة، مثل دليل هاتف مرتب باسم العائلة ثم الاسم الأول. البحث بالأيام وحدها لا يستفيد منه:

db.loans.find({ days: { $gte: 10 } }).explain('executionStats')
الناتج (أهم الحقول)
stage: 'COLLSCAN'
nReturned: 5
totalDocsExamined: 7

القاعدة: الفهرس المركّب يخدم الاستعلامات التي تبدأ بحقوله من اليسار: city وحدها، أو city مع days.

والفهرس يسرّع الترتيب أيضاً. الاستعلام find({ city: 'القاهرة' }).sort({ days: -1 }) يقرأ النتائج من الفهرس مرتبة جاهزة، دون مرحلة ترتيب إضافية في الذاكرة.

للبحث عن كلمة داخل نص طويل يوجد نوع خاص:

db.books.createIndex({ title: 'text' })
db.books.find({ $text: { $search: 'البرمجة' } }, { title: 1, _id: 0 })
الناتج
[ { title: 'البرمجة للمبتدئين' } ]

الفهرس النصي يقسّم العناوين إلى كلمات ويفهرس كل كلمة. والمعامل $text يبحث فيها. مناسب لمربع بحث بسيط في موقعك.

db.loans.dropIndex('city_1_days_-1')
الناتج
{ nIndexesWas: 3, ok: 1 }

الحذف بالاسم، ولا يمس المستندات. والفهرس الوحيد الذي لا يُحذف هو _id_: المحاولة تعطي cannot drop _id index.

لماذا لا نفهرس كل الحقول؟

  • الكتابة تبطؤ: مع كل insert و update و delete تحدّث MongoDB المجموعة وكل فهارسها.
  • الذاكرة: الفهارس تعمل بأفضل أداء حين تتسع لها الذاكرة، وكل فهرس يأخذ نصيبه.
فهرِس لا تفهرِس
حقول تبحث بها كثيراً المجموعات الصغيرة
حقول الترتيب المتكرر حقول قليلة التنوع مثل true/false
حقول يجب ألا تتكرر (فريد) حقول نادراً ما تُستخدم في البحث
  1. افحص الاستعلام db.loans.find({ fee: { $gt: 0 } }) بـ explain('executionStats') وسجّل قيمة totalDocsExamined.
  2. أنشئ فهرساً على fee، وأعد الفحص وقارن.
  3. اعرض الفهارس بـ getIndexes()، ثم احذف فهرس fee.
  4. تحدٍّ: أنشئ فهرساً مركّباً على { member: 1, days: -1 }، واكتب استعلاماً يستفيد منه وآخر لا يستفيد، وتحقق بـ explain.
  • فهرسة كل شيء: تبطئ الكتابة وتملأ الذاكرة. فهرِس ما تحتاجه استعلاماتك فقط.
  • ترتيب خاطئ في الفهرس المركّب: { city: 1, days: -1 } لا يخدم البحث بـ days وحدها.
  • فهرس فريد على بيانات مكررة: إذا كانت المجموعة تحتوي قيماً مكررة أصلاً يفشل إنشاء الفهرس بالخطأ E11000. نظّف البيانات أولاً.
  • افتراض أن الفهرس مستخدَم: تحقق بـ explain دائماً.

سؤال

ماذا تعني COLLSCAN في ناتج explain؟

سؤال

أي فهرس موجود تلقائياً في كل مجموعة؟

  • بدون فهرس تقرأ MongoDB المجموعة كلها (COLLSCAN)، ومع الفهرس تقفز للمطلوب (IXSCAN).
  • createIndex({ field: 1 }) ينشئ فهرساً، و getIndexes() يعرض الفهارس، و dropIndex يحذف.
  • explain('executionStats') يكشف عدد المستندات المقروءة مقابل المُرجعة.
  • unique يمنع التكرار، والفهرس المركّب يُستخدم من اليسار، و text للبحث عن الكلمات.
  • الفهارس تسرّع القراءة وتبطئ الكتابة: أضفها عند الحاجة وتحقق منها.