الفهرسة
الفهرسة
مع نمو أي جدول في قاعدة بيانات إنتاجية، يصبح المسح الكامل للجدول بطيئًا بشكل مؤلم. مسح 50 مليون صف للعثور على مستخدم واحد بالبريد الإلكتروني ليس خيارًا عندما يجب أن يستجيب API الخاص بك في أقل من 20 ميلي ثانية. الفهارس (Indexes) هي الأداة الأساسية التي تستخدمها قواعد البيانات للعثور على الصفوف دون قراءة كل صفحة على القرص. فهم آلية عملها — وما تكلفه — أمر ضروري لكل مهندس يصمم أنظمة على نطاق واسع.
ما هو الفهرس؟
الفهرس هو هيكل بيانات منفصل يُحتفظ به بجانب الجدول، يربط قيم الأعمدة بالموقع الفعلي (رقم الصفحة وإزاحة الصف) للصفوف المطابقة. فكّر فيه كفهرس الكتاب في نهايته: بدلاً من قراءة كل صفحة للعثور على "B-tree"، تبحث في الفهرس وتنتقل مباشرة إلى الصفحة الصحيحة.
بدون فهرس، تجري قاعدة البيانات مسحًا كاملاً للجدول — تقرأ كل صف لتقييم شرط WHERE. مع وجود فهرس على العمود المبحوث، تنفذ بحثًا في الفهرس — تتنقل في هيكل الفهرس في خطوات O(log n) وتجلب الصفوف المطابقة فحسب.
الـ B-Tree: كيف تعمل معظم الفهارس
يعتمد B-tree (الشجرة المتوازنة) على هيكل البيانات الذي يُشكّل الفهرس الافتراضي في PostgreSQL وMySQL (InnoDB) وOracle وSQL Server. هو هيكل متوازن ذاتيًا، بمعنى أن كل ورقة تقع على نفس العمق بغض النظر عن عدد الصفوف المُدرَجة أو المحذوفة. جدول بـ50 مليون صف يمتلك B-tree بعمق 4-5 مستويات تقريبًا — وهذا يعني أن العثور على أي صف لا يتطلب أكثر من 4-5 قراءات صفحة.
يتكون B-tree من ثلاثة أنواع من العقد:
- العقدة الجذر (Root node) — نقطة الدخول الوحيدة أعلى الشجرة.
- العقد الداخلية (Internal nodes) — توجّه عمليات البحث نحو الشجرة الفرعية الصحيحة عبر مقارنة قيم المفاتيح.
- عقد الأوراق (Leaf nodes) — تخزن مفتاح الفهرس الفعلي مع مؤشر لصف الجدول (معرّف الصف أو رقم الصفحة+الفتحة). ترتبط العقد الورقية ببعضها في قائمة مزدوجة الارتباط لتمكين مسح النطاقات (مثل
WHERE created_at BETWEEN …) بكفاءة دون العودة إلى الجذر.
أنواع الفهارس
إلى جانب B-tree القياسي، تقدم قواعد البيانات عدة أنواع متخصصة من الفهارس:
- فهرس Hash — يخزن خريطة هاش غير مرتبة من المفتاح إلى الصف. سريع جدًا لعمليات البحث عن تطابق تام (O(1))، لكنه عديم الفائدة لاستعلامات النطاق أو الترتيب.
- الفهرس المركّب (Composite index) — B-tree مبني على عمودين أو أكثر. يجب أن تستخدم الاستعلامات البادئة اليسرى للفهرس. فهرس على
(last_name, first_name)يفيد الاستعلام الذي يصفّي علىlast_nameوحده، لكنه لا يفيد من يصفّي علىfirst_nameوحده. - الفهرس الشامل (Covering index) — فهرس يحتوي على جميع الأعمدة التي يحتاجها الاستعلام، بحيث لا تضطر قاعدة البيانات لقراءة جدول الكومة (heap) أصلًا. يُنجز الاستعلام كاملاً من الفهرس — وهو مكسب كبير للجداول ذات معدل القراءة العالي.
- الفهرس الجزئي (Partial index) — فهرس على مجموعة فرعية من الصفوف (مثل
WHERE status = 'active'). أصغر حجمًا وأسرع بكثير حين تستعلم دائمًا عن السجلات النشطة فحسب. - فهرس النص الكامل (Full-text index) — فهرس مقلوب مبني للبحث بالكلمات المفتاحية داخل أعمدة النص. محرّك مختلف عن B-trees.
المقايضة: التكلفة في الكتابة والمساحة
الفهارس ليست مجانية. كل فهرس تضيفه يفرض ثلاثة تكاليف:
- مساحة إضافية على القرص — B-tree على جدول بـ50 مليون صف مع عمود
VARCHAR(255)للبريد الإلكتروني يمكن أن يستهلك عدة غيغابايت بسهولة. - عبء الكتابة — عند كل
INSERTأوUPDATEأوDELETE، يجب على قاعدة البيانات تحديث كل فهرس يغطي الأعمدة المُعدَّلة. جدول بـ8 فهارس يتطلب ضعف الجهد تقريبًا مقارنةً بجدول بلا فهارس. في الجداول ذات الكتابة المكثفة (مثل جداول أحداث السلاسل الزمنية التي تستوعب 100 ألف صف/ثانية) يمكن أن تُرهق الفهارس الزائدة عمليات الإدخال/الإخراج. - التنافس على الأقفال — يمكن أن تصبح صفحات الفهرس نقاط ازدحام، خاصة في أقصى يمين ورقة متسلسل متزايد باستمرار.
EXPLAIN ANALYZE في PostgreSQL أو EXPLAIN في MySQL للتأكد من استخدام الفهرس فعليًا قبل إضافته، وراجع دوريًا الفهارس غير المستخدمة واحذفها.
الفهارس المجمّعة مقابل غير المجمّعة
يحدد الفهرس المجمّع (Clustered index) الترتيب الفعلي للصفوف على القرص. في InnoDB (المحرك الافتراضي في MySQL) يكون المفتاح الأساسي دائمًا فهرسًا مجمّعًا — الجدول هو شجرة B-tree نفسها. للوصول إلى صف عبر المفتاح الأساسي تكفي عملية اجتياز واحدة للشجرة. أما الفهارس الثانوية في InnoDB فتخزن قيمة المفتاح الأساسي كمؤشر للصف، لذا تتطلب عمليتَي اجتياز: الأولى في الفهرس الثانوي للحصول على المفتاح الأساسي، ثم الثانية في الفهرس المجمّع لجلب الصف الكامل.
AUTO_INCREMENT أو BIGSERIAL) أو UUIDs المرتبة زمنيًا (UUIDv7) تُبقي الإدراج عند أقصى يمين الورقة، مما يُقلل انقسامات الصفحات وتضخيم الكتابة.
إرشادات عملية
- أضف فهرسًا على كل عمود مفتاح خارجي (Foreign Key) — أعمدة FK بلا فهرس تُسبب مسحًا كاملاً عند كل ضم (join).
- أضف فهرسًا على الأعمدة المستخدمة في جمل WHERE وJOIN ON وORDER BY للاستعلامات البطيئة المتكررة.
- فضّل الفهارس المركّبة على الفهارس الفردية المتعددة حين تصفّي الاستعلامات دائمًا على مجموعة ثابتة من الأعمدة.
- استخدم الفهارس الجزئية للمجموعات المصفّاة ذات الكثافة المنخفضة (مثل
WHERE processed = false). - راقب مخطط الاستعلام: إذا اختارت قاعدة البيانات المسح الكامل رغم وجود فهرس، فربما إحصائيات الجدول قديمة — شغّل
ANALYZEلتحديثها.
EXPLAIN ANALYZE <استعلامك> في PostgreSQL (أو EXPLAIN FORMAT=JSON في MySQL) وانظر إلى الصفوف الفعلية التي مُسحت، وعقدة الخطة المختارة، ووقت التنفيذ. هذه العادة البسيطة التي تستغرق 30 ثانية توفر ساعات من التخمين وتمنع إضافة فهارس غير ضرورية.
ملخص
الفهارس هي أكثر أدوات الضبط تأثيرًا في قواعد البيانات العلائقية. يُحوّل فهرس B-tree مسحًا بتعقيد O(n) إلى بحث بتعقيد O(log n) عبر الحفاظ على هيكل بيانات متوازن ومرتب مع مؤشرات على مستوى الأوراق. الثمن هو مساحة قرص إضافية وتضخيم الكتابة — يجب تحديث كل فهرس عند التعديل. فن الفهرسة هو إيجاد أصغر مجموعة من الفهارس التي تُلبي أنماط القراءة دون الإضرار بإنتاجية الكتابة. تحقق دائمًا من مخطط الاستعلام قبل التغييرات وبعدها.