ما الفهرس؟ وقياس أثره
كيف يجد النظام 10 صفوف بين 200 ألف في أقل من ملّي ثانية؟ ⏱ نحو 25 دقيقة.
- تشرح فكرة الفهرس وكيف يسرّع البحث.
- تنشئ فهرسًا وتحذفه.
- تقيس زمن الاستعلام قبل الفهرس وبعده وتحسب نسبة التحسن.
- توازن بين فائدة الفهرس وثمنه.
١الفهرس: مثل فهرس الكتاب
تبحث عن «المفتاح الأجنبي» في كتاب من 500 صفحة. إما أن تقلّب الصفحات كلها، أو تفتح الفهرس في آخر الكتاب: الكلمات مرتبة أبجديًّا، وبجانب كل كلمة رقم صفحتها.
الفهرس في قاعدة البيانات Index الفكرة نفسها: نسخة مرتبة من قيم عمود (أو أكثر)، وبجانب كل قيمة موضع صفوفها. فإذا سألت عن قيمة، قفز النظام إليها بدل قراءة الجدول كله.
٢جدول كبير للتجربة
الفرق لا يظهر في جدول من 12 صفًّا. لذلك سنستخدم قاعدة «المبيعات الكبيرة»: جدول sales فيه 200 ألف عملية بيع لـ 20 ألف عميل في 7 مدن:
SELECT COUNT(*) AS sales_count FROM sales; SELECT * FROM sales LIMIT 5;
| sales_count |
|---|
| 200000 |
| id | customer_id | city | amount | sale_date |
|---|---|---|---|---|
| 1 | 7920 | مكة المكرمة | 937.61 | 2024-01-14 |
| 2 | 15839 | أبها | 866.22 | 2024-01-27 |
| 3 | 3758 | المدينة المنورة | 794.83 | 2024-02-09 |
| 4 | 11677 | تبوك | 723.44 | 2024-02-22 |
| 5 | 19596 | جدة | 652.05 | 2024-03-06 |
٣قِس بنفسك: قبل الفهرس وبعده
نريد مشتريات العميل رقم 777. هذا الاستعلام بلا فهرس. اضغط ✏️ جرّب وعدّل ثم ▶ تشغيل عدة مرات، وسجّل الزمن بجانب النتيجة:
SELECT COUNT(*) AS purchases, ROUND(SUM(amount), 2) AS totalFROM salesWHERE customer_id = 777;
| purchases | total |
|---|---|
| 10 | 4705.4 |
والآن ننشئ فهرسًا على العمود customer_id، ثم ننفّذ الاستعلام نفسه. قارن زمن الاستعلام الأخير بالزمن السابق:
CREATE INDEX idx_sales_customer ON sales (customer_id); SELECT COUNT(*) AS purchases, ROUND(SUM(amount), 2) AS totalFROM salesWHERE customer_id = 777;
| purchases | total |
|---|---|
| 10 | 4705.4 |
بلا فهرس يقرأ النظام 200 ألف صف ليجد 10 صفوف. وبالفهرس يقفز إلى الصفوف العشرة مباشرة. في متصفح عادي ينخفض الزمن من عشرات الملّي ثواني إلى أقل من ملّي ثانية، أي بنسبة تتجاوز 90%. وهدف المقرر: تقليل زمن التنفيذ بأكثر من 30%.
| الصيغة | |
|---|---|
| إنشاء فهرس | CREATE INDEX اسم_الفهرس ON الجدول (العمود); |
| حذف فهرس | DROP INDEX اسم_الفهرس; (وفي MySQL: DROP INDEX اسم ON الجدول;) |
| تسمية مقترحة | idx_الجدول_العمود، مثل idx_sales_customer |
٤اسأل النظام: هل استخدم الفهرس؟
بدل التخمين، نطلب من النظام أن يشرح خطته قبل التنفيذ بالأمر EXPLAIN QUERY PLAN (وفي MySQL: EXPLAIN):
EXPLAIN QUERY PLANSELECT * FROM sales WHERE customer_id = 777;
SCAN sales
CREATE INDEX idx_sales_customer ON sales (customer_id); EXPLAIN QUERY PLANSELECT * FROM sales WHERE customer_id = 777;
SEARCH sales USING INDEX idx_sales_customer (customer_id=?)
SCAN تعني المرور على الجدول كله، و SEARCH … USING INDEX تعني البحث بالفهرس. وسنتعمق في قراءة الخطة في الدرس 24.
٥الفهارس الموجودة تلقائيًّا
المفتاح الأساسي مفهرس دائمًا، وكذلك كل عمود UNIQUE؛ فالنظام يحتاج الفهرس ليتحقق من عدم التكرار بسرعة. هذه فهارس «متجر الواحة»:
SELECT name AS index_name, tbl_name AS table_nameFROM sqlite_masterWHERE type = 'index';
| index_name | table_name |
|---|---|
| sqlite_autoindex_customers_1 | customers |
| sqlite_autoindex_order_items_1 | order_items |
فهرس customers.phone أنشأه القيد UNIQUE، وفهرس order_items أنشأه المفتاح الأساسي المركّب. أما المفتاح INTEGER PRIMARY KEY فلا يظهر لأنه هو ترتيب الجدول نفسه. وفي MySQL تعرض فهارس جدول بالأمر SHOW INDEX FROM customers;.
٦للفهرس ثمن
| الفائدة | الثمن |
|---|---|
| يسرّع البحث بـ WHERE والربط بـ JOIN والترتيب بـ ORDER BY. | يشغل مساحة إضافية على القرص. |
| يسرّع التحقق من UNIQUE والمفاتيح الأجنبية. | يبطئ INSERT و UPDATE و DELETE؛ لأن كل تغيير في الجدول يتطلب تحديث الفهرس أيضًا. |
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;
جرّبه، ثم احذف أوامر CREATE INDEX الثلاثة وشغّله مرة أخرى، وقارن زمن INSERT في الحالتين.
لذلك لا نفهرس كل عمود. نفهرس الأعمدة التي نبحث بها كثيرًا في جداول كبيرة، وهذا موضوع الدرس القادم.
توقّع وتحقّق
الفهرس في قاعدة البيانات يشبه:
الأمر الذي ينشئ فهرسًا:
CREATE INDEX اسم ON جدول (عمود).أي عملية يبطئها الفهرس؟
SEARCH … USING INDEX في الخطة تعني:
الخلاصة
- الفهرس نسخة مرتبة من قيم عمود تدل على مكان الصفوف.
CREATE INDEXاسمONجدول (عمود); وDROP INDEXاسم;- قِس الزمن عدة مرات قبل الفهرس وبعده، وتحقق بـ
EXPLAIN QUERY PLAN. - المفتاح الأساسي و
UNIQUEمفهرسان تلقائيًّا. - الفهرس يسرّع القراءة ويبطئ الكتابة ويشغل مساحة.
تحدٍّ برمجي: سرّع تقرير المدينة
متوسطمدير المبيعات يطلب كل يوم عدد مبيعات جدة ومجموعها، والاستعلام بطيء. أنشئ فهرسًا مناسبًا ثم اكتب الاستعلام.
- أنشئ فهرسًا اسمه
idx_sales_cityعلى العمودcityفيsales. - ثم اعرض عدد المبيعات في «جدة» باسم
sales_count، ومجموع مبالغها مقرّبًا إلى منزلتين باسمtotal. - قارن الزمن مع الفهرس وبدونه، واكتب نسبة التحسن في تعليق.
👀 اعرض النتيجة المطلوبة
| sales_count | total |
|---|---|
| 28571 | 14507932 |
- الكود يعمل بلا أخطاء
- النتيجة: عدد مبيعات جدة ومجموعها
- يوجد فهرس idx_sales_city على العمود city
- النظام يستخدم الفهرس فعلًا
💡 تلميح 1
CREATE INDEX idx_sales_city ON sales (city);💡 تلميح 2
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
CREATE INDEX idx_sales_city ON sales (city); SELECT COUNT(*) AS sales_count, ROUND(SUM(amount), 2) AS totalFROM salesWHERE city = 'جدة';