الوحدة 4 · SQL في العالم الحقيقي
الـ indexes وخطط التنفيذ
16 دقراءة 5 دتمرين SQL
الدرس في لمحة
اقرأ مخرجات EXPLAIN QUERY PLAN لتميّز المسح الكامل من البحث عبر الـ index، واكتب شروطًا تستطيع قاعدة البيانات خدمتها من index، وقدّر متى يستحق index جديد أن تطلبه.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
لدى مارك لوحة معلومات تعرض طلبات يوم مختار، وقد أصبحت بطيئة على قاعدة البيانات الإنتاجية. «الـ query نفسه كالعادة. لماذا صار بطيئًا فجأة؟». على طلبات Cartwheel التي تبلغ ألفين، يعيد كل query في هذه الدورة نتيجته فورًا. أما بيئة الإنتاج ففيها سنوات من الطلبات، وهناك يكون الفرق بين query يقرأ كل صف وآخر يقفز إلى الصفوف الصحيحة فرقًا بين ثوانٍ ودقائق. لا يمكنك أن تشعر بهذا الفرق على قاعدة بيانات صغيرة، لكن يمكنك أن تراه في خطة التنفيذ (query plan).
ما هو الـ index
الـ index بنية منفصلة تحفظ قيم عمود أو أكثر مرتّبة، مع مؤشّر من كل قيمة إلى صفها. إيجاد ‘2025-11-28’ في قائمة مرتّبة يشبه إيجاد كلمة في قاموس: تقفز إلى الصفحة الصحيحة على وجه التقريب بدل القراءة من البداية. بدون index، تضطر قاعدة البيانات إلى قراءة الجدول كله واختبار كل صف، وهذا هو المسح (scan).
في قاعدة بيانات Cartwheel بعض الـ indexes بالفعل. يمكنك عرضها من جدول المخطط:
SELECT name, tbl_name, sql
FROM sqlite_schema
WHERE type = 'index';على orders يوجد index على customer_id وآخر على order_date، وعلى order_items index على order_id وآخر على product_id. أما المدخل sqlite_autoindex_customers_1 فموجود لأن email معرّف كـ UNIQUE؛ ويفرض SQLite التفرّد باستخدام index.
قراءة خطة التنفيذ
ضع EXPLAIN QUERY PLAN قبل أي SELECT، فيصف SQLite كيف سينفّذه بدل أن ينفّذه. الجزء المهم هو العمود detail:
EXPLAIN QUERY PLAN
SELECT order_id, status
FROM orders
WHERE order_date >= '2025-11-28'
AND order_date < '2025-11-29';SEARCH orders USING INDEX idx_orders_date (order_date>? AND order_date<?). تعني SEARCH أن الـ index ضيّق العمل إلى نطاق. والآن الـ query الخاص بلوحة مارك، الذي «بسّطه» أحدهم الشهر الماضي:
EXPLAIN QUERY PLAN
SELECT order_id, status
FROM orders
WHERE date(order_date) = '2025-11-28';SCAN orders. النتيجة نفسها، لكن الآن يُقرأ كل صف وتُحسب date() لكل واحد منها. يخزّن الـ index قيم order_date الخام؛ ولا يعرف شيئًا عن date(order_date)، فلا يستطيع المساعدة. هذا هو «البطء المفاجئ».
هناك عبارتان أخريان في الخطط تستحقان أن تتعرّف عليهما. USING COVERING INDEX تعني أن الـ index وحده احتوى على كل عمود احتاجه الـ query، فلم يُلمس الجدول إطلاقًا، وهذا أسرع أنواع البحث. وUSE TEMP B-TREE FOR GROUP BY أو FOR ORDER BY تعني أن SQLite اضطر إلى ترتيب الصفوف بنفسه، وهذا مقبول مع النتائج الصغيرة ويستحق نظرة مع الكبيرة.
خطط الـ JOIN تعرض سطرًا لكل جدول، بالترتيب الذي يزورها به SQLite:
EXPLAIN QUERY PLAN
SELECT o.order_id, SUM(oi.quantity) AS units
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE o.customer_id = 42
GROUP BY o.order_id;السطران كلاهما SEARCH: اعثر على طلبات العميل 42 عبر idx_orders_customer، ثم على عناصر كل طلب عبر idx_items_order. وجود index على كل مفتاح أجنبي تربط به هو أثمن عادة منفردة في الفهرسة.
الخطط تنبؤات
خطة التنفيذ هي اختيار المُحسِّن (optimizer)، لا ضمان للسرعة، وهي تعتمد على البيانات. في جدول صغير جدًا قد يقرّر المُحسِّن أن المسح أرخص من الـ index، وسيكون محقًا. يستطيع المحرّكان كلاهما الاحتفاظ بإحصاءات عن كل جدول يجمعها الأمر ANALYZE (في PostgreSQL تشغّله عملية autovacuum تلقائيًا، أما في SQLite فلا بد أن يشغّله أحد)، ويستخدمانها لتقدير عدد الصفوف التي تعيدها كل خطوة. حين تصبح هذه الإحصاءات قديمة، تخطئ الخطط بطرق مفاجئة، وهذا سؤال آخر تطرحه على صاحب قاعدة البيانات بشأن query تباطأ «بلا سبب».
بعض الشروط لا تستطيع استخدام index عادي مهما كتبتها. name LIKE '%mat%' يضطر إلى النظر داخل كل قيمة، لأن القائمة المرتّبة لا تساعدك في إيجاد نص في منتصف الكلمات. إذا كان بحث كهذا متكرّرًا، فالحل أداة مختلفة، مثل البحث النصي الكامل (full-text search)، لا index إضافي.
متى تطلب index جديدًا
كثيرًا ما لا يملك المحلّلون المخطط، لكنك ستكون من يلاحظ الـ query البطيء. يصفّي فريق الدعم التذاكر حسب opened_at باستمرار، ولا يوجد index على هذا العمود، فتكون خطة هذه الـ queries مسحًا (SCAN). والطلب الذي سترفعه إلى صاحب قاعدة البيانات سيكون:
CREATE INDEX idx_tickets_opened_at ON support_tickets (opened_at);الـ indexes ليست مجانية. كلٌّ منها يأخذ مساحة تخزين ويجب تحديثه مع كل إدراج وتحديث وحذف، فالجدول الذي عليه عشرة indexes يكتب أبطأ بشكل ملحوظ. اطلب indexes على أعمدة تصفّي عليها queries متكرّرة أو تربط بها أو ترتّب حسبها، وتضيّق النتيجة كثيرًا. الـ index على status، الذي له خمس قيم فقط، نادرًا ما يساعد؛ أما الـ index على تاريخ أو معرّف فيساعد عادةً. وفي الـ index متعدّد الأعمدة، ضع أولًا العمود الذي تختبره بـ = ثم عمود النطاق: (customer_id, order_date) يخدم «طلبات العميل 42 في 2025» ببحث واحد.
الـ queries السريعة لا تنفع إلا إذا استطاع الناس قراءتها. في الدرس التالي: عادات الأسلوب وفروق اللهجات التي تجعل SQL الذي تكتبه قابلًا للمراجعة والنقل.
أهم النقاط
- الـ index نسخة مرتّبة من عمود أو أكثر مع مؤشّرات تعود إلى الصفوف، فتستطيع قاعدة البيانات القفز إلى القيم المطابقة بدل قراءة كل صف.
- في EXPLAIN QUERY PLAN تعني SEARCH … USING INDEX أن الـ index ضيّق العمل؛ وتعني SCAN أن كل صف (أو كل مدخل في الـ index) قد قُرئ.
- وضع عمود عليه index داخل دالة أو عملية حسابية، مثل date(order_date)، يمنع عادةً استخدام الـ index؛ قارن العمود المجرّد بنطاق بدل ذلك.
- الـ indexes تسرّع القراءة لكنها تكلّف مساحة تخزين وتبطئ كل إدراج وتحديث، لذا أضفها للشروط المتكرّرة والانتقائية ولمفاتيح الربط.