طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 121 · مبادئ قواعد البيانات
الدرس 22 · الوحدة 10: تصميم الفهارس لتحسين الأداء

ما الفهرس؟ وقياس أثره

كيف يجد النظام 10 صفوف بين 200 ألف في أقل من ملّي ثانية؟ ⏱ نحو 25 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تشرح فكرة الفهرس وكيف يسرّع البحث.
  • تنشئ فهرسًا وتحذفه.
  • تقيس زمن الاستعلام قبل الفهرس وبعده وتحسب نسبة التحسن.
  • توازن بين فائدة الفهرس وثمنه.

١الفهرس: مثل فهرس الكتاب

تبحث عن «المفتاح الأجنبي» في كتاب من 500 صفحة. إما أن تقلّب الصفحات كلها، أو تفتح الفهرس في آخر الكتاب: الكلمات مرتبة أبجديًّا، وبجانب كل كلمة رقم صفحتها.

الفهرس في قاعدة البيانات Index الفكرة نفسها: نسخة مرتبة من قيم عمود (أو أكثر)، وبجانب كل قيمة موضع صفوفها. فإذا سألت عن قيمة، قفز النظام إليها بدل قراءة الجدول كله.

بلا فهرس: قراءة كل الصفوفبالفهرس: قفزة مباشرة1 → ……777 → 17,43…20000 → …
الفهرس قائمة مرتبة بقيم العمود، وبجانب كل قيمة مكان صفوفها في الجدول.

٢جدول كبير للتجربة

الفرق لا يظهر في جدول من 12 صفًّا. لذلك سنستخدم قاعدة «المبيعات الكبيرة»: جدول sales فيه 200 ألف عملية بيع لـ 20 ألف عميل في 7 مدن:

query.sql
SELECT COUNT(*) AS sales_count FROM sales; SELECT * FROM sales LIMIT 5;
النتيجة · صف واحد
sales_count
200000
النتيجة · 5 صفوف
idcustomer_idcityamountsale_date
17920مكة المكرمة937.612024-01-14
215839أبها866.222024-01-27
33758المدينة المنورة794.832024-02-09
411677تبوك723.442024-02-22
519596جدة652.052024-03-06

٣قِس بنفسك: قبل الفهرس وبعده

نريد مشتريات العميل رقم 777. هذا الاستعلام بلا فهرس. اضغط ✏️ جرّب وعدّل ثم ▶ تشغيل عدة مرات، وسجّل الزمن بجانب النتيجة:

query.sql
SELECT COUNT(*) AS purchases, ROUND(SUM(amount), 2) AS totalFROM salesWHERE customer_id = 777;
النتيجة · صف واحد
purchasestotal
104705.4

والآن ننشئ فهرسًا على العمود customer_id، ثم ننفّذ الاستعلام نفسه. قارن زمن الاستعلام الأخير بالزمن السابق:

query.sql
CREATE INDEX idx_sales_customer ON sales (customer_id); SELECT COUNT(*) AS purchases, ROUND(SUM(amount), 2) AS totalFROM salesWHERE customer_id = 777;
✓ أُنشئ الفهرس
النتيجة · صف واحد
purchasestotal
104705.4

بلا فهرس يقرأ النظام 200 ألف صف ليجد 10 صفوف. وبالفهرس يقفز إلى الصفوف العشرة مباشرة. في متصفح عادي ينخفض الزمن من عشرات الملّي ثواني إلى أقل من ملّي ثانية، أي بنسبة تتجاوز 90%. وهدف المقرر: تقليل زمن التنفيذ بأكثر من 30%.

الصيغة
إنشاء فهرسCREATE INDEX اسم_الفهرس ON الجدول (العمود);
حذف فهرسDROP INDEX اسم_الفهرس; (وفي MySQL: DROP INDEX اسم ON الجدول;)
تسمية مقترحةidx_الجدول_العمود، مثل idx_sales_customer

٤اسأل النظام: هل استخدم الفهرس؟

بدل التخمين، نطلب من النظام أن يشرح خطته قبل التنفيذ بالأمر EXPLAIN QUERY PLAN (وفي MySQL: EXPLAIN):

query.sql
EXPLAIN QUERY PLANSELECT * FROM sales WHERE customer_id = 777;
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SCAN sales
query.sql
CREATE INDEX idx_sales_customer ON sales (customer_id); EXPLAIN QUERY PLANSELECT * FROM sales WHERE customer_id = 777;
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SEARCH sales USING INDEX idx_sales_customer (customer_id=?)

