هذا المقال موجّه للمستوى المتوسط: يفترض إنك بتكتب 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