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

أنواع الفهارس واختيار أعمدتها

ليست كل الفهارس سواء: أي نوع نختار، وعلى أي عمود؟ ⏱ نحو 30 دقيقة.

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

١المجمّع وغير المجمّع

المجمّع Clusteredid=1سعود · المدينةid=2نورة · الرياضid=3خالد · المدينةid=4ريم · جدةid=5فهد · الرياضالجدول نفسه مرتب بالمفتاح: ترتيب واحد فقطغير المجمّع Non-clusteredجدة→4الرياض→2الرياض→5المدينة→1المدينة→3id=1id=2id=3id=4id=5قائمة منفصلة تشير إلى الصفوف: عدد غير محدود
المجمّع هو ترتيب الصفوف نفسها، وغير المجمّع «فهرس في آخر الكتاب».
الفهرس المجمّع Clusteredالفهرس غير المجمّع Non-clustered
ما هو؟ترتيب الصفوف نفسها على القرص حسب المفتاح.هيكل منفصل مرتب، فيه القيمة ومكان الصف.
كم في الجدول؟واحد فقط (للجدول ترتيب واحد).عدة فهارس.
أين نراه؟المفتاح الأساسي في MySQL (InnoDB)، و INTEGER PRIMARY KEY في SQLite.كل فهرس ننشئه بـ CREATE INDEX.
السرعةالأسرع: يجد الصف نفسه مباشرة.خطوتان: يجد القيمة في الفهرس، ثم يذهب إلى الصف.
query.sql
-- البحث بالمفتاح الأساسي: الفهرس المجمّعEXPLAIN QUERY PLANSELECT * FROM sales WHERE id = 150000;
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SEARCH sales USING INTEGER PRIMARY KEY (rowid=?)

٢الفهرس الفريد UNIQUE

فهرس يسرّع البحث ويمنع التكرار معًا. هو ما ينشئه القيد UNIQUE تلقائيًّا، ويمكن إنشاؤه صراحة:

query.sql
CREATE UNIQUE INDEX idx_products_name ON products (name); -- منتج باسم موجودINSERT INTO products (name, category, price)VALUES ('شاي أخضر', 'مشروبات', 20);
✓ أُنشئ الفهرس
✗ خطأ (السطر 3): مخالفة قيد UNIQUE: القيمة مكررة في products.name

٣الفهرس المركّب: أكثر من عمود

حين نبحث كثيرًا بعمودين معًا (المدينة والتاريخ مثلًا) نجعلهما فهرسًا واحدًا مركّبًا Composite. ترتيبه مثل دليل الهاتف: بالمدينة أولًا، ثم بالتاريخ داخل كل مدينة.

query.sql
CREATE INDEX idx_sales_city_date ON sales (city, sale_date); EXPLAIN QUERY PLANSELECT * FROM salesWHERE city = 'جدة' AND sale_date BETWEEN '2025-01-01' AND '2025-01-31'; EXPLAIN QUERY PLANSELECT * FROM salesWHERE sale_date = '2025-01-15';
✓ أُنشئ الفهرس
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SEARCH sales USING INDEX idx_sales_city_date (city=? AND sale_date>? AND sale_date<?)
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SCAN sales

قاعدة البادئة اليسرى: الفهرس (city, sale_date) يفيد في البحث بـ city وحدها، أو بـ city و sale_date معًا، لكنه لا يفيد في البحث بـ sale_date وحدها؛ كما أن دليل الهاتف المرتب بالمدينة ثم الاسم لا يساعدك إن عرفت الاسم دون المدينة.

٤الفهرس المغطّي: لا حاجة للجدول

إن احتوى الفهرس كل الأعمدة التي يطلبها الاستعلام، أخذ النظام الجواب من الفهرس وحده دون أن يفتح الجدول. يسمى هذا فهرسًا مغطّيًا Covering:

query.sql
CREATE INDEX idx_sales_cust_amount ON sales (customer_id, amount); EXPLAIN QUERY PLANSELECT SUM(amount) FROM sales WHERE customer_id = 777; EXPLAIN QUERY PLANSELECT * FROM sales WHERE customer_id = 777;
✓ أُنشئ الفهرس
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SEARCH sales USING COVERING INDEX idx_sales_cust_amount (customer_id=?)
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SEARCH sales USING INDEX idx_sales_cust_amount (customer_id=?)