SCAN تعني المرور على الجدول كله، و SEARCH … USING INDEX تعني البحث بالفهرس. وسنتعمق في قراءة الخطة في الدرس 24.

٥الفهارس الموجودة تلقائيًّا

المفتاح الأساسي مفهرس دائمًا، وكذلك كل عمود UNIQUE؛ فالنظام يحتاج الفهرس ليتحقق من عدم التكرار بسرعة. هذه فهارس «متجر الواحة»:

query.sql
SELECT name AS index_name, tbl_name AS table_nameFROM sqlite_masterWHERE type = 'index';
النتيجة · صفّان
index_nametable_name
sqlite_autoindex_customers_1customers
sqlite_autoindex_order_items_1order_items

فهرس customers.phone أنشأه القيد UNIQUE، وفهرس order_items أنشأه المفتاح الأساسي المركّب. أما المفتاح INTEGER PRIMARY KEY فلا يظهر لأنه هو ترتيب الجدول نفسه. وفي MySQL تعرض فهارس جدول بالأمر SHOW INDEX FROM customers;.

٦للفهرس ثمن

الفائدةالثمن
يسرّع البحث بـ WHERE والربط بـ JOIN والترتيب بـ ORDER BY.يشغل مساحة إضافية على القرص.
يسرّع التحقق من UNIQUE والمفاتيح الأجنبية.يبطئ INSERT و UPDATE و DELETE؛ لأن كل تغيير في الجدول يتطلب تحديث الفهرس أيضًا.
query.sql
CREATE INDEX idx_sales_city   ON sales (city);CREATE INDEX idx_sales_amount ON sales (amount);CREATE INDEX idx_sales_date   ON sales (sale_date); -- إضافة 20 ألف صف: الفهارس الثلاثة تتحدث مع كل صفINSERT INTO sales (customer_id, city, amount, sale_date)SELECT customer_id, city, amount, sale_date FROM sales WHERE id <= 20000;
✓ أُنشئ الفهرس
✓ أُنشئ الفهرس
✓ أُنشئ الفهرس
✓ أُضيف 20000 صفًّا

جرّبه، ثم احذف أوامر CREATE INDEX الثلاثة وشغّله مرة أخرى، وقارن زمن INSERT في الحالتين.

لذلك لا نفهرس كل عمود. نفهرس الأعمدة التي نبحث بها كثيرًا في جداول كبيرة، وهذا موضوع الدرس القادم.

توقّع وتحقّق

الفهرس في قاعدة البيانات يشبه:

قائمة مرتبة تدلّك على المكان.

الأمر الذي ينشئ فهرسًا:

CREATE INDEX اسم ON جدول (عمود).

أي عملية يبطئها الفهرس؟

كل كتابة تحدّث الفهرس.

SEARCH … USING INDEX في الخطة تعني:

بحث مباشر.

الخلاصة

تحدٍّ برمجي: سرّع تقرير المدينة

متوسط

مدير المبيعات يطلب كل يوم عدد مبيعات جدة ومجموعها، والاستعلام بطيء. أنشئ فهرسًا مناسبًا ثم اكتب الاستعلام.

المطلوب:
  • أنشئ فهرسًا اسمه idx_sales_city على العمود city في sales.
  • ثم اعرض عدد المبيعات في «جدة» باسم sales_count، ومجموع مبالغها مقرّبًا إلى منزلتين باسم total.
  • قارن الزمن مع الفهرس وبدونه، واكتب نسبة التحسن في تعليق.
👀 اعرض النتيجة المطلوبة
النتيجة المطلوبة
sales_counttotal
2857114507932
✓ التحقق الآلي من حلّك:
  • الكود يعمل بلا أخطاء
  • النتيجة: عدد مبيعات جدة ومجموعها
  • يوجد فهرس idx_sales_city على العمود city
  • النظام يستخدم الفهرس فعلًا

💡 تلميح 1
CREATE INDEX idx_sales_city ON sales (city);
💡 تلميح 2
لاحظ أن النتيجة لا تتغير بالفهرس؛ الذي يتغير هو الزمن بجانبها.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

solution.sql
CREATE INDEX idx_sales_city ON sales (city); SELECT COUNT(*) AS sales_count, ROUND(SUM(amount), 2) AS totalFROM salesWHERE city = 'جدة';
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس