أنواع الفهارس واختيار أعمدتها
ليست كل الفهارس سواء: أي نوع نختار، وعلى أي عمود؟ ⏱ نحو 30 دقيقة.
- تفرّق بين الفهرس المجمّع وغير المجمّع.
- تنشئ فهرسًا فريدًا ومركّبًا وجزئيًّا.
- تطبّق قاعدة البادئة اليسرى في الفهرس المركّب، وتعرف الفهرس المغطّي.
- تختار الأعمدة المناسبة للفهرسة بمعيار الانتقائية.
١المجمّع وغير المجمّع
| الفهرس المجمّع Clustered | الفهرس غير المجمّع Non-clustered | |
|---|---|---|
| ما هو؟ | ترتيب الصفوف نفسها على القرص حسب المفتاح. | هيكل منفصل مرتب، فيه القيمة ومكان الصف. |
| كم في الجدول؟ | واحد فقط (للجدول ترتيب واحد). | عدة فهارس. |
| أين نراه؟ | المفتاح الأساسي في MySQL (InnoDB)، و INTEGER PRIMARY KEY في SQLite. | كل فهرس ننشئه بـ CREATE INDEX. |
| السرعة | الأسرع: يجد الصف نفسه مباشرة. | خطوتان: يجد القيمة في الفهرس، ثم يذهب إلى الصف. |
-- البحث بالمفتاح الأساسي: الفهرس المجمّعEXPLAIN QUERY PLANSELECT * FROM sales WHERE id = 150000;
SEARCH sales USING INTEGER PRIMARY KEY (rowid=?)
٢الفهرس الفريد UNIQUE
فهرس يسرّع البحث ويمنع التكرار معًا. هو ما ينشئه القيد UNIQUE تلقائيًّا، ويمكن إنشاؤه صراحة:
CREATE UNIQUE INDEX idx_products_name ON products (name); -- منتج باسم موجودINSERT INTO products (name, category, price)VALUES ('شاي أخضر', 'مشروبات', 20);
٣الفهرس المركّب: أكثر من عمود
حين نبحث كثيرًا بعمودين معًا (المدينة والتاريخ مثلًا) نجعلهما فهرسًا واحدًا مركّبًا Composite. ترتيبه مثل دليل الهاتف: بالمدينة أولًا، ثم بالتاريخ داخل كل مدينة.
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';
SEARCH sales USING INDEX idx_sales_city_date (city=? AND sale_date>? AND sale_date<?)
SCAN sales
قاعدة البادئة اليسرى: الفهرس (city, sale_date) يفيد في البحث بـ city وحدها، أو بـ city و sale_date معًا، لكنه لا يفيد في البحث بـ sale_date وحدها؛ كما أن دليل الهاتف المرتب بالمدينة ثم الاسم لا يساعدك إن عرفت الاسم دون المدينة.
٤الفهرس المغطّي: لا حاجة للجدول
إن احتوى الفهرس كل الأعمدة التي يطلبها الاستعلام، أخذ النظام الجواب من الفهرس وحده دون أن يفتح الجدول. يسمى هذا فهرسًا مغطّيًا Covering:
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;
SEARCH sales USING COVERING INDEX idx_sales_cust_amount (customer_id=?)
SEARCH sales USING INDEX idx_sales_cust_amount (customer_id=?)
الأول USING COVERING INDEX: المبلغ موجود في الفهرس. والثاني يحتاج بقية الأعمدة فيذهب إلى الجدول. لهذا من عادات الأداء الجيدة: اطلب الأعمدة التي تحتاجها فقط، لا SELECT *.
٥الفهرس الجزئي (الفرعي): جزء من الصفوف
فهرس على جزء من صفوف الجدول فقط، بشرط WHERE. مفيد حين نبحث دائمًا في مجموعة صغيرة، مثل مبيعات مدينة واحدة، أو الطلبات «الجديدة» فقط من بين ملايين الطلبات القديمة. أصغر حجمًا وأسرع تحديثًا:
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;
SEARCH sales USING INDEX idx_tabuk_customer (customer_id=?)
SCAN sales
الاستعلام الثاني لا يستطيع استخدام الفهرس؛ لأن الفهرس لا يعرف عملاء المدن الأخرى. والفهرس الجزئي موجود في SQLite و PostgreSQL و SQL Server، أما MySQL فلا يدعمه.
٦أي الأعمدة نفهرس؟
المقياس المهم هو الانتقائية Selectivity: كم قيمة مختلفة في العمود؟ كلما كثرت القيم المختلفة، قلّت الصفوف لكل قيمة، فكان الفهرس أنفع:
SELECT COUNT(DISTINCT customer_id) AS customers, COUNT(DISTINCT sale_date) AS dates, COUNT(DISTINCT city) AS citiesFROM sales;
| customers | dates | cities |
|---|---|---|
| 20000 | 730 | 7 |
العميل الواحد له نحو 10 صفوف فقط من 200 ألف: انتقائية عالية جدًّا. والمدينة الواحدة لها نحو 28 ألف صف: انتقائية ضعيفة، والفهرس عليها أقل فائدة.
| فهرِس العمود حين… | لا تفهرسه حين… |
|---|---|
| يظهر كثيرًا في WHERE أو JOIN … ON أو ORDER BY. | الجدول صغير (مئات الصفوف): القراءة الكاملة سريعة أصلًا. |
| انتقائيته عالية: قيم مختلفة كثيرة. | قيمه قليلة جدًّا، مثل «نعم/لا» أو الجنس. |
| هو مفتاح أجنبي تربط به الجداول. | يتغير كثيرًا جدًّا، والجدول يُكتب فيه أكثر مما يُقرأ. |
| يُبحث به مع عمود آخر دائمًا: فهرس مركّب بالأكثر انتقائية أو بالأكثر استخدامًا أولًا. | لا يُستخدم في البحث أبدًا. |
توقّع وتحقّق
كم فهرسًا مجمّعًا للجدول؟
فهرس (city, sale_date) لا يفيد في البحث بـ:
الفهرس المغطّي:
أنسب عمود للفهرسة:
الخلاصة
- المجمّع: ترتيب الصفوف نفسها (واحد). غير المجمّع: هيكل منفصل (عدة).
UNIQUE INDEXيسرّع ويمنع التكرار.- المركّب
(a, b)يفيد فيaوحده أوaوb، لا فيbوحده. - المغطّي فيه كل أعمدة الاستعلام، والجزئي على صفوف محددة بشرط.
- فهرِس أعمدة
WHEREوJOINوORDER BYعالية الانتقائية في الجداول الكبيرة.
تحدٍّ برمجي: فهرس مركّب لتقرير شهري
متقدمتقرير يتكرر كثيرًا: مبيعات مدينة معينة في شهر معين. صمّم فهرسًا مركّبًا يجعل النظام يبحث بالمدينة والتاريخ معًا.
- أنشئ فهرسًا اسمه
idx_sales_city_dateعلىcityثمsale_dateبهذا الترتيب. - اعرض عدد مبيعات «الدمام» في مارس 2025 باسم
sales_count، ومجموعها مقرّبًا لمنزلتين باسمtotal. - استخدم
sale_date BETWEEN '2025-03-01' AND '2025-03-31'.
👀 اعرض النتيجة المطلوبة
| sales_count | total |
|---|---|
| 1218 | 622465.32 |
- الكود يعمل بلا أخطاء
- النتيجة: عدد مبيعات الدمام في مارس ومجموعها
- الفهرس على city ثم sale_date بهذا الترتيب
- النظام يبحث بالعمودين معًا
💡 تلميح 1
CREATE INDEX idx_sales_city_date ON sales (city, sale_date);💡 تلميح 2
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
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';