EduFlow
SQL للمحلّلين
⁦0%⁩

الوحدة 4 · SQL في العالم الحقيقي

الـ indexes وخطط التنفيذ

16 دقراءة 5 دتمرين SQL

الدرس في لمحة

اقرأ مخرجات EXPLAIN QUERY PLAN لتميّز المسح الكامل من البحث عبر الـ index، واكتب شروطًا تستطيع قاعدة البيانات خدمتها من index، وقدّر متى يستحق index جديد أن تطلبه.

  1. 1ما هو الـ index
  2. 2قراءة خطة التنفيذ
  3. 3الخطط تنبؤات
  4. 4متى تطلب index جديدًا

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

لدى مارك لوحة معلومات تعرض طلبات يوم مختار، وقد أصبحت بطيئة على قاعدة البيانات الإنتاجية. «الـ query نفسه كالعادة. لماذا صار بطيئًا فجأة؟». على طلبات Cartwheel التي تبلغ ألفين، يعيد كل query في هذه الدورة نتيجته فورًا. أما بيئة الإنتاج ففيها سنوات من الطلبات، وهناك يكون الفرق بين query يقرأ كل صف وآخر يقفز إلى الصفوف الصحيحة فرقًا بين ثوانٍ ودقائق. لا يمكنك أن تشعر بهذا الفرق على قاعدة بيانات صغيرة، لكن يمكنك أن تراه في خطة التنفيذ (query plan).

ما هو الـ index

الـ index بنية منفصلة تحفظ قيم عمود أو أكثر مرتّبة، مع مؤشّر من كل قيمة إلى صفها. إيجاد ‘2025-11-28’ في قائمة مرتّبة يشبه إيجاد كلمة في قاموس: تقفز إلى الصفحة الصحيحة على وجه التقريب بدل القراءة من البداية. بدون index، تضطر قاعدة البيانات إلى قراءة الجدول كله واختبار كل صف، وهذا هو المسح (scan).

على اليسار: مسح كامل يفحص كل صفوف جدول orders واحدًا تلو الآخر. على اليمين: الـ index المسمّى idx_orders_date يحفظ التواريخ مرتّبة؛ والبحث يقفز إلى 2025-11-28، ويقرأ المدخلات المطابقة ويتبع مؤشّراتها إلى الصفوف. SCAN orders اختبار كل صف SEARCH idx_orders_date 2024-01-02 … 2025-11-28 08:… 2025-11-28 23:… 2025-12-30 الصف 2290 الصف 1769 قفزة، ثم قراءة النطاق، ثم تتبّع المؤشّرات
المسح يقرأ كل صف؛ والبحث عبر الـ index يقفز إلى النطاق المطابق

في قاعدة بيانات Cartwheel بعض الـ indexes بالفعل. يمكنك عرضها من جدول المخطط:

SQL
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:

SQL
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 الخاص بلوحة مارك، الذي «بسّطه» أحدهم الشهر الماضي:

SQL
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:

SQL
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). والطلب الذي سترفعه إلى صاحب قاعدة البيانات سيكون:

SQL
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 تسرّع القراءة لكنها تكلّف مساحة تخزين وتبطئ كل إدراج وتحديث، لذا أضفها للشروط المتكرّرة والانتقائية ولمفاتيح الربط.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

على orders يوجد index على order_date. أي شرط لطلبات 28 نوفمبر 2025 يستطيع استخدامه كـ SEARCH؟

السؤال 2 من 4

تعرض خطة التنفيذ SCAN orders USING COVERING INDEX idx_orders_date. ماذا يعني ذلك؟

السؤال 3 من 4

قاعدة بيانات تحليلية تستقبل مليون جلسة ويب جديدة يوميًا، ويُستعلم منها بضع مرات يوميًا حسب source. يقترح أحدهم indexes على الأعمدة التسعة كلها في web_sessions. ما أفضل رد؟

السؤال 4 من 4

في PostgreSQL، ماذا يفعل EXPLAIN ANALYZE ولا يفعله EXPLAIN؟

تدريب

تمرين SQL

شرط سريع وصحيح لعطلة نهاية الأسبوع

تعرض لوحة مارك الطلبات لكل يوم في عطلة نهاية أسبوع Black Friday، من الجمعة 28 نوفمبر إلى الاثنين 1 ديسمبر 2025 شاملًا (كل الحالات).

أعِد day (‘YYYY-MM-DD’) وorders، مرتّبة حسب day. الترتيب مهم. شرط الكود المبدئي متوافق مع الـ index لكنه يُسقط يومًا كاملًا.

أصلحه حتى تكون النتيجة كاملة ويبقى الشرط مستخدمًا لـ idx_orders_date: شغّل الـ query الخاص بك مع EXPLAIN QUERY PLAN قبله وتحقّق من أن الخطة تقول SEARCH لا SCAN.

الجداول
customers
(customer_id, first_name, last_name, email, country, city, signup_date, segment, referred_by)
orders
(order_id, customer_id, order_date, status, channel, coupon_code, shipping_fee)
order_items
(order_item_id, order_id, product_id, quantity, unit_price, discount)
products
(product_id, name, category_id, unit_price, unit_cost, launched_on, is_active)
categories
(category_id, name, parent_id)
returns
(return_id, order_item_id, return_date, reason, refund_amount)
employees
(employee_id, first_name, last_name, title, department, manager_id, hire_date, salary)
support_tickets
(ticket_id, customer_id, order_id, opened_at, closed_at, priority, channel, satisfaction, agent_id)
web_sessions
(session_id, customer_id, started_at, source, device, pages_viewed, added_to_cart, purchased)
-- Orders per day over the Black Friday weekend (Fri 28 Nov to Mon 1 Dec 2025)
SELECT
  date(order_date) AS day,
  COUNT(*) AS orders
FROM orders
WHERE order_date BETWEEN '2025-11-28' AND '2025-12-01'
GROUP BY day
ORDER BY day;جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.