كلاود
development

فهارس قواعد البيانات عملياً: متى تسرّع استعلامك 100 ضعف ومتى تضرّه

كلاود|١٩ سبتمبر ٢٠٢٦|6 دقائق قراءة|١١٥ مشاهدة
فهارس قواعد البيانات عملياً: متى تسرّع استعلامك 100 ضعف ومتى تضرّه

الفهرس يحوّل مسحاً كاملاً لمليون صف إلى بحث في نحو 20 خطوة، فيسرّع القراءة عشرات إلى مئات الأضعاف. لكنه يبطّئ الكتابة ويضخّم القرص، وبعض الفهارس لا تعمل أصلاً. هذا دليل بقراءة EXPLAIN وأرقام حقيقية.

الفهرس (index) هو أهم أداة أداء تملكها في قاعدة البيانات، ومتى استُخدم صح حوّل استعلاماً يمسح مليون صف إلى بحث ينتهي في نحو 20 خطوة. السبب رياضي: الفهرس بنية شجرية (B-Tree) عمقها لوغاريتمي، فمسح جدول فيه مليون صف تكلفته تقارب المليون عملية قراءة، بينما البحث في فهرس B-Tree لنفس المليون يكلّف نحو log₂(1,000,000) ≈ 20 خطوة فقط. هذا هو الفرق بين استعلام في 800 ميلي ثانية واستعلام في أقل من 5 ميلي ثانية على نفس البيانات. لكن الفهرس ليس مجانياً: كل فهرس تضيفه يبطّئ عمليات INSERT وUPDATE وDELETE لأن القاعدة تحدّث الفهرس مع كل كتابة، ويأخذ مساحة قرص إضافية، وقد يستهلك ذاكرة تحتاجها لأشياء أخرى. الجواب المباشر: فهرِس الأعمدة التي تظهر كثيراً في WHERE وJOIN وORDER BY، لكن لا تفهرس كل عمود، وتحقق دائماً بأمر EXPLAIN أن الفهرس يُستخدم فعلاً قبل أن تفترض أنه يعمل. كثير من الفهارس تُنشأ ثم لا تُستدعى أبداً بسبب طريقة كتابة الاستعلام نفسها.

القاعدة الذهبية: الفهرس يفيد القراءة ويكلّف الكتابة، فاختر بناءً على نسبة القراءة إلى الكتابة في جدولك. جدول تقارير يُقرأ آلاف المرات ويُكتب نادراً يستحق فهارس كثيرة، وجدول سجلات يُكتب باستمرار ويُقرأ قليلاً يريد فهارس قليلة مختارة بعناية.

اقرأ EXPLAIN قبل أي قرار

لا تخمّن أداء استعلام، اسأل القاعدة نفسها بأمر EXPLAIN (أو EXPLAIN ANALYZE في PostgreSQL الذي ينفّذ فعلاً ويعطي زمناً حقيقياً). خذ مثالاً واقعياً: جدول طلبات فيه مليون صف، والاستعلام:

  • SELECT * FROM orders WHERE customer_id = 4820;

قبل الفهرس، يظهر EXPLAIN نوع الوصول ALL في MySQL (أي Seq Scan في PostgreSQL) وعمود rows يقارب 1,000,000، أي أن القاعدة تقرأ كل صف لتصفية القليل. زمن التنفيذ يقارب 700 إلى 900 ميلي ثانية على قرص SSD نموذجي.

أضف الفهرس: CREATE INDEX idx_orders_customer ON orders(customer_id);. الآن يظهر EXPLAIN نوع الوصول ref وعمود rows بضعة صفوف فقط (عدد طلبات هذا العميل)، وزمن التنفيذ يهبط إلى نحو 2 إلى 5 ميلي ثانية. هذا تحسّن بأكثر من 100 ضعف بسطر واحد.

الحالةنوع الوصول (type)الصفوف المفحوصةالزمن التقريبي
بلا فهرسALL (مسح كامل)~1,000,000~800 ميلي ثانية
مع فهرس على customer_idref~12~3 ميلي ثانية

