هذا المقال موجّه للمستوى المتوسط: يفترض إنك بتكتب SQL وعملت SELECT … WHERE قبل كده، بس لسه مش فاهم بالظبط ليه إضافة سطر واحد ممكن تقلّب الأداء رأسًا على عقب.
الـ Database Index: ازاي تحوّل query من ثانية لـ نص مللي ثانية
لو عندك جدول فيه مليون مستخدم، وSELECT * FROM users WHERE email = '…' بياخد أكتر من ثانية، المشكلة مش في حجم السيرفر ولا في عدد الـ CPU cores. المشكلة إن قاعدة البيانات بتقرأ المليون صف واحد واحد علشان تلاقي صف واحد. الحل سطر واحد، بس لازم تفهم بيعمل إيه قبل ما تكتبه.
المشكلة باختصار
بدون index، أي بحث بشرط WHERE بيتحوّل لـ Sequential Scan: القاعدة بتمر على كل صف في الجدول وتقارن. ده معناه إن وقت البحث بيكبر خطيًا مع حجم الجدول. ألف صف سريع، مليون صف بطيء، مية مليون صف كارثة. ومع كل مستخدم جديد بيدخل النظام، الـ query بيبقى أبطأ من غير ما تغيّر سطر واحد في كودك.
الفكرة الأساسية بمثال بسيط
تخيّل كتاب 1000 صفحة وعايز تلاقي كلمة "Transaction". عندك طريقتين. الأولى: تفتح من أول صفحة وتقرا كلمة كلمة لحد ما تلاقيها — ده الـ Sequential Scan. الثانية: تروح للفهرس في آخر الكتاب، الكلمات مترتبة أبجديًا، تلاقي "Transaction" في ثانية وجنبها رقم الصفحة 418، تفتح على طول. ده بالظبط اللي بيعمله الـ index.
الفهرس نفسه مش الكتاب، هو نسخة صغيرة مرتبة من كلمة واحدة (العمود) + إشارة لمكان الصف الأصلي. علشان كده الـ index بياخد مساحة زيادة على الديسك، لكنه بيوفّر إنك متقراش الكتاب كله.
التعريف العلمي: ليه B-Tree تحديدًا
أغلب قواعد البيانات (PostgreSQL، MySQL/InnoDB) بتخزّن الـ index في هيكل اسمه B-Tree، اتعرّف أول مرة في ورقة Rudolf Bayer و Edward McCreight سنة 1972. الـ B-Tree شجرة متوازنة (balanced): كل المسارات من الجذر للورقة ليها نفس الطول تقريبًا. ده بيخلّي البحث بتكلفة O(log n) بدل O(n).
الفرق مش تجميلي. في O(n) مليون صف = مليون خطوة. في O(log n) بقاعدة فروع كبيرة، مليون صف = حوالي 3 إلى 4 قفزات للوصول للصف. ده مصدر التحسّن: مش إن القاعدة بقت أسرع، لكن عدد الخطوات نزل من مليون لأربعة.
مثال تنفيذي على PostgreSQL
جرّب ده بنفسك على جدول فيه مليون صف. استخدم EXPLAIN ANALYZE علشان تشوف الفرق بأرقام حقيقية مش تخمين:
-- جدول مستخدمين
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL,
city TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
-- عبّيه بمليون صف للاختبار
INSERT INTO users (email, city)
SELECT 'user' || g || '@example.com',
(ARRAY['Cairo','Giza','Alex'])[1 + (g % 3)]
FROM generate_series(1, 1000000) AS g;
-- (1) بحث قبل الـ index
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'user777777@example.com';
-- Seq Scan on users ... actual time=312.4 ms
-- (2) ضيف الـ index (السطر الواحد بتاعنا)
CREATE INDEX idx_users_email ON users (email);
-- (3) نفس البحث بعد الـ index
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'user777777@example.com';
-- Index Scan using idx_users_email ... actual time=0.5 msعلى إعداد PostgreSQL 16 عادي، النتيجة المقاسة نزلت من حوالي 312 مللي ثانية لـ 0.5 مللي ثانية — أسرع بحوالي 600 مرة. الرقم بيتغيّر حسب جهازك وإعداداتك، لكن رتبة الفرق (مئات الأضعاف) ثابتة. لاحظ كلمة Seq Scan في الخطة الأولى وIndex Scan في التانية — دي أول حاجة تدوّر عليها في EXPLAIN ANALYZE.
سيناريو واقعي
افترض إن عندك متجر بـ 50 ألف طلب/يوم، وصفحة "طلباتي" بتعمل WHERE user_id = ? على جدول طلبات فيه 8 ملايين صف. بدون index على user_id، كل فتح للصفحة بيعمل Seq Scan على 8 ملايين صف. في ساعة الذروة، عشرات الـ Seq Scans بالتوازي بياكلوا الـ CPU ويوقّعوا زمن الاستجابة لكل المستخدمين، مش بس صاحب الصفحة. index واحد على user_id بيحوّل ده لـ Index Scan على آلاف الصفوف فقط.
الـ Trade-offs — مفيش حاجة ببلاش
- مساحة ديسك: الـ index بياخد مساحة إضافية (ممكن 10–30% من حجم الجدول لكل index). بتكسب سرعة قراءة، بتخسر تخزين.
- كتابة أبطأ: كل
INSERTوUPDATEوDELETEلازم يحدّث الـ index كمان. الافتراض هنا إن جدولك بيتقري أكتر مما بيتكتب — وده صحيح لأغلب التطبيقات، بس مش كلها. - صيانة: الـ B-Tree ممكن يتفكّك (fragmentation) مع التعديلات الكتيرة ويحتاج
REINDEXأحيانًا. - أعمدة منخفضة التنوّع: index على عمود قيمه قليلة (زي
is_activeبقيمتين بس) غالبًا مش بيفيد، لأن نص الجدول هيرجع بأي حال.
متى لا تستخدم index
متحطّش index لو الجدول صغير (آلاف الصفوف القليلة) — الـ Seq Scan هيبقى أسرع أصلًا. متحطّش index على عمود نادرًا ما بتفلتر بيه، لأنك بتدفع تكلفة الكتابة من غير مكسب قراءة. ومتحطّش index على جدول كتابة-مكثّفة (logs/events) إلا للأعمدة اللي بتبحث بيها فعلًا، عشان متبطّئش الـ ingestion. القاعدة: index الأعمدة اللي بتظهر في WHERE و JOIN و ORDER BY على الجداول الكبيرة، مش كل عمود.
الخطوة التالية
افتح أبطأ query عندك دلوقتي وشغّل قبله EXPLAIN ANALYZE. لو شفت Seq Scan على جدول كبير مع شرط WHERE على عمود معيّن، ضيف index على العمود ده وأعد القياس. لو الزمن نزل لرتبة المللي ثانية، كنت بتدفع ضريبة بحث خطّي من غير داعي.
المصادر
- R. Bayer, E. McCreight, "Organization and Maintenance of Large Ordered Indexes", Acta Informatica, 1972 — الورقة الأصلية للـ B-Tree.
- PostgreSQL Documentation — Chapter 11 "Indexes" و Chapter 14 "Using EXPLAIN".
- Markus Winand, "Use The Index, Luke" (use-the-index-luke.com) — مرجع عملي لفهم B-Tree والأداء.
- MySQL Reference Manual — "How MySQL Uses Indexes" و InnoDB B-Tree indexes.
- CMU 15-445 Database Systems — محاضرات Tree Indexes (B+Tree).