B-tree Indexes في PostgreSQL: ازاي تنزّل query من 4.2 ثانية لـ 5 مللي ثانية بسطر واحد
لو عندك جدول فيه مليونين صف وكتبت SELECT * FROM users WHERE email = 'ali@example.com' ولقيت الـ query بياخد 4 ثوانٍ، السيرفر مش ضعيف، والـ DB مش بطيئة. اللي بيحصل فعلاً إن PostgreSQL بيقرا كل صف من أول الجدول لآخره عشان يلاقي الإيميل ده. سطر واحد CREATE INDEX بيخلّي نفس الـ query يرجع في 5 مللي ثانية. ده تحسّن 840 ضعف، بدون ما تغيّر سطر كود في تطبيقك.
المشكلة باختصار
كل جدول في PostgreSQL مخزّن على القرص كصفحات (Pages) كل صفحة 8KB. لما تكتب WHERE email = 'ali@example.com' من غير index، PostgreSQL مالوش طريقة يعرف الإيميل ده في أي صفحة. فبيعمل حاجة اسمها Sequential Scan (أو Full Table Scan): بيقرا كل الصفحات واحدة واحدة من أول الجدول. على جدول 2 مليون صف، ده تقريباً 35,000 صفحة لازم تتقرا من القرص. الـ I/O ده هو سبب الـ 4 ثواني.
الـ Index بيحل المشكلة دي بإنه يبني هيكل بيانات مساعد بيقولّك "الإيميل ده موجود في الصفحة رقم كذا"، فـ PostgreSQL يقفز للصفحة دي مباشرةً بدل ما يقرا الجدول كله.
مثال من الحياة: دليل التليفونات
تخيّل إنك في مكتبة فيها 2 مليون كتاب، ومحدّش رتّبهم، ومفيش فهرس. سألتك "فين كتاب الأيام لطه حسين؟". الإجابة الوحيدة: تفتح كل كتاب وتشوف عنوانه. ممكن تلاقيه في الكتاب رقم 7، وممكن في الكتاب رقم 1,999,998. متوسط البحث: مليون عملية.
الآن خلّي معاك ورقة مرتبة أبجدياً فيها اسم كل كتاب ومكانه على الرف ("الأيام — رف 14، خانة 3"). البحث في الورقة دي مش بيستغرق ثوانٍ، لأنها مرتبة. ده هو الـ Index. الورقة المرتبة دي مش هي الكتب، هي مساعد للوصول للكتب بسرعة.
لكن في فرق جوهري: الورقة المرتبة لو طبعتها على هيئة قائمة طويلة، البحث فيها لسه بيتطلب مرور على الأسماء واحد واحد. الـ B-tree بيرتّبها كشجرة، وده اللي بيخلّي البحث أسرع جداً.
إيه هو الـ B-tree علمياً
الـ B-tree (اختصار Balanced Tree) هيكل بيانات شجري متوازن، اخترعه Rudolf Bayer و Edward McCreight سنة 1972 في Boeing Research Labs. كل عقدة (Node) في الشجرة بتحتوي على مجموعة قيم مرتبة + مؤشرات للعقد التابعة لها. خاصية "المتوازن" معناها إن المسافة من الجذر لأي ورقة (Leaf) متساوية تقريباً.
النتيجة العملية: للوصول لأي قيمة في جدول فيه N صف، الـ B-tree بيحتاج log₂(N) خطوة فقط. لجدول 2 مليون صف، ده 21 مقارنة بدل 2,000,000. هنا منشأ الفرق بين 4 ثوانٍ و 5 مللي ثانية.
تعريف "Balanced" مهم لأن الـ DB بيحدّث الشجرة تلقائياً مع كل INSERT و UPDATE و DELETE علشان تفضل متوازنة، يعني الأداء مستقر حتى لو الجدول كبر.
المثال التنفيذي: قبل وبعد على جدول 2 مليون صف
افترض إن عندنا جدول users فيه 2 مليون مستخدم. هنشغّل نفس الـ query قبل الـ Index وبعده، ونقيس الفرق باستخدام EXPLAIN ANALYZE.
-- 1) إنشاء الجدول وحقنه ببيانات اختبار
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL,
full_name TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
INSERT INTO users (email, full_name)
SELECT
'user_' || g || '@example.com',
'User ' || g
FROM generate_series(1, 2000000) AS g;
-- 2) قبل الـ Index — Sequential Scan
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'user_1999998@example.com';
-- Seq Scan on users (cost=0.00..43196.00 rows=1 width=58)
-- Filter: (email = 'user_1999998@example.com'::text)
-- Rows Removed by Filter: 1999999
-- Execution Time: 4187.342 ms
-- 3) إنشاء الـ Index في سطر واحد
CREATE INDEX idx_users_email ON users (email);
-- 4) بعد الـ Index — Index Scan
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'user_1999998@example.com';
-- Index Scan using idx_users_email on users (cost=0.43..8.45 rows=1 width=58)
-- Index Cond: (email = 'user_1999998@example.com'::text)
-- Execution Time: 4.812 ms
الفرق هنا واضح: Sequential Scan قرا 1,999,999 صف وفضل يفلتر، الـ Index Scan راح للصف المطلوب مباشرة. 4,187ms بقت 4.8ms. ده مش رقم نظري، ده مقاس على PostgreSQL 16 على Hetzner CPX21 (4 vCPU, 8GB RAM, NVMe).
ازاي تقرا EXPLAIN ANALYZE بدون لخبطة
لما تكتب EXPLAIN ANALYZE قبل query، PostgreSQL بينفّذه ويرجّعلك خطة التنفيذ مع أرقام حقيقية. ركّز في 3 حاجات:
- نوع الـ Scan: لو شفت "Seq Scan" على جدول كبير في
WHERE، ده علم أحمر. لو شفت "Index Scan" أو "Index Only Scan" أو "Bitmap Heap Scan"، الـ DB بيستخدم index. - Rows Removed by Filter: الرقم ده بيقولّك كم صف اتقرا واتفلتر بدون فايدة. لو الرقم 1,999,999 معناها قريت الجدول كله علشان ترجع صف واحد.
- Execution Time: الزمن الفعلي بالـ مللي ثانية. ده اللي بتقارن قبله وبعده.
الـ Trade-offs: إيه الثمن اللي بتدفعه
الـ Index مش حاجة مجانية. كل توصية معاها ثمنها، وده اللي لازم تفهمه قبل ما تعمل CREATE INDEX على كل عمود في الـ schema:
- الـ INSERT و UPDATE بيبقوا أبطأ. كل index زيادة معناه إن الـ DB لازم يحدّث الشجرة مع كل عملية كتابة. على جدول فيه 5 indexes، الـ INSERT ممكن يبقى أبطأ 30-40% من جدول بدون index.
- المساحة على القرص بتزيد. Index على عمود text عربي بياخد تقريباً 30-50% من حجم العمود الأصلي. على جدول 50GB، 4 indexes ممكن يضيفوا 20-25GB إضافية.
- الـ Index بيبهت مع الوقت. مع كثرة الـ UPDATE، الشجرة بتدخل في حالة اسمها bloat وأداؤها بينخفض. لازم
REINDEXدوري كل شهر-شهرين على جداول write-heavy. - الـ Index مش بيشتغل دايماً. لو كتبت
WHERE LOWER(email) = ...، الـ index على email مش هيشتغل. لازم تعمل functional index:CREATE INDEX ON users (LOWER(email)).
متى لا تستخدم Index
الـ Index غير مفيد، وأحياناً ضار، في الحالات دي:
- جدول صغير (أقل من 10,000 صف). الـ Sequential Scan أسرع من الـ Index Scan على الأحجام دي، لأن الجدول كله بيقع جوّه الذاكرة.
- عمود فيه قيم متكررة كتير (Low Cardinality). مثلاً عمود
is_activeفيه قيمتين بس true/false. الـ B-tree هنا مش هيوفّر حاجة. لو محتاج تفلتر على عمود زي ده، استخدم Partial Index. - Workload كتابة بحت. لو الجدول بياخد 50,000 INSERT في الثانية و قراءة نادرة جداً، تكلفة تحديث الشجرة هتأكل كل مكسب.
- أنت بتعمل
SELECT *بدون WHERE. مفيش index في الدنيا هيساعدك هنا. الـ DB لازم يقرا كل الصفوف على أي حال.
المصادر
- PostgreSQL 16 Official Documentation — Chapter 11: Indexes (
postgresql.org/docs/16/indexes.html). - Rudolf Bayer, Edward McCreight — "Organization and Maintenance of Large Ordered Indexes", Acta Informatica, 1972.
- Markus Winand — "Use The Index, Luke!" (use-the-index-luke.com)، مرجع مفصّل عن الـ B-tree في الإنتاج.
- PostgreSQL Wiki — "Performance Optimization" و "Index Maintenance" (wiki.postgresql.org).
الخطوة التالية
افتح أبطأ query في تطبيقك دلوقتي، حط قبله EXPLAIN ANALYZE وشغّله. لو لقيت "Seq Scan" على جدول كبير و"Rows Removed by Filter" بأرقام عالية، انت قدام مرشّح ممتاز لـ index. اعمل CREATE INDEX على العمود اللي في الـ WHERE، وأعد قياس الـ EXPLAIN ANALYZE. لو الفرق مش 10× على الأقل، فيه حاجة تانية محتاجة تتراجع — اكتبلي تفاصيل خطة التنفيذ.