الخطوات
-
1
افهم ما يفعله فهرس B-tree
فهرس B-tree هو نسخة مرتبة من عمود أو أكثر مُخزَّنة منفصلة عن الجدول. حين ينفّذ MySQL الشرط
WHERE email = 'alice@example.com'، بدون فهرس يقرأ كل صف (مسح كامل للجدول). مع فهرس علىemail، يتنقل عبر B-tree — عادةً 3-4 عقد لجدول مليون صف — ويقفز مباشرة إلى الصفوف المطابقة.المقايضة: الفهرس يستهلك مساحة قرص، وكل INSERT/UPDATE/DELETE يجب أن يُحدّث بنية الفهرس بالإضافة إلى بيانات الصف. فهارس أكثر = قراءات أسرع، كتابة أبطأ، تخزين أكثر.
sql-- Without an index on email: scans all rows SELECT id, name FROM users WHERE email = 'alice@example.com'; -- EXPLAIN → type: ALL, rows: 500000 -- After adding an index CREATE INDEX idx_users_email ON users (email); -- Same query SELECT id, name FROM users WHERE email = 'alice@example.com'; -- EXPLAIN → type: ref, rows: 1 -
2
حدّد الأعمدة التي تحتاج فهرساً
مرشحات الفهرسة هي الأعمدة التي تظهر في:
- جمل WHERE:
WHERE status = ?،WHERE user_id = ? - شروط JOIN:
JOIN orders ON orders.user_id = users.id— كلا طرفي JOIN يحتاجان فهرساً - ORDER BY:
ORDER BY created_at DESC— الفهرس يمنع filesort - GROUP BY: مشابه لـ ORDER BY
ليس كل عمود يستحق فهرساً.
is_publishedبقيمة منطقية مع 90% من الصفوف قيمتها1لديها انتقائية شبه معدومة — الفهرس لن يُستخدم لأن مسح تلك الصفوف أسرع من اجتياز الفهرس.sql-- High selectivity = good index candidate -- email: every value is unique → perfect CREATE INDEX idx_users_email ON users (email); -- slug: unique per record → perfect CREATE UNIQUE INDEX idx_posts_slug ON posts (slug); -- user_id on orders: one user has many orders → good CREATE INDEX idx_orders_user_id ON orders (user_id); -- Low selectivity = poor index candidate — avoid -- is_published TINYINT(1): only 2 distinct values → bad -- gender ENUM('m','f','other'): only 3 values → likely bad - جمل WHERE:
-
3
ابنِ الفهارس المركّبة بالترتيب الصحيح
فهرس مركّب على
(a, b, c)يمكن استخدامه لاستعلامات علىa، أوa + b، أوa + b + c— لكن ليس لـbوحده أوcوحده. هذه قاعدة البادئة اليسرى.ترتيب الأعمدة الصحيح للفهرس المركّب:
- شروط المساواة أولاً (
status = ?) - شروط النطاق ثانياً (
created_at > ?) - أعمدة ORDER BY أخيراً
هذا الترتيب يطابق كيفية تضييق MySQL لمساحة البحث.
sql-- Query: WHERE status = 'active' AND created_at > '2024-01-01' ORDER BY created_at DESC -- Bad order: range before equality wastes the index CREATE INDEX idx_bad ON orders (created_at, status); -- Good order: equality first, range/sort second CREATE INDEX idx_orders_status_created ON orders (status, created_at); -- MySQL uses (status) to narrow to 'active' rows, -- then (created_at) to scan in order — no filesort needed -- Verify with EXPLAIN EXPLAIN SELECT id FROM orders WHERE status = 'active' AND created_at > '2024-01-01' ORDER BY created_at DESC; - شروط المساواة أولاً (
-
4
استخدم الفهارس المغطية لتجنب جلب الصفوف
الفهرس المغطي يحتوي على كل الأعمدة التي يحتاجها الاستعلام — SELECT وWHERE وORDER BY. يمكن لـ MySQL الإجابة على الاستعلام كلياً من الفهرس دون لمس الجدول الرئيسي. في EXPLAIN يظهر هذا كـ
Extra: Using index.الفهارس المغطية مفيدة بشكل خاص في الاستعلامات عالية الحركة التي تقرأ مجموعة صغيرة ومحددة من الأعمدة. أضف أعمدة SELECT في نهاية الفهرس (بعد أعمدة WHERE وORDER BY).
sql-- Query reads only id, status, created_at from orders SELECT id, status, created_at FROM orders WHERE user_id = 42 ORDER BY created_at DESC; -- A covering index includes all referenced columns CREATE INDEX idx_orders_covering ON orders (user_id, created_at, status, id); -- ^^^^^^^ WHERE ^^^^^^^^^^^ ORDER BY ^^^^^^^^ SELECT -- EXPLAIN will now show: Extra = Using index -- No table lookup required — 100% served from the index -
5
أضف فهارس UNIQUE للمفاتيح الطبيعية
فهرس UNIQUE يخدم غرضين: يُطبّق قيد التفرد على مستوى قاعدة البيانات (لا مجرد التطبيق) وهو فهرس B-tree عادي يُسرّع عمليات البحث. أضف فهرس UNIQUE على أي عمود يجب أن يكون فريداً بالتعريف:
email،slug،username،invoice_number.قيد قاعدة البيانات يصطاد حالات التسابق والأخطاء التي تُفوّتها الفحوصات على مستوى التطبيق — طلبان متزامنان لا يمكنهما كلاهما إدراج نفس البريد الإلكتروني إن كان فهرس UNIQUE موجوداً.
sql-- Single-column unique index ALTER TABLE users ADD UNIQUE INDEX idx_users_email_unique (email); -- Composite unique index (combination must be unique) -- e.g. a user can only follow a given tag once ALTER TABLE user_tag_follows ADD UNIQUE INDEX idx_user_tag_unique (user_id, tag_id); -- In a Laravel migration Schema::table('users', function (Blueprint $table) { $table->unique('email'); }); Schema::table('user_tag_follows', function (Blueprint $table) { $table->unique(['user_id', 'tag_id']); }); -
6
أضف فهارس دائماً لأعمدة المفاتيح الخارجية
InnoDB ينشئ قيد المفتاح الخارجي لكنه لا ينشئ تلقائياً فهرساً على عمود الإسناد في بعض الإعدادات. بدون فهرس على عمود FK، كل DELETE أو UPDATE على الجدول الأب يُطلق مسحاً كاملاً للجدول الابن للتحقق من الصفوف التابعة.
تحقق بعد تشغيل الـ migrations: إن كان عمود FK يفتقر إلى فهرس، أضفه يدوياً.
sql-- Check for un-indexed foreign keys SELECT kcu.TABLE_NAME, kcu.COLUMN_NAME, kcu.CONSTRAINT_NAME FROM information_schema.KEY_COLUMN_USAGE kcu LEFT JOIN information_schema.STATISTICS s ON s.TABLE_SCHEMA = kcu.TABLE_SCHEMA AND s.TABLE_NAME = kcu.TABLE_NAME AND s.COLUMN_NAME = kcu.COLUMN_NAME WHERE kcu.REFERENCED_TABLE_NAME IS NOT NULL AND kcu.TABLE_SCHEMA = 'your_database' AND s.INDEX_NAME IS NULL; -- Returns FK columns that have no index — add one for each result -- In a Laravel migration, this is always safe Schema::table('orders', function (Blueprint $table) { $table->index('user_id'); // index the FK $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); }); -
7
أضف الفهارس عبر Laravel Migrations
في Laravel، تُضاف الفهارس في ملفات migration على كائن
Blueprint. الصياغة واضحة وملف migration يعمل كسجل دائم لكل فهرس في المخطط.php<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::table('orders', function (Blueprint $table) { // Single-column index $table->index('user_id'); // Composite index (equality + range) $table->index(['status', 'created_at'], 'idx_orders_status_created'); // Unique index $table->unique('reference_number'); // Covering index — include additional columns $table->index(['user_id', 'created_at', 'status', 'id'], 'idx_orders_covering'); }); } public function down(): void { Schema::table('orders', function (Blueprint $table) { $table->dropIndex('idx_orders_status_created'); $table->dropIndex('idx_orders_covering'); $table->dropUnique(['reference_number']); }); } }; -
8
اكتشف الفهارس غير المستخدمة واحذفها
الفهارس غير المستخدمة تستهلك مساحة القرص وتُبطئ كل عملية كتابة بصمت. يتتبع MySQL استخدام الفهارس في مخطط
sys— بعد أسبوع من حركة مرور الإنتاج، أي فهرس بصفر قراءات هو مرشح للحذف.sql-- Find indexes never used in queries (MySQL 5.7+ with sys schema) SELECT object_schema, object_name AS table_name, index_name FROM sys.schema_unused_indexes WHERE object_schema = 'your_database' ORDER BY object_name; -- Also check for duplicate / redundant indexes SELECT * FROM sys.schema_redundant_indexes WHERE table_schema = 'your_database'; -- e.g. index on (a) is redundant if (a, b) already exists -- because (a, b) satisfies all queries that (a) alone would -- Drop an index ALTER TABLE orders DROP INDEX idx_old_unused_index; -- Or in Laravel -- $table->dropIndex('idx_old_unused_index');
نصائح ومحاذير
- عند الشك، انظر إلى <code>EXPLAIN</code> قبل وبعد إضافة الفهرس — عمود <code>type</code> يخبرك إن كان الفهرس يُستخدم فعلاً.
- الفهارس على الأعمدة ذات القيم المتميزة القليلة (القيم المنطقية، ENUM الحالة بـ 2-3 قيم) عادةً يتجاهلها المحسّن. استخدم فهرساً مركّباً يقرن عموداً منخفض الانتقائية بعمود عالي الانتقائية.
- في MySQL/InnoDB، المفتاح الأولي جزء من كل فهرس ثانوي (هكذا يحل InnoDB الصف من الفهرس). أبقِ مفتاحك الأولي قصيراً — UUID كبير كمفتاح أولي ينفخ كل فهرس في الجدول.
- شغّل <code>ANALYZE TABLE table_name</code> بعد تحميل البيانات بالجملة لتحديث إحصاءات الفهرس. الإحصاءات القديمة تجعل المحسّن يختار خطة تنفيذ خاطئة.
- كن حذراً عند إضافة فهارس لجداول إنتاج كبيرة أثناء ساعات الذروة — <code>ALTER TABLE ADD INDEX</code> يحصل على قفل metadata يمكنه تكديس الكتابات. استخدم <code>pt-online-schema-change</code> أو MySQL 8+ online DDL لإضافة فهارس بدون توقف.
خاتمة
الفهرس الصحيح في العمود الصحيح غالباً ما يكون أكبر مكسب أداء متاح دون إعادة كتابة منطق التطبيق. ابدأ بالاستعلامات التي ثبت بطؤها، استخدم EXPLAIN لتأكيد نوع الوصول، ثم أضف الحد الأدنى من الفهارس التي تغطي WHERE وORDER BY وإن احتجت SELECT. راجع الفهارس غير المستخدمة ربع سنوياً وأزل الوزن الزائد.