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

الفهارس في MySQL: CREATE INDEX و EXPLAIN

الدرس 10 من 12 · MySQL

متجرنا فيه ستة منتجات وخمسة طلبات، وكل استعلام ينتهي في لحظة. لكن المتاجر الناجحة تكبر: مئة ألف منتج، وملايين الطلبات. عندها يتحول استعلام بسيط إلى انتظار طويل، والسبب في أغلب الحالات غياب الفهرس (Index) المناسب. في هذا الدرس ستتعلم كيف تعمل الفهارس في MySQL، وكيف تستخدم EXPLAIN لترى ما تفعله قاعدة البيانات فعلاً.

بدون فهرس، لتنفيذ WHERE stock = 0 تقرأ MySQL الجدول صفاً صفاً من أوله لآخره. هذا اسمه الفحص الكامل (Full Table Scan).

الفهرس مثل فهرس آخر الكتاب: قائمة مرتبة بالكلمات، وبجانب كل كلمة رقم صفحتها. بدل تقليب 500 صفحة، تبحث في القائمة المرتبة وتقفز إلى الصفحة مباشرة. فهرس MySQL قائمة مرتبة بقيم عمود، ومع كل قيمة مكان صفها.

فهارس موجودة من البداية

Section titled “فهارس موجودة من البداية”
SHOW INDEX FROM orders;

الناتج (أهم الأعمدة):

Table Non_unique Key_name Column_name
orders 0 PRIMARY id
orders 1 customer_id customer_id
orders 1 product_id product_id

لم ننشئ أي فهرس، ومع ذلك عندنا ثلاثة:

  • PRIMARY: المفتاح الأساسي مفهرس تلقائياً، و Non_unique = 0 تعني أنه لا يقبل التكرار.
  • customer_id و product_id: محرك InnoDB ينشئ فهرساً لكل مفتاح أجنبي تلقائياً، لأن الربط JOIN يحتاجه دائماً.

ضع EXPLAIN قبل أي استعلام، فتعرض MySQL خطة التنفيذ بدل النتيجة:

EXPLAIN SELECT * FROM products WHERE stock = 0;

الناتج (أهم الأعمدة):

table type possible_keys key rows
products ALL NULL NULL 6
  • type = ALL: فحص كامل للجدول. هذه أسوأ قيمة.
  • key = NULL: لا فهرس مستخدَم.
  • rows = 6: العدد التقديري للصفوف التي ستُقرأ: كل الجدول.
CREATE INDEX idx_products_stock ON products (stock);
EXPLAIN SELECT * FROM products WHERE stock = 0;

الناتج (أهم الأعمدة):

table type possible_keys key rows
products ref idx_products_stock idx_products_stock 1

تغيّر كل شيء دون تعديل الاستعلام نفسه: type أصبحت ref (بحث بالفهرس)، و key تعرض اسم الفهرس المستخدم، و rows نزلت من 6 إلى 1. في جدول من مليون منتج، هذا الفرق بين قراءة مليون صف وقراءة حفنة صفوف.

قيم type الشائعة من الأفضل للأسوأ:

type المعنى
const صف واحد بالمفتاح الأساسي أو فهرس فريد، مثل WHERE id = 3
ref بحث بالمساواة في فهرس عادي
range مدى في فهرس: > و < و BETWEEN و LIKE 'نص%'
ALL فحص كامل للجدول
CREATE UNIQUE INDEX idx_products_name ON products (name);
INSERT INTO products (name, category, price)
VALUES ('مجموعة أقلام', 'أدوات مدرسية', 20);

الناتج: ERROR 1062 (23000): Duplicate entry 'مجموعة أقلام' for key 'products.idx_products_name'

الفهرس الفريد يسرّع البحث بالاسم ويمنع تكراره في الوقت نفسه. مناسب للبريد الإلكتروني واسم المستخدم ورمز المنتج.

صفحة «طلباتي» في المتجر تعرض طلبات عميل معيّن في فترة معيّنة. فهرس واحد على العمودين معاً يخدمها:

