الفهارس في SQL: تسريع الاستعلامات بـ CREATE INDEX
جدول من ستة طلاب يُفحص في لمح البصر. لكن ماذا عن جدول فيه عشرة ملايين طلب؟ استعلام بسيط بـ WHERE قد يستغرق ثواني طويلة، والموقع كله يبطؤ معه. الحل في أغلب الحالات سطر واحد: الفهرس (Index). في هذا الدرس ستفهم كيف يعمل، وكيف تتأكد أن قاعدة البيانات تستخدمه فعلاً، ومتى يضر أكثر مما ينفع.
المشكلة: فحص كل الصفوف
Section titled “المشكلة: فحص كل الصفوف”عندما تكتب WHERE city = 'القاهرة'، كيف تجد قاعدة البيانات الصفوف المطلوبة؟ بدون مساعدة، ليس أمامها إلا قراءة الجدول صفاً صفاً من أوله لآخره. هذا اسمه الفحص الكامل (Full Scan).
تخيّل أنك تبحث عن كلمة «مؤشر» في كتاب من 500 صفحة. بدون فهرس ستقلب الصفحات كلها. مع فهرس الكتاب في آخره، تجد الكلمة في القائمة المرتبة أبجدياً، وبجانبها رقم الصفحة، فتذهب إليها مباشرة. فهرس قاعدة البيانات يعمل بالطريقة نفسها: قائمة مرتبة بقيم عمود معيّن، ومع كل قيمة مكان صفها في الجدول.
خطة التنفيذ
Section titled “خطة التنفيذ”يمكنك أن تسأل قاعدة البيانات: «كيف ستنفذين هذا الاستعلام؟» بوضع EXPLAIN QUERY PLAN قبله (هذه صيغة SQLite، وفي MySQL و PostgreSQL تكتب EXPLAIN فقط):
EXPLAIN QUERY PLANSELECT * FROM students WHERE city = 'القاهرة';SCAN studentsSCAN تعني فحصاً كاملاً: كل صفوف الجدول ستُقرأ.
إنشاء فهرس
Section titled “إنشاء فهرس”CREATE INDEX idx_students_city ON students (city);idx_students_city: اسم الفهرس. العُرف الشائع:idxثم اسم الجدول ثم اسم العمود.ON students (city): الجدول والعمود المطلوب فهرسته.
والآن نسأل عن الخطة مرة ثانية:
EXPLAIN QUERY PLANSELECT * FROM students WHERE city = 'القاهرة';SEARCH students USING INDEX idx_students_city (city=?)تحولت SCAN إلى SEARCH ... USING INDEX: قاعدة البيانات ستقفز مباشرة إلى الصفوف المطلوبة عبر الفهرس. والمهم: لم تغيّر حرفاً في استعلامك. النتيجة نفسها (سارة ويوسف)، لكن الطريق أقصر. قاعدة البيانات تقرر وحدها متى تستخدم الفهرس.
مع ستة صفوف لن تشعر بفرق. مع ملايين الصفوف يتحول الانتظار من ثوانٍ إلى أجزاء من الألف من الثانية.
الفهرس الفريد
Section titled “الفهرس الفريد”CREATE UNIQUE INDEX ينشئ فهرساً ويمنع التكرار في العمود في الوقت نفسه:
CREATE UNIQUE INDEX idx_courses_title ON courses (title);
INSERT INTO courses (id, title) VALUES (4, 'Python');UNIQUE constraint failed: courses.titleكورس Python موجود من قبل، فرُفضت الإضافة. فائدتان بسطر واحد: بحث سريع بالعنوان، وضمان عدم تكراره.
الفهرس المركّب
Section titled “الفهرس المركّب”إذا كانت استعلاماتك تصفّي بعمودين معاً، يمكن فهرستهما في فهرس واحد:
CREATE INDEX idx_students_city_age ON students (city, age);
EXPLAIN QUERY PLANSELECT * FROM students WHERE city = 'القاهرة' AND age = 15;SEARCH students USING INDEX idx_students_city_age (city=? AND age=?)لكن ترتيب الأعمدة مهم. الفهرس مرتب حسب المدينة أولاً، ثم العمر داخل كل مدينة، مثل دليل هاتف مرتب باسم العائلة ثم الاسم الأول: تجد «آل سعيد» بسهولة، لكنك لا تستطيع إيجاد كل من اسمه «أحمد» دون تقليب الدليل كله. لذلك:
EXPLAIN QUERY PLANSELECT * FROM students WHERE age = 15;SCAN studentsالبحث بالعمر وحده لم يستفد من الفهرس. القاعدة: الفهرس المركّب يفيد الاستعلامات التي تستخدم أعمدته من اليسار: city وحدها، أو city و age معاً.
استعلامات لا يفيدها الفهرس
Section titled “استعلامات لا يفيدها الفهرس”وجود الفهرس لا يكفي؛ طريقة كتابة الشرط قد تمنع استخدامه:
EXPLAIN QUERY PLANSELECT * FROM students WHERE UPPER(city) = 'X';SCAN students| الشرط | يستخدم الفهرس؟ | السبب |
|---|---|---|
city = 'القاهرة' |
نعم | مقارنة مباشرة بالقيمة المفهرسة |
UPPER(city) = ... |
لا | الفهرس يخزن القيم الأصلية لا ناتج الدالة |
city LIKE 'الق%' |
غالباً نعم | بداية النص معروفة |
city LIKE '%رة' |
لا | البداية مجهولة، فلا يفيد الترتيب |
تذكّر تشبيه الكتاب: الفهرس الأبجدي لا يساعدك في إيجاد الكلمات التي تنتهي بحرف معيّن.
الفهارس ليست مجانية
Section titled “الفهارس ليست مجانية”لماذا لا نفهرس كل الأعمدة إذن؟ لأن للفهرس ثمناً:
- الكتابة تبطؤ: مع كل
INSERTأوUPDATEأوDELETEيجب تحديث الجدول وكل فهارسه. عشرة فهارس تعني عشرة تحديثات إضافية. - مساحة التخزين: الفهرس نسخة مرتبة من بيانات العمود، ويأخذ مكاناً على القرص.
| فهرِس | لا تفهرِس |
|---|---|
أعمدة تظهر كثيراً في WHERE |
الجداول الصغيرة (مئات الصفوف) |
أعمدة الربط في JOIN (المفاتيح الأجنبية) |
أعمدة قيمها قليلة التنوع (مثل «ذكر/أنثى») |
أعمدة ORDER BY المتكررة |
أعمدة نادراً ما تبحث بها |
| جداول تُكتب كثيراً وتُقرأ قليلاً |
من أهم ما يُفهرَس أعمدة الربط. في جدول enrollments مثلاً:
CREATE INDEX idx_enrollments_student ON enrollments (student_id);بهذا يصبح جلب اشتراكات طالب معيّن، وأي JOIN على هذا العمود، سريعاً مهما كبر الجدول.
حذف الفهرس
Section titled “حذف الفهرس”DROP INDEX idx_students_city_age;حذف الفهرس لا يحذف أي بيانات من الجدول. يختفي الطريق المختصر فقط، والاستعلامات تعمل كما كانت لكن أبطأ.
جرّب بنفسك
Section titled “جرّب بنفسك”- اعرض خطة تنفيذ الاستعلام
SELECT * FROM students WHERE score > 80;ولاحظ كلمةSCAN. - أنشئ فهرساً على العمود
score، ثم اعرض الخطة مرة ثانية. - جرّب الخطة مع الشرط
WHERE score + 0 > 80. هل ما زال الفهرس مستخدماً؟ ولماذا؟ - تحدٍّ: أنشئ فهرساً مركّباً على
(age, score)، واكتب استعلامين: واحداً يستفيد منه وآخر لا يستفيد، وتحقق بـEXPLAIN QUERY PLAN.
أخطاء شائعة
Section titled “أخطاء شائعة”- فهرسة كل شيء: كل فهرس يبطئ الكتابة ويأخذ مساحة. فهرِس ما تبحث به فعلاً فقط.
- دالة على العمود المفهرس:
WHERE LOWER(email) = ...تتجاهل فهرسemail. خزّن القيمة موحّدة من البداية، أو قارن بالعمود مباشرة. - ترتيب خاطئ في الفهرس المركّب: فهرس على
(city, age)لا يفيد البحث بـageوحدها. ضع العمود الأكثر استخداماً في الشروط أولاً. - عدم التحقق: إنشاء الفهرس لا يضمن استخدامه. افحص خطة التنفيذ دائماً.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ما الثمن الأساسي لإضافة فهرس على جدول؟
كل تعديل على الجدول يستلزم تحديث فهارسه أيضاً، والفهرس نفسه يُخزَّن على القرص.
سؤال
عندك فهرس مركّب على (city, age). أي شرط لا يستفيد منه؟
الفهرس مرتب بالمدينة أولاً، فالبحث بالعمر وحده لا يجد ترتيباً يعتمد عليه.
الخلاصة
Section titled “الخلاصة”- بدون فهرس تفحص قاعدة البيانات كل الصفوف، ومع الفهرس تقفز للمطلوب مباشرة.
CREATE INDEX name ON table (column)ينشئ الفهرس، والاستعلامات لا تتغير.EXPLAINتعرض خطة التنفيذ:SCANفحص كامل، وSEARCH ... USING INDEXبحث بالفهرس.- الفهرس المركّب يُستخدم من اليسار، والدوال و
LIKE '%...'تعطّل الفهرس. - الفهارس تسرّع القراءة وتبطئ الكتابة: أضفها عند الحاجة وتحقق من استخدامها.