الأول USING COVERING INDEX: المبلغ موجود في الفهرس. والثاني يحتاج بقية الأعمدة فيذهب إلى الجدول. لهذا من عادات الأداء الجيدة: اطلب الأعمدة التي تحتاجها فقط، لا SELECT *.

٥الفهرس الجزئي (الفرعي): جزء من الصفوف

فهرس على جزء من صفوف الجدول فقط، بشرط WHERE. مفيد حين نبحث دائمًا في مجموعة صغيرة، مثل مبيعات مدينة واحدة، أو الطلبات «الجديدة» فقط من بين ملايين الطلبات القديمة. أصغر حجمًا وأسرع تحديثًا:

query.sql
CREATE INDEX idx_tabuk_customer ON sales (customer_id)WHERE city = 'تبوك'; EXPLAIN QUERY PLANSELECT * FROM sales WHERE city = 'تبوك' AND customer_id = 5; EXPLAIN QUERY PLANSELECT * FROM sales WHERE customer_id = 5;
✓ أُنشئ الفهرس
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SEARCH sales USING INDEX idx_tabuk_customer (customer_id=?)
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SCAN sales

الاستعلام الثاني لا يستطيع استخدام الفهرس؛ لأن الفهرس لا يعرف عملاء المدن الأخرى. والفهرس الجزئي موجود في SQLite و PostgreSQL و SQL Server، أما MySQL فلا يدعمه.

٦أي الأعمدة نفهرس؟

المقياس المهم هو الانتقائية Selectivity: كم قيمة مختلفة في العمود؟ كلما كثرت القيم المختلفة، قلّت الصفوف لكل قيمة، فكان الفهرس أنفع:

query.sql
SELECT COUNT(DISTINCT customer_id) AS customers,       COUNT(DISTINCT sale_date)   AS dates,       COUNT(DISTINCT city)        AS citiesFROM sales;
النتيجة · صف واحد
customersdatescities
200007307

العميل الواحد له نحو 10 صفوف فقط من 200 ألف: انتقائية عالية جدًّا. والمدينة الواحدة لها نحو 28 ألف صف: انتقائية ضعيفة، والفهرس عليها أقل فائدة.

فهرِس العمود حين…لا تفهرسه حين…
يظهر كثيرًا في WHERE أو JOIN … ON أو ORDER BY.الجدول صغير (مئات الصفوف): القراءة الكاملة سريعة أصلًا.
انتقائيته عالية: قيم مختلفة كثيرة.قيمه قليلة جدًّا، مثل «نعم/لا» أو الجنس.
هو مفتاح أجنبي تربط به الجداول.يتغير كثيرًا جدًّا، والجدول يُكتب فيه أكثر مما يُقرأ.
يُبحث به مع عمود آخر دائمًا: فهرس مركّب بالأكثر انتقائية أو بالأكثر استخدامًا أولًا.لا يُستخدم في البحث أبدًا.

توقّع وتحقّق

كم فهرسًا مجمّعًا للجدول؟

ترتيب واحد للصفوف.

فهرس (city, sale_date) لا يفيد في البحث بـ:

البادئة اليسرى.

الفهرس المغطّي:

لا يفتح الجدول.

أنسب عمود للفهرسة:

انتقائية عالية.

الخلاصة

تحدٍّ برمجي: فهرس مركّب لتقرير شهري

متقدم

تقرير يتكرر كثيرًا: مبيعات مدينة معينة في شهر معين. صمّم فهرسًا مركّبًا يجعل النظام يبحث بالمدينة والتاريخ معًا.

المطلوب:
  • أنشئ فهرسًا اسمه idx_sales_city_date على city ثم sale_date بهذا الترتيب.
  • اعرض عدد مبيعات «الدمام» في مارس 2025 باسم sales_count، ومجموعها مقرّبًا لمنزلتين باسم total.
  • استخدم sale_date BETWEEN '2025-03-01' AND '2025-03-31'.
👀 اعرض النتيجة المطلوبة
النتيجة المطلوبة
sales_counttotal
1218622465.32
✓ التحقق الآلي من حلّك:
  • الكود يعمل بلا أخطاء
  • النتيجة: عدد مبيعات الدمام في مارس ومجموعها
  • الفهرس على city ثم sale_date بهذا الترتيب
  • النظام يبحث بالعمودين معًا

💡 تلميح 1
CREATE INDEX idx_sales_city_date ON sales (city, sale_date);
💡 تلميح 2
الشرطان معًا بـ AND.

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

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

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