الفهرس المركّب وترتيب الأعمدة

حين يرشّح استعلامك بأكثر من عمود، الفهرس المركّب (composite index) على عدة أعمدة أقوى من فهارس مفردة منفصلة. لكن ترتيب الأعمدة داخله حاسم، والقاعدة تُعرف باسم البادئة اليسرى (leftmost prefix): الفهرس على (a, b, c) يخدم استعلامات ترشّح بـ a وحده، أو a وb، أو a وb وc، لكنه لا يخدم استعلاماً يرشّح بـ b وحده أو c وحده.

مثال: فهرس على (status, created_at) يسرّع استعلام «الطلبات المعلّقة مرتبة بالتاريخ» بامتياز، لكنه عديم الفائدة لاستعلام يرشّح بالتاريخ فقط. القاعدة العملية لترتيب الأعمدة: ضع عمود المساواة (=) قبل عمود المدى (>، <، BETWEEN)، وضع العمود الأكثر انتقائية (الذي يقلّص النتائج أكثر) أولاً كلما أمكن. عمود status فيه ثلاث قيم أقل انتقائية من عمود customer_id فيه ملايين القيم.

الفهرس المُغطّي

إن احتوى الفهرس على كل الأعمدة التي يحتاجها الاستعلام (في SELECT وWHERE)، تقرأ القاعدة الفهرس وحده دون العودة إلى الجدول، وهذا يسمى الفهرس المُغطّي (covering index). في EXPLAIN بـMySQL يظهر Using index في عمود Extra، وهو من أسرع أنماط القراءة على الإطلاق.

تكلفة الفهرس على الكتابة والقرص

لا تكتب فهارس دون حساب. كل فهرس إضافي يعني:

  • كتابة أبطأ: كل INSERT أو UPDATE على عمود مفهرس يجب أن يحدّث شجرة الفهرس أيضاً. جدول بخمسة فهارس قد تكون كتابته أبطأ بنسبة 30% إلى 50% من نفس الجدول بفهرس واحد.
  • مساحة قرص: الفهرس نسخة مرتّبة من عموده، وقد تصل الفهارس مجتمعة إلى حجم يقارب حجم البيانات نفسها أو يتجاوزه في الجداول كثيرة الفهرسة.
  • ذاكرة: القاعدة تحمّل الفهارس النشطة في الذاكرة (buffer pool) لتسريعها، وكل فهرس يزاحم غيره على هذه الذاكرة المحدودة.

فهارس لا تعمل مهما أنشأتها

أشيع سبب لبطء استعلام «مفهرس» أن الفهرس لا يُستدعى أصلاً بسبب طريقة كتابة الشرط. انتبه لهذه المصائد:

  • دالة على العمود المفهرس: شرط مثل WHERE YEAR(created_at) = 2026 يلغي الفهرس لأن القاعدة تحسب الدالة لكل صف. الحل: WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01'، فيبقى الفهرس فعّالاً.
  • LIKE ببادئة حرة: WHERE name LIKE '%خالد%' يمنع الفهرس لأن البحث لا يبدأ من أول القيمة. أما LIKE 'خالد%' فيستخدم الفهرس لأنه بادئة ثابتة. للبحث النصي الحر استخدم فهرس نص كامل (FULLTEXT).
  • عدم تطابق النوع: مقارنة عمود رقمي بقيمة نصية يجبر القاعدة على تحويل ضمني يلغي الفهرس أحياناً.
  • OR على أعمدة مختلفة: قد يعجز المخطّط عن استخدام فهرس واحد، وأحياناً يكون UNION أسرع.
  • انتقائية ضعيفة: فهرس على عمود قيمه قليلة جداً (مثل is_active بقيمتين) قد يتجاهله المخطّط لأن مسح الجدول أرخص من قراءة نصف الفهرس ثم العودة للجدول.

حدود الذاكرة على باقات VPS المختلفة

أداء الفهرس مرتبط مباشرة بالذاكرة المتاحة لأن القاعدة تريد إبقاء الفهارس النشطة في الـbuffer pool. إليك تقديراً عملياً على باقات الخادم السحابي:

