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

الفهارس في SQL: تسريع الاستعلامات بـ CREATE INDEX

الدرس 11 من 12 · SQL

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

عندما تكتب WHERE city = 'القاهرة'، كيف تجد قاعدة البيانات الصفوف المطلوبة؟ بدون مساعدة، ليس أمامها إلا قراءة الجدول صفاً صفاً من أوله لآخره. هذا اسمه الفحص الكامل (Full Scan).

تخيّل أنك تبحث عن كلمة «مؤشر» في كتاب من 500 صفحة. بدون فهرس ستقلب الصفحات كلها. مع فهرس الكتاب في آخره، تجد الكلمة في القائمة المرتبة أبجدياً، وبجانبها رقم الصفحة، فتذهب إليها مباشرة. فهرس قاعدة البيانات يعمل بالطريقة نفسها: قائمة مرتبة بقيم عمود معيّن، ومع كل قيمة مكان صفها في الجدول.

يمكنك أن تسأل قاعدة البيانات: «كيف ستنفذين هذا الاستعلام؟» بوضع EXPLAIN QUERY PLAN قبله (هذه صيغة SQLite، وفي MySQL و PostgreSQL تكتب EXPLAIN فقط):

EXPLAIN QUERY PLAN
SELECT * FROM students WHERE city = 'القاهرة';
الناتج
SCAN students

SCAN تعني فحصاً كاملاً: كل صفوف الجدول ستُقرأ.

CREATE INDEX idx_students_city ON students (city);
  • idx_students_city: اسم الفهرس. العُرف الشائع: idx ثم اسم الجدول ثم اسم العمود.
  • ON students (city): الجدول والعمود المطلوب فهرسته.

والآن نسأل عن الخطة مرة ثانية:

EXPLAIN QUERY PLAN
SELECT * FROM students WHERE city = 'القاهرة';
الناتج
SEARCH students USING INDEX idx_students_city (city=?)

تحولت SCAN إلى SEARCH ... USING INDEX: قاعدة البيانات ستقفز مباشرة إلى الصفوف المطلوبة عبر الفهرس. والمهم: لم تغيّر حرفاً في استعلامك. النتيجة نفسها (سارة ويوسف)، لكن الطريق أقصر. قاعدة البيانات تقرر وحدها متى تستخدم الفهرس.

مع ستة صفوف لن تشعر بفرق. مع ملايين الصفوف يتحول الانتظار من ثوانٍ إلى أجزاء من الألف من الثانية.

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 موجود من قبل، فرُفضت الإضافة. فائدتان بسطر واحد: بحث سريع بالعنوان، وضمان عدم تكراره.

إذا كانت استعلاماتك تصفّي بعمودين معاً، يمكن فهرستهما في فهرس واحد:

CREATE INDEX idx_students_city_age ON students (city, age);
EXPLAIN QUERY PLAN
SELECT * FROM students WHERE city = 'القاهرة' AND age = 15;
الناتج
SEARCH students USING INDEX idx_students_city_age (city=? AND age=?)

لكن ترتيب الأعمدة مهم. الفهرس مرتب حسب المدينة أولاً، ثم العمر داخل كل مدينة، مثل دليل هاتف مرتب باسم العائلة ثم الاسم الأول: تجد «آل سعيد» بسهولة، لكنك لا تستطيع إيجاد كل من اسمه «أحمد» دون تقليب الدليل كله. لذلك:

EXPLAIN QUERY PLAN
SELECT * FROM students WHERE age = 15;
الناتج
SCAN students

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

استعلامات لا يفيدها الفهرس

Section titled “استعلامات لا يفيدها الفهرس”

وجود الفهرس لا يكفي؛ طريقة كتابة الشرط قد تمنع استخدامه:

EXPLAIN QUERY PLAN
SELECT * FROM students WHERE UPPER(city) = 'X';
الناتج
SCAN students
الشرط يستخدم الفهرس؟ السبب
city = 'القاهرة' نعم مقارنة مباشرة بالقيمة المفهرسة
UPPER(city) = ... لا الفهرس يخزن القيم الأصلية لا ناتج الدالة
city LIKE 'الق%' غالباً نعم بداية النص معروفة
city LIKE '%رة' لا البداية مجهولة، فلا يفيد الترتيب

تذكّر تشبيه الكتاب: الفهرس الأبجدي لا يساعدك في إيجاد الكلمات التي تنتهي بحرف معيّن.

لماذا لا نفهرس كل الأعمدة إذن؟ لأن للفهرس ثمناً:

  • الكتابة تبطؤ: مع كل INSERT أو UPDATE أو DELETE يجب تحديث الجدول وكل فهارسه. عشرة فهارس تعني عشرة تحديثات إضافية.
  • مساحة التخزين: الفهرس نسخة مرتبة من بيانات العمود، ويأخذ مكاناً على القرص.
فهرِس لا تفهرِس
أعمدة تظهر كثيراً في WHERE الجداول الصغيرة (مئات الصفوف)
أعمدة الربط في JOIN (المفاتيح الأجنبية) أعمدة قيمها قليلة التنوع (مثل «ذكر/أنثى»)
أعمدة ORDER BY المتكررة أعمدة نادراً ما تبحث بها
جداول تُكتب كثيراً وتُقرأ قليلاً

من أهم ما يُفهرَس أعمدة الربط. في جدول enrollments مثلاً:

CREATE INDEX idx_enrollments_student ON enrollments (student_id);

بهذا يصبح جلب اشتراكات طالب معيّن، وأي JOIN على هذا العمود، سريعاً مهما كبر الجدول.

DROP INDEX idx_students_city_age;

حذف الفهرس لا يحذف أي بيانات من الجدول. يختفي الطريق المختصر فقط، والاستعلامات تعمل كما كانت لكن أبطأ.

  1. اعرض خطة تنفيذ الاستعلام SELECT * FROM students WHERE score > 80; ولاحظ كلمة SCAN.
  2. أنشئ فهرساً على العمود score، ثم اعرض الخطة مرة ثانية.
  3. جرّب الخطة مع الشرط WHERE score + 0 > 80. هل ما زال الفهرس مستخدماً؟ ولماذا؟
  4. تحدٍّ: أنشئ فهرساً مركّباً على (age, score)، واكتب استعلامين: واحداً يستفيد منه وآخر لا يستفيد، وتحقق بـ EXPLAIN QUERY PLAN.
  • فهرسة كل شيء: كل فهرس يبطئ الكتابة ويأخذ مساحة. فهرِس ما تبحث به فعلاً فقط.
  • دالة على العمود المفهرس: WHERE LOWER(email) = ... تتجاهل فهرس email. خزّن القيمة موحّدة من البداية، أو قارن بالعمود مباشرة.
  • ترتيب خاطئ في الفهرس المركّب: فهرس على (city, age) لا يفيد البحث بـ age وحدها. ضع العمود الأكثر استخداماً في الشروط أولاً.
  • عدم التحقق: إنشاء الفهرس لا يضمن استخدامه. افحص خطة التنفيذ دائماً.

سؤال

ما الثمن الأساسي لإضافة فهرس على جدول؟

سؤال

عندك فهرس مركّب على (city, age). أي شرط لا يستفيد منه؟

  • بدون فهرس تفحص قاعدة البيانات كل الصفوف، ومع الفهرس تقفز للمطلوب مباشرة.
  • CREATE INDEX name ON table (column) ينشئ الفهرس، والاستعلامات لا تتغير.
  • EXPLAIN تعرض خطة التنفيذ: SCAN فحص كامل، و SEARCH ... USING INDEX بحث بالفهرس.
  • الفهرس المركّب يُستخدم من اليسار، والدوال و LIKE '%...' تعطّل الفهرس.
  • الفهارس تسرّع القراءة وتبطئ الكتابة: أضفها عند الحاجة وتحقق من استخدامها.