لو SELECT بسيط على جدول 1.2 مليون صف بياخد 8.4 ثانية وأنت متأكد إن السيرفر قوي، المشكلة مش في الـ RAM ولا الـ CPU. المشكلة إن قاعدة البيانات بتقلّب الجدول صف صف زي حد بيبص في 1.2 مليون ورقة بإيده. الـ Index بسطر SQL واحد بينزّل الزمن لـ 5 مللي ثانية، 1690 ضعف أسرع.
هذا المقال للمستوى: مبتدئ
Database Indexes: من 8 ثواني لـ 5 مللي ثانية بدون لمس السيرفر
المشكلة باختصار
عندك جدول users فيه 1.2 مليون مستخدم، وعملت أبسط استعلام ممكن:
SELECT * FROM users WHERE email = 'ahmed@example.com';الاستعلام بياخد 8.4 ثانية على PostgreSQL 16 على لابتوب 16GB رام و SSD سريع. السبب مش حجم البيانات. السبب إن الـ DB بتعمل عملية اسمها Sequential Scan: بتقرا الجدول من أوله لآخره، صف صف، وبتقارن كل email حتى تلاقي اللي محتاجاه. لو السطر اللي بتدوّر عليه في آخر الجدول، لازم تقرا 1.2 مليون صف كاملة قبل ما تلاقيه.
المثال البسيط: قاموس المدرسة
تخيّل إنك بتدوّر على كلمة "احترام" في قاموس فيه 50,000 كلمة، لكن الكلمات مش مرتّبة أبجدياً، متناثرة بشكل عشوائي. هتقعد تقلّب صفحة صفحة لمدة ساعة ونصف. ده بدون Index.
دلوقتي تخيّل نفس القاموس بس الكلمات مرتّبة أبجدياً. بتفتح في النص، تشوف "م" يبقى لازم ترجع لورا. تفتح في الربع الأول، تشوف "ا"، تكمّل في "اح"، بعدين "احت"، وفي خلال 15 ثانية بتلاقي الكلمة. ده الـ Index.
الـ Index في قاعدة البيانات هو نفس الفكرة: هيكل بيانات منفصل عن الجدول، فيه قيم العمود اللي اخترته (مثلاً email) مرتّبة، وكل قيمة معاها مؤشّر (pointer) للسطر الأصلي في الجدول. الـ DB بتدوّر في الـ Index، تلاقي القيمة، تجيب المؤشّر، وتروح للسطر مباشرةً.
التعريف العلمي بدقة
الـ Index الافتراضي في PostgreSQL نوعه B-tree (شجرة متوازنة بـ branching factor عالي). الشجرة دي مكوّنة من عقد، كل عقدة فيها مجموعة قيم مرتّبة + مؤشّرات للعقد الفرعية. الفايدة الرياضية: للبحث عن قيمة، بتنزل من الجذر للأوراق في log₂(N) خطوة بدل N.
الفرق رياضي بحت، مش متعلق بنوع السيرفر:
- Sequential Scan على 1.2M صف = 1,200,000 مقارنة في أسوأ حالة.
- B-tree lookup على 1.2M صف = log₂(1,200,000) ≈ 21 مقارنة فقط.
الفرق النظري ~57,000 ضعف. الفرق العملي أقل (1,690x في الاختبار) بسبب overhead القراءة من القرص و cache hits، لكن المبدأ نفسه.
الكود الكامل قابل للنسخ
على PostgreSQL 16، خطوة بخطوة:
-- 1. قياس قبل الـ Index
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'ahmed@example.com';
-- النتيجة:
-- Seq Scan on users (cost=0.00..32841.00 rows=1 width=84)
-- Execution Time: 8423.521 ms
-- 2. إنشاء الـ Index (سطر واحد)
CREATE INDEX idx_users_email ON users(email);
-- 3. قياس بعد الـ Index
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'ahmed@example.com';
-- النتيجة:
-- Index Scan using idx_users_email on users
-- Execution Time: 4.987 msسطر CREATE INDEX واحد. النتيجة: 1690x أسرع. لازم تلاحظ كلمة Index Scan في خطة الاستعلام بدل Seq Scan؛ دي اللي بتأكدلك إن الـ Index اتستخدم فعلاً.
متى الـ Index بيشتغل ومتى لا
الـ Index مش سحر. بيشتغل في حالات محددة، وبيتجاهله الـ planner في حالات تانية:
WHERE email = 'x@y.com'← بيشتغل (equality match مباشر).WHERE email LIKE 'ahm%'← بيشتغل (prefix match، بداية معروفة).WHERE email LIKE '%example.com'← ما بيشتغلش (suffix match، الـ B-tree مرتّبة من اليمين، مش من الشمال).WHERE LOWER(email) = 'a@b.com'← ما بيشتغلش إلا لو عملت expression index:CREATE INDEX ON users(LOWER(email)).
Trade-offs حقيقية لازم تعرفها
الـ Index مش مجاني. كل سطر CREATE INDEX ليه ثمن:
- مساحة قرص. Index على عمود email في جدول 1.2M صف بياخد ~80MB إضافية. على 10 indexes، الجدول ممكن يبقى ضعف حجمه.
- بطء INSERT و UPDATE و DELETE. كل عملية كتابة لازم تحدّث كل index على الجدول. على benchmarks فعلية، INSERT بقى أبطأ بـ 12-18% بعد إضافة 4 indexes.
- صيانة دورية. الـ Indexes بتتعرّض لـ bloat بعد كتير من الـ updates، فلازم REINDEX كل فترة، وده بيقفل الجدول لو مستخدمتش
CONCURRENTLY.
الافتراض هنا: عندك جدول قراءته أكتر بكتير من كتابته (نسبة 80/20 أو أعلى). لو الجدول write-heavy، الحساب بيتغيّر.
متى لا تستخدم Index
- الجدول صغير (أقل من 10,000 صف). Sequential Scan أسرع لأن الـ planner بيحسب إن قراءة الجدول كله من الذاكرة أرخص من البحث في الـ Index.
- عمود بقيم متكررة بشدة. مثلاً عمود
is_activeفيهtrueعلى 95% من الصفوف. الـ Index هنا تقريباً عديم الفايدة، الـ DB هتفضل تعمل scan. - جداول write-heavy والقراءات نادرة. زي logs و audit_trail. تكلفة تحديث الـ Index في كل INSERT هتفوق فايدة الـ SELECT اللي بتحصل مرة في اليوم.
- أعمدة بتتغير قيمتها كتير. Index على عمود بيتعدّل في كل request هيخلق bloat بسرعة.
الخطوة التالية
افتح PostgreSQL عندك دلوقتي ونفّذ الاستعلام ده:
SELECT schemaname, tablename, indexname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC;لو لقيت Indexes بـ idx_scan = 0، دي Indexes بتشغل مساحة وبتبطّأ الكتابة بدون أي فايدة. احذفها بـ DROP INDEX idx_name; ولاحظ الـ INSERT بيرجع أسرع. ابعتلي مخرجات الاستعلام لو في حاجة مش واضحة.
المصادر
- PostgreSQL 16 Documentation - Indexes: postgresql.org/docs/16/indexes.html
- Bayer & McCreight, "Organization and Maintenance of Large Ordered Indexes" (1972) - الورقة الأصلية للـ B-tree.
- Markus Winand, "Use The Index, Luke!" - use-the-index-luke.com
- PostgreSQL Wiki - Index Maintenance: wiki.postgresql.org/wiki/Index_Maintenance