الباقةRAMما يناسبها
أساس2 GBقاعدة صغيرة، فهارس أساسية لجداول تحت مليون صف
نمو4 GBتطبيق متوسط، عدة جداول مفهرسة بمليون إلى بضعة ملايين صف
أعمال8 GBقاعدة نشطة، فهارس واسعة تبقى في الذاكرة
مؤسسة16 GBبيانات كبيرة، فهارس مركّبة متعددة مع تخزين مؤقت واسع

إن رأيت استعلاماتك تبطؤ فجأة رغم وجود الفهارس، فغالباً حجم الفهارس تجاوز الذاكرة فصارت القاعدة تقرأ من القرص. الحل إما تحسين الفهارس أو زيادة الذاكرة، وإضافة +2GB RAM بـ600 ريال سنوياً حل مباشر ورخيص مقارنة بأثر البطء على تجربة المستخدم. راجع الأسعار والباقات لكل الإضافات.

أنواع الفهارس ومتى تستخدم كلاً منها

ليس كل فهرس شجرة B-Tree. اختيار النوع الصحيح للحالة يوفّر ذاكرة ويسرّع النوع المناسب من البحث:

النوعيناسبملاحظة
B-Treeالمساواة، المدى، الترتيب (النوع الافتراضي)يخدم =، >، <، BETWEEN، وORDER BY
Hashالمساواة الدقيقة فقط (=)لا يخدم المدى ولا الترتيب، نادر الاستعمال
FULLTEXTالبحث النصي الحر داخل الكلماتالبديل الصحيح عن LIKE بنسبة مئوية على الجانبين
GIN / GiST (PostgreSQL)JSONB، المصفوفات، البحث النصي، البيانات المكانيةيفهرس محتوى معقّداً داخل العمود

مثال عملي: عمود JSONB يحفظ خصائص المنتج، والبحث داخله بفهرس B-Tree عديم الفائدة، بينما فهرس GIN يجعل استعلام «كل المنتجات التي لها الخاصية اللون=أحمر» سريعاً، وللبحث النصي العربي الحر، فهرس FULLTEXT يتفوّق على LIKE بمراحل لأنه يبني قاموس كلمات مسبقاً.

متى تحذف فهرساً

الفهارس تتراكم مع الوقت، وبعضها يُنشأ لاستعلام لم يعد موجوداً. الفهرس غير المستخدم عبء صافٍ: يبطّئ الكتابة ويأكل قرصاً وذاكرة بلا مقابل. راقب الاستخدام دورياً: في PostgreSQL يعطيك pg_stat_user_indexes عمود idx_scan (عدد مرات استخدام الفهرس)، وإذا بقي صفراً فترة طويلة فهو مرشح للحذف. في MySQL يعطيك sys.schema_unused_indexes قائمة جاهزة بالفهارس التي لم تُستدعَ منذ آخر تشغيل. احذف بحذر بعد التأكد أن الاستعلام المرتبط اختفى فعلاً، فحذف فهرس مستخدم يعيد استعلاماً إلى المسح الكامل ويقلب التحسّن رأساً على عقب، فانتبه للفهارس المكرّرة: فهرس على (a) زائد فهرس على (a, b) يجعل الأول زائداً غالباً لأن المركّب يخدم البادئة a وحدها.

خاتمة عملية

ابدأ بقياس لا بتخمين: شغّل EXPLAIN على أبطأ ثلاثة استعلامات لديك، وابحث عن نوع الوصول ALL أو Seq Scan فهو علامة المسح الكامل. أضف فهرساً على أعمدة WHERE وJOIN الأكثر تكراراً، وأعد قياس EXPLAIN لتؤكد أن نوع الوصول تحسّن إلى ref أو range. راجع فهارسك دورياً واحذف ما لا يُستخدم منها (PostgreSQL يعطيك pg_stat_user_indexes، وMySQL يعطيك sys.schema_unused_indexes). القاعدة النهائية: الفهرس قرار هندسي بميزان القراءة مقابل الكتابة، لا زر سحري تضغطه على كل عمود.

مشاركة