Indexes وتحسين الأداء — SQL الأساسيات
الفهارس وتحسين الأداء
بدون فهرس، البحث عن email = 'x' في جدول من مليون صف يعني قراءة كل الصفوف (Full Table Scan). الفهرس مثل فهرس الكتاب: بنية مرتبة (B-Tree عادة) تقودك للصف مباشرة.
إنشاء فهرس
CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER, status TEXT, created_at TEXT);
CREATE INDEX idx_orders_user ON orders(user_id);
CREATE INDEX idx_orders_status_created ON orders(status, created_at); -- مركب
CREATE UNIQUE INDEX idx_orders_unique_demo ON orders(id, user_id);PRIMARY KEY و UNIQUE يُنشئان فهارس تلقائياً.
متى تحتاج فهرساً؟
- أعمدة تظهر كثيراً في WHERE.
- المفاتيح الأجنبية المستخدمة في JOIN (PostgreSQL و SQLite لا يفهرسانها تلقائياً!).
- أعمدة ORDER BY مع LIMIT (آخر 20 طلباً).
الفهرس المركب: الترتيب مهم
فهرس على (status, created_at) يفيد:
- WHERE status = 'paid' ✅
- WHERE status = 'paid' AND created_at > '2026-01-01' ✅
- WHERE status = 'paid' ORDER BY created_at ✅
- WHERE created_at > '2026-01-01' وحده ❌ (مثل البحث في دليل هاتف مرتب بالاسم الأخير عن اسم أول)
ضع العمود الذي تُطابقه بالمساواة أولاً، ثم عمود المدى أو الترتيب.
اقرأ خطة التنفيذ
CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER, status TEXT);
EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id = 5; -- SCAN orders (بلا فهرس)
CREATE INDEX idx_orders_user ON orders(user_id);
EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id = 5; -- SEARCH orders USING INDEXفي PostgreSQL: EXPLAIN ANALYZE يشغّل الاستعلام ويعرض الزمن الفعلي لكل خطوة. ابحث عن "Seq Scan" على جداول كبيرة.
ما يمنع استخدام الفهرس
- دالة على العمود: WHERE lower(email) = '...' (الحل: فهرس على التعبير، أو تخزين البريد بأحرف صغيرة).
- LIKE '%كلمة' (نمط يبدأ بـ %). للبحث النصي: فهارس النص الكامل.
- مقارنة أنواع مختلفة (نص مع رقم).
الفهارس ليست مجانية
كل فهرس يبطئ INSERT و UPDATE و DELETE (يجب تحديثه) ويستهلك مساحة. أنشئها بناءً على الاستعلامات الفعلية البطيئة، لا على كل عمود.
أشهر مشكلة أداء في التطبيقات: N+1
جلب 50 طلباً ثم استعلام منفصل لمستخدم كل طلب = 51 استعلاماً. الحل: JOIN واحد، أو جلب كل المستخدمين المطلوبين بـ WHERE id IN (...) دفعة واحدة.
تمرين
أنشئ جدولاً بـ 100,000 صف (بـ WITH RECURSIVE لتوليد الأرقام)، وقِس زمن SELECT ... WHERE user_id = 777 قبل الفهرس وبعده، واقرأ EXPLAIN QUERY PLAN في الحالتين.
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
status TEXT NOT NULL,
created_at TEXT NOT NULL
);
-- توليد 20,000 طلب تجريبي
WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 20000)
INSERT INTO orders (user_id, status, created_at)
SELECT i % 500, CASE WHEN i % 3 = 0 THEN 'paid' ELSE 'pending' END, date('2026-01-01', '+' || (i % 365) || ' days')
FROM n;
-- بلا فهرس: SCAN
EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id = 42;
CREATE INDEX idx_orders_user ON orders(user_id);
CREATE INDEX idx_orders_status_created ON orders(status, created_at);
-- مع الفهرس: SEARCH ... USING INDEX
EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id = 42;
EXPLAIN QUERY PLAN SELECT id FROM orders WHERE status = 'paid' AND created_at >= '2026-06-01' ORDER BY created_at LIMIT 20;
SELECT COUNT(*) FROM orders WHERE user_id = 42;اختبر فهمك