خطة التنفيذ: EXPLAIN
اسأل النظام كيف سينفّذ استعلامك قبل أن تخمّن سبب بطئه. ⏱ نحو 30 دقيقة.
- تشرح دور المحسّن في اختيار خطة التنفيذ.
- تقرأ خطة
SQLiteوتميّزSCANوSEARCHوTEMP B-TREEوAUTOMATIC INDEX. - تقابل أعمدة
EXPLAINفيMySQLبعباراتSQLite. - تتبع دورة التحسين: قِس، اقرأ، عدّل، قِس.
١من يقرر كيف يُنفَّذ الاستعلام؟
في SQL تكتب ماذا تريد، لا كيف يُجلب. والذي يقرر «كيف» جزء في نظام إدارة قواعد البيانات اسمه المحسّن Query Optimizer: ينظر إلى الجداول والفهارس المتاحة، ويوازن بين الطرق الممكنة، ويختار أرخصها.
والأمر EXPLAIN QUERY PLAN في SQLite، أو EXPLAIN في MySQL، يطلب من المحسّن أن يعرض خطته دون أن ينفّذ الاستعلام.
٢قاموس الخطة في SQLite
| العبارة في الخطة | معناها | الحكم |
|---|---|---|
SCAN t | يقرأ الجدول كله صفًّا صفًّا. | 🟠 مقبول للجداول الصغيرة، ومكلف للكبيرة |
SCAN t USING COVERING INDEX i | يمر على الفهرس كله بدل الجدول. | 🔵 أخف من قراءة الجدول |
SEARCH t USING INDEX i (col=?) | يقفز إلى الصفوف المطلوبة بالفهرس. | 🟢 ممتاز |
SEARCH t USING INTEGER PRIMARY KEY | يقفز بالمفتاح الأساسي. | 🟢 الأسرع |
USE TEMP B-TREE FOR ORDER BY | يرتب النتيجة في ذاكرة مؤقتة بعد جلبها. | 🟡 عمل إضافي يمكن تجنبه بفهرس |
… AUTOMATIC … INDEX | بنى النظام فهرسًا مؤقتًا لهذا الاستعلام فقط. | 🟡 إشارة: أنشئ الفهرس بنفسك |
CORRELATED … SUBQUERY | استعلام فرعي يُنفَّذ مرة لكل صف. | 🟡 قد يكون بطيئًا |
٣قراءة خطة الربط JOIN
كيف يربط النظام الطلبات بالعملاء؟ اقرأ الخطة من الأعلى إلى الأسفل، فهي مثل حلقتين متداخلتين:
EXPLAIN QUERY PLANSELECT c.name, o.order_dateFROM orders oJOIN customers c ON c.id = o.customer_id;
SCAN oSEARCH c USING INTEGER PRIMARY KEY (rowid=?)
المعنى: «لكل طلب في orders (SCAN)، اقفز إلى عميله في customers بالمفتاح الأساسي (SEARCH)». وهذا ممتاز.
لكن في الاتجاه المعاكس، من العميل إلى طلباته، لا يوجد فهرس على المفتاح الأجنبي orders.customer_id:
EXPLAIN QUERY PLANSELECT c.name, COUNT(o.id) AS ordersFROM customers cLEFT JOIN orders o ON o.customer_id = c.idGROUP BY c.id;
SCAN cBLOOM FILTER ON o (customer_id=?)SEARCH o USING AUTOMATIC COVERING INDEX (customer_id=?) LEFT-JOIN
AUTOMATIC COVERING INDEX تعني أن SQLite اضطر لبناء فهرس مؤقت في كل مرة. والحل الدائم:
CREATE INDEX idx_orders_customer ON orders (customer_id); EXPLAIN QUERY PLANSELECT c.name, COUNT(o.id) AS ordersFROM customers cLEFT JOIN orders o ON o.customer_id = c.idGROUP BY c.id;
SCAN cSEARCH o USING COVERING INDEX idx_orders_customer (customer_id=?) LEFT-JOIN
٤الترتيب المكلف: TEMP B-TREE
«أعلى 5 مبيعات في أبها». بلا فهرس مناسب: يقرأ النظام 200 ألف صف، ثم يرتب مبيعات أبها كلها ليأخذ أول 5:
EXPLAIN QUERY PLANSELECT id, amountFROM salesWHERE city = 'أبها'ORDER BY amount DESCLIMIT 5;
SCAN salesUSE TEMP B-TREE FOR ORDER BY
بفهرس مركّب (city, amount) تكون مبيعات أبها في الفهرس مرتبة بالمبلغ أصلًا، فيأخذ النظام آخر 5 مباشرة بلا ترتيب:
CREATE INDEX idx_sales_city_amount ON sales (city, amount); EXPLAIN QUERY PLANSELECT id, amount FROM salesWHERE city = 'أبها' ORDER BY amount DESC LIMIT 5; SELECT id, amount FROM salesWHERE city = 'أبها' ORDER BY amount DESC LIMIT 5;
SEARCH sales USING COVERING INDEX idx_sales_city_amount (city=?)
| id | amount |
|---|---|
| 10859 | 1008.99 |
| 132477 | 1008.97 |
| 86872 | 1008.92 |
| 41267 | 1008.87 |
| 162885 | 1008.85 |
جرّبه، ثم احذف سطر CREATE INDEX وشغّل مرة أخرى، وقارن زمن الاستعلام الأخير في الحالتين.
٥EXPLAIN في MySQL
في MySQL Workbench تكتب EXPLAIN قبل الاستعلام، فتظهر الخطة جدولًا. هذا شكل تقريبي لخطة استعلام العميل 777 قبل الفهرس وبعده:
EXPLAIN SELECT * FROM sales WHERE customer_id = 777;| type | key | rows | Extra | |
|---|---|---|---|---|
| قبل الفهرس | ALL | NULL | ≈ 200000 | Using where |
| بعد الفهرس | ref | idx_sales_customer | 10 | — |
| قيمة type في MySQL | معناها | يقابلها في SQLite |
|---|---|---|
ALL | قراءة الجدول كله | SCAN t |
index | قراءة الفهرس كله | SCAN t USING COVERING INDEX |
range | نطاق من الفهرس (BETWEEN، > ، <) | SEARCH … (col>? AND col<?) |
ref | بحث بقيمة في فهرس غير فريد | SEARCH … (col=?) |
eq_ref / const | صف واحد بمفتاح أساسي أو فريد | SEARCH … PRIMARY KEY |
وفي العمود Extra: Using filesort يقابل TEMP B-TREE FOR ORDER BY، و Using index يعني فهرسًا مغطّيًا. وفي MySQL 8 يوجد EXPLAIN ANALYZE الذي ينفّذ الاستعلام فعلًا ويعرض الزمن الحقيقي لكل خطوة.
٦طريقة العمل: قِس، اقرأ، عدّل، قِس
- قِس زمن الاستعلام البطيء عدة مرات.
- اقرأ الخطة: ابحث عن SCAN على جدول كبير، و
TEMP B-TREE، و AUTOMATIC INDEX. - عدّل: أضف فهرسًا مناسبًا، أو أعد صياغة الاستعلام (الدرس القادم).
- قِس مرة أخرى، وتأكد من الخطة الجديدة، واحسب نسبة التحسن.
والأمر ANALYZE يجمع إحصاءات عن الجداول والفهارس (عدد الصفوف وتوزيع القيم)، فيختار المحسّن خططًا أدق. شغّله بعد إدخال كميات كبيرة من البيانات.
توقّع وتحقّق
الجزء الذي يختار طريقة تنفيذ الاستعلام:
USE TEMP B-TREE FOR ORDER BY تعني:
type = ALL في MySQL تقابل في SQLite:
AUTOMATIC INDEX في الخطة إشارة إلى:
الخلاصة
- المحسّن يختار كيف يُنفَّذ الاستعلام، و
EXPLAINيعرض خطته. SCANقراءة كاملة، وSEARCHقفز بالفهرس، وTEMP B-TREEترتيب إضافي.- خطة الربط حلقتان:
SCANللجدول الخارجي وSEARCHللداخلي بالمفتاح. - في
MySQL: type = ALLسيئ للجداول الكبيرة، وrefوrangeوconstجيدة. - فهرس (عمود
_الشرط، عمود_الترتيب) يلغي الترتيب الإضافي.
تحدٍّ برمجي: أعلى المبيعات بلا ترتيب إضافي
متقدمتقرير «أعلى 5 مبيعات في الرياض» يُطلب باستمرار، وخطته فيها SCAN و TEMP B-TREE. اجعل خطته بحثًا مباشرًا في الفهرس بلا ترتيب إضافي.
- أنشئ فهرسًا مركّبًا اسمه
idx_sales_city_amountمناسبًا لهذا التقرير. - ثم اعرض
idوamountلأعلى 5 مبيعات في «الرياض» مرتبة تنازليًّا بالمبلغ.
👀 اعرض النتيجة المطلوبة
| id | amount |
|---|---|
| 121618 | 1008.98 |
| 76013 | 1008.93 |
| 197631 | 1008.91 |
| 30408 | 1008.88 |
| 152026 | 1008.86 |
- الكود يعمل بلا أخطاء
- النتيجة: أعلى 5 مبيعات في الرياض بالترتيب
- الخطة: بحث في الفهرس بلا
TEMP B-TREE
💡 تلميح 1
💡 تلميح 2
CREATE INDEX idx_sales_city_amount ON sales (city, amount);حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
CREATE INDEX idx_sales_city_amount ON sales (city, amount); SELECT id, amount FROM salesWHERE city = 'الرياض'ORDER BY amount DESCLIMIT 5;