CREATE INDEX idx_orders_customer_date ON orders (customer_id, order_date);
EXPLAIN SELECT * FROM orders
WHERE customer_id = 1 AND order_date >= '2026-11-01';

الناتج (أهم الأعمدة):

table type key rows
orders range idx_orders_customer_date 1

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

EXPLAIN SELECT * FROM orders WHERE order_date >= '2026-11-01';

الناتج (أهم الأعمدة):

table type key rows
orders ALL NULL 5

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

كيف تعطّل الفهرس دون أن تدري

Section titled “كيف تعطّل الفهرس دون أن تدري”

أنشأنا فهرساً على التاريخ وحده: CREATE INDEX idx_orders_date ON orders (order_date);. والآن طريقتان لجلب طلبات سنة 2025:

EXPLAIN SELECT * FROM orders WHERE YEAR(order_date) = 2025;

الناتج (أهم الأعمدة):

table type key rows
orders ALL NULL 5
EXPLAIN SELECT * FROM orders
WHERE order_date >= '2025-01-01' AND order_date < '2026-01-01';

الناتج (أهم الأعمدة):

table type key rows
orders range idx_orders_date 1

الاستعلامان يعطيان النتيجة نفسها، لكن الأول يفحص الجدول كله! السبب: الفهرس يخزن التواريخ، لا ناتج الدالة YEAR. فإذا وضعت العمود داخل دالة، اضطرت MySQL لحساب الدالة لكل صف.

والأمر نفسه مع LIKE:

الشرط type السبب
name LIKE 'كتاب%' range بداية النص معروفة، فالترتيب مفيد
name LIKE '%أقلام' ALL البداية مجهولة، فلا يفيد الترتيب
  • الكتابة تبطؤ: كل INSERT و UPDATE و DELETE يحدّث الجدول وكل فهارسه.
  • المساحة: كل فهرس نسخة مرتبة من أعمدته على القرص.
فهرِس لا تفهرِس
أعمدة WHERE المتكررة الجداول الصغيرة
أعمدة JOIN و ORDER BY أعمدة قليلة التنوع مثل «نعم/لا»
الأعمدة التي يجب ألا تتكرر (فريد) أعمدة لا تبحث بها
DROP INDEX idx_orders_date ON orders;

في MySQL يجب ذكر اسم الجدول بعد ON. والحذف لا يمس البيانات: يختفي الطريق المختصر فقط.

  1. اعرض فهارس جدول customers بـ SHOW INDEX.
  2. اعرض خطة تنفيذ SELECT * FROM customers WHERE city = 'القاهرة';، ثم أنشئ فهرساً على city وقارن.
  3. اعرض خطة SELECT * FROM products WHERE id = 3; ولاحظ قيمة type.
  4. تحدٍّ: اكتب استعلامين يجلبان منتجات أُضيفت في شهر أكتوبر 2026: واحداً بـ MONTH(added_on) وآخر بمدى تواريخ، وأنشئ فهرساً على added_on وقارن الخطتين.
  • فهرسة كل الأعمدة: تبطئ الكتابة بلا فائدة. فهرِس ما تبحث به فعلاً.
  • دالة على العمود المفهرس: WHERE YEAR(col) = ... و WHERE col + 1 = ... تعطّلان الفهرس.
  • ترتيب خاطئ في الفهرس المركّب: العمود الذي تبحث به دائماً يجب أن يكون الأول.
  • عدم استخدام EXPLAIN: لا تفترض أن الفهرس مستخدَم. تحقق.

سؤال

ماذا تعني القيمة ALL في عمود type من ناتج EXPLAIN؟

سؤال

أي شرط يستفيد من فهرس على العمود order_date؟

  • الفهرس قائمة مرتبة تسمح بالقفز للصفوف المطلوبة بدل فحص الجدول كله.
  • المفتاح الأساسي والمفاتيح الأجنبية مفهرسة تلقائياً في InnoDB.
  • EXPLAIN تعرض الخطة: راقب type و key و rows.
  • الفهرس المركّب يُستخدم من اليسار، والدوال على العمود تعطّل الفهرس.
  • الفهارس تسرّع القراءة وتبطئ الكتابة، فأضفها عند الحاجة وتحقق منها.