كانت صفحة الطلبات سريعة حين كان فيها عشرة آلاف صف. أما الآن ففيها مليونا صف ويستغرق فتحها كل مرة عدة ثوانٍ. لم يتغير الكود ولا الخادم؛ الذي تغير هو حجم البيانات فقط. وفي معظم هذه الحالات تكون المشكلة غياب الفهرس المناسب.
يمكن تشبيه الفهرس بفهرس آخر الكتاب. فمن دونه عليك تقليب كل الصفحات لتجد موضوعًا واحدًا. ومعه تذهب مباشرة إلى الصفحة الصحيحة. تبيّن هذه المقالة كيف تجد الاستعلام البطيء، وكيف تقرأ بـEXPLAIN ما يحدث، وكيف تبني الفهرس الصحيح. وإن لم تكن قد أنهيت تصميم جداولك بعد فراجع أيضًا تصميم جداول قاعدة البيانات.
اعثر أولًا على الاستعلام البطيء#
التخمين ليس عملًا صائبًا. تأكد أولًا من الاستعلامات البطيئة فعلًا. ولدى MySQL ميزة تسجيل الاستعلامات البطيئة يمكن تشغيلها (وقد لا تملك الصلاحية على الاستضافة المشتركة فاطلبها من الدعم أو استعن بأدوات لوحة التحكم):
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;وفي ووردبريس توجد إضافات تعرض استعلامات كل صفحة. المهم على أي حال أن تعزل استعلامًا محددًا وتعمل عليه.
قراءة EXPLAIN#
تأمل الجدول orders:
CREATE TABLE orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
status VARCHAR(20) NOT NULL,
total DECIMAL(12,2) NOT NULL,
created_at DATETIME NOT NULL
) ENGINE=InnoDB;يطلب الاستعلام التالي آخر طلبات مستخدم:
EXPLAIN SELECT id, total, created_at
FROM orders
WHERE user_id = 42
ORDER BY created_at DESC
LIMIT 20;انظر في الناتج إلى هذه الأعمدة:
| العمود | المعنى | علامة السوء |
|---|---|---|
type | طريقة الوصول إلى الجدول | ALL تعني قراءة الجدول كله |
key | الفهرس الذي استُخدم فعلًا | NULL تعني لا فهرس |
rows | عدد الصفوف التي يقدّر MySQL أنه سيقرؤها | رقم كبير جدًا مقابل نتيجة صغيرة |
Extra | توضيح إضافي | Using filesort تعني فرزًا منفصلًا مكلفًا |
إذا كان type يساوي ALL وكان key فارغًا وشمل Extra العبارة Using filesort فقد رأيت ما أبطأ الاستعلام. وفي إصدارات MySQL الحديثة يمكنك تجربة EXPLAIN ANALYZE أيضًا الذي يعرض زمن التنفيذ الفعلي.
إنشاء الفهرس#
لهذا الاستعلام يناسب فهرس مركّب على user_id وcreated_at:
ALTER TABLE orders
ADD INDEX idx_orders_user_created (user_id, created_at);شغّل EXPLAIN الآن مرة أخرى. يجب أن يكون key هو هذا الفهرس وأن ينقص rows. ولأن الفهرس يغطي مرشّح user_id وترتيب created_at معًا فلا حاجة إلى فرز منفصل أيضًا.
على جدول كبير وعالي الحركة قد يستغرق إنشاء الفهرس وقتًا ويشغل الجدول. خذ نسخة احتياطية قبل التنفيذ على الخادم الرئيسي، والأفضل أن يتم في ساعة قليلة الحركة. والنسخ الاحتياطي التلقائي لقاعدة البيانات أداة مفيدة هنا.
ترتيب الأعمدة في الفهرس المركّب مهم#
الفهرس (user_id, created_at) كدليل هاتف رُتّب أولًا بحسب اسم العائلة ثم بحسب الاسم الأول. يمكنك البحث باسم العائلة، أو بالاثنين معًا؛ لكن بالاسم الأول وحده لا. والقاعدة العملية:
- ضع الأعمدة التي تُقارن بـ
=أولًا. - ثم العمود المستخدم للمدى (
>و<وBETWEEN) أو للترتيب (ORDER BY). - يعمل الفهرس
(a, b)بدل فهرس منفصل(a)أيضًا، فلا يلزم عادةً إنشاء الاثنين.
أشياء تُبطل مفعول الفهرس#
حتى مع فهرس صحيح فإن بعض صيغ كتابة الاستعلام تجعله بلا فائدة:
-- سيئ: دالة على العمود فلا يُستخدم فهرس created_at
SELECT id FROM orders WHERE DATE(created_at) = '2026-10-01';
-- جيد: مدى على العمود نفسه
SELECT id FROM orders
WHERE created_at >= '2026-10-01' AND created_at < '2026-10-02';
-- سيئ: LIKE بعلامة النسبة المئوية في البداية
SELECT id FROM users WHERE email LIKE '%@example.com';
-- أفضل: LIKE يبدأ من أول القيمة
SELECT id FROM users WHERE email LIKE 'ali%';ومن المشكلات الأخرى مقارنة أنواع غير متوافقة؛ كأن تقارن عمودًا نصيًا برقم. فقد يضطر MySQL إلى تحويل قيمة كل صف فيبطل الفهرس. وللبحث في النصوص العربية انظر أيضًا البحث بالعربية في MySQL، فمشكلاته من نوع آخر.
الفهرس الشامل (Covering Index)#
إذا كانت كل الأعمدة التي يطلبها الاستعلام داخل الفهرس نفسه فلا يذهب MySQL إلى الجدول الأصلي أصلًا. وتسمى هذه الحالة فهرسًا شاملًا وتظهر في EXPLAIN بالعبارة Using index في العمود Extra. وللاستعلام أعلاه يمكنك إضافة total إلى الفهرس أيضًا:
ALTER TABLE orders
ADD INDEX idx_orders_user_created_total (user_id, created_at, total);يسرّع هذا القراءة لكنه يكبّر الفهرس. ولا يستحق إلا للاستعلامات كثيرة التكرار جدًا.
كلفة الفهرس#
الفهرس ليس مجانيًا. فكل INSERT وUPDATE وDELETE يجب أن يحدّث الفهارس أيضًا، وكل فهرس يحتاج مساحة قرص. لذلك:
- اعثر على الفهارس المكررة وغير المستخدمة واحذفها.
- الأعمدة قليلة التنوع جدًا (مثل الجنس أو الحالة الثنائية) ليست فهرسًا جيدًا بمفردها.
- بعد كل فهرس جديد تأكد بـ
EXPLAINأنه يُستخدم فعلًا.
ولحذف فهرس لا فائدة منه:
ALTER TABLE orders DROP INDEX idx_orders_user_created;الخلاصة#
- حدّد الاستعلام البطيء أولًا ثم أنشئ الفهرس؛ فالتخمين لا يكفي.
- اقرأ
EXPLAIN: فـtype=ALLوkey=NULLوUsing filesortعلامات المشكلة. - ابدأ الفهرس المركّب بأعمدة المساواة وأنهِه بعمود المدى أو الترتيب.
- الدالة على العمود و
LIKE '%...'تُبطلان الفهرس. - لكل فهرس كلفة كتابة ومساحة؛ فأنشئه للاستعلامات الحقيقية فقط.
- خذ نسخة احتياطية قبل التعديل على جدول كبير.