إتقان SQL وتحسين الاستعلامات
استراتيجيات الفهرسة
تصميم الفهرس مهارة هندسة بيانات حرجة. أسئلة المقابلات غالباً تطلب منك تصميم فهارس لأنماط استعلام محددة أو استكشاف أخطاء الاستعلامات البطيئة.
أنواع الفهارس وحالات الاستخدام
فهارس B-Tree (الافتراضي)
نوع الفهرس الأكثر شيوعاً، مثالي لاستعلامات المساواة والنطاق.
-- إنشاء فهرس B-Tree
CREATE INDEX idx_orders_customer ON orders(customer_id);
-- جيد لـ:
SELECT * FROM orders WHERE customer_id = 123;
SELECT * FROM orders WHERE customer_id > 100 AND customer_id < 200;
SELECT * FROM orders WHERE customer_id IN (1, 2, 3);
| العملية | أداء B-Tree |
|---|---|
= المساواة |
ممتاز |
<, >, BETWEEN |
ممتاز |
LIKE 'prefix%' |
جيد |
LIKE '%suffix' |
ضعيف (مسح كامل) |
IS NULL |
جيد |
فهارس Hash
محسنة لعمليات البحث عن المساواة الدقيقة فقط.
-- فهرس hash في PostgreSQL
CREATE INDEX idx_orders_status_hash ON orders USING HASH (status);
-- فعال فقط لـ:
SELECT * FROM orders WHERE status = 'pending';
-- غير مفيد لـ:
SELECT * FROM orders WHERE status != 'pending'; -- مسح كامل
SELECT * FROM orders WHERE status IN ('a', 'b'); -- عمليات بحث متعددة
الفهارس المركبة (متعددة الأعمدة)
حرجة للاستعلامات التي تفلتر على أعمدة متعددة.
-- فهرس مركب
CREATE INDEX idx_orders_customer_date
ON orders(customer_id, order_date);
قاعدة البادئة الأيسر: الفهرس يُستخدم عندما تفلتر الاستعلامات على الأعمدة الأيسر.
| الاستعلام | يستخدم الفهرس؟ |
|---|---|
WHERE customer_id = 1 |
✅ نعم |
WHERE customer_id = 1 AND order_date = '2024-01-01' |
✅ نعم |
WHERE order_date = '2024-01-01' |
❌ لا |
WHERE order_date = '2024-01-01' AND customer_id = 1 |
✅ نعم (المحسن يعيد الترتيب) |
سؤال المقابلة: "صمم فهارس لأنماط الاستعلام هذه"
-- استعلام 1: فلترة بالعميل، ترتيب بالتاريخ
SELECT * FROM orders WHERE customer_id = ? ORDER BY order_date DESC;
-- استعلام 2: فلترة بنطاق التاريخ والحالة
SELECT * FROM orders WHERE order_date BETWEEN ? AND ? AND status = ?;
-- استعلام 3: الحصول على آخر طلب لكل عميل
SELECT * FROM orders WHERE customer_id = ? ORDER BY order_date DESC LIMIT 1;
الجواب:
-- للاستعلام 1 و 3: customer_id أولاً، ثم order_date للترتيب
CREATE INDEX idx_customer_date ON orders(customer_id, order_date DESC);
-- للاستعلام 2: اعتبر الانتقائية - إذا كانت الحالة أكثر انتقائية
CREATE INDEX idx_status_date ON orders(status, order_date);
-- أو إذا كان نطاق التاريخ أكثر انتقائية
CREATE INDEX idx_date_status ON orders(order_date, status);
الفهارس الشاملة
تتضمن كل الأعمدة المطلوبة للاستعلام لتجنب البحث في الجدول.
-- الاستعلام
SELECT customer_id, order_date, total
FROM orders
WHERE customer_id = 123;
-- فهرس شامل
CREATE INDEX idx_covering ON orders(customer_id)
INCLUDE (order_date, total);
-- الآن الاستعلام يقرأ من الفهرس فقط، لا وصول للجدول
الفهارس الجزئية (المفلترة)
فهرس فقط الصفوف التي تطابق شرط—أصغر وأسرع.
-- فهرس فقط الطلبات المعلقة (مجموعة فرعية صغيرة)
CREATE INDEX idx_pending_orders ON orders(customer_id)
WHERE status = 'pending';
-- فعال لـ:
SELECT * FROM orders WHERE status = 'pending' AND customer_id = 123;
مبادئ تصميم الفهرس
1. تحليل الاستعلام أولاً
قبل إنشاء الفهارس، حلل أنماط الاستعلام الفعلية:
-- PostgreSQL: إيجاد الاستعلامات البطيئة
SELECT query, calls, mean_time, total_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;
-- حدد الأعمدة في:
-- 1. عبارات WHERE
-- 2. شروط JOIN
-- 3. عبارات ORDER BY
-- 4. عبارات GROUP BY
2. الانتقائية مهمة
الانتقائية العالية (القيم الفريدة) تجعل الفهارس أكثر فعالية.
-- تحقق من الانتقائية
SELECT
COUNT(DISTINCT status) as status_cardinality, -- منخفضة (مثلاً 5)
COUNT(DISTINCT customer_id) as customer_cardinality, -- عالية (مثلاً 100K)
COUNT(*) as total_rows
FROM orders;
-- فهرس customer_id أولاً (انتقائية عالية)
-- الحالة قد لا تحتاج فهرس وحدها
3. ترتيب الأعمدة في الفهارس المركبة
رتب حسب:
- شروط المساواة أولاً (تطابق دقيق)
- شروط النطاق أخيراً (>, <, BETWEEN)
- اعتبر ترتيب الفرز لـ ORDER BY
-- استعلام: WHERE status = 'active' AND created_at > '2024-01-01' ORDER BY created_at
-- أفضل فهرس:
CREATE INDEX idx_optimized ON table(status, created_at);
-- status (مساواة) → created_at (نطاق + ترتيب)
4. الوعي بتكلفة الفهرس
| الاعتبار | التأثير |
|---|---|
| أداء الكتابة | كل فهرس يبطئ INSERT/UPDATE/DELETE |
| التخزين | الفهارس تستهلك مساحة القرص |
| الصيانة | تجزئة الفهرس بمرور الوقت |
| Vacuum/Analyze | فهارس أكثر = صيانة أطول |
قاعدة عامة: استهدف 3-5 فهارس لكل جدول كحد أقصى. كل فهرس إضافي يحتاج تبرير قوي.
سيناريوهات المقابلة الشائعة
السيناريو 1: استعلام لوحة قيادة بطيء
المشكلة: "استعلام لوحة القيادة هذا يأخذ 30 ثانية"
SELECT
DATE(created_at) as date,
COUNT(*) as orders,
SUM(total) as revenue
FROM orders
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
AND status = 'completed'
GROUP BY DATE(created_at);
الحل:
-- 1. إنشاء فهرس مركب
CREATE INDEX idx_orders_status_created ON orders(status, created_at)
WHERE status = 'completed'; -- فهرس جزئي إذا مدعوم
-- 2. اعتبر materialized view للوحة القيادة
CREATE MATERIALIZED VIEW daily_completed_orders AS
SELECT
DATE(created_at) as date,
COUNT(*) as orders,
SUM(total) as revenue
FROM orders
WHERE status = 'completed'
GROUP BY DATE(created_at);
السيناريو 2: أداء الربط
المشكلة: "الربط بين الطلبات والعملاء بطيء"
SELECT c.name, o.total
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE c.country = 'USA';
الحل:
-- تأكد أن المفتاح الخارجي لديه فهرس
CREATE INDEX idx_orders_customer ON orders(customer_id);
-- فهرس لشرط الفلترة
CREATE INDEX idx_customers_country ON customers(country);
-- أو فهرس شامل
CREATE INDEX idx_customers_country_covering ON customers(country)
INCLUDE (id, name);
السيناريو 3: كشف الفهرس غير المستخدم
سؤال المقابلة: "كيف تحدد الفهارس غير المستخدمة؟"
-- PostgreSQL: إيجاد الفهارس غير المستخدمة
SELECT
schemaname || '.' || relname AS table,
indexrelname AS index,
pg_size_pretty(pg_relation_size(i.indexrelid)) AS size,
idx_scan AS scans
FROM pg_stat_user_indexes ui
JOIN pg_index i ON ui.indexrelid = i.indexrelid
WHERE idx_scan = 0
AND NOT indisunique
ORDER BY pg_relation_size(i.indexrelid) DESC;
نظرة المقابلة: عند مناقشة الفهارس، دائماً اعتبر المقايضة بين أداء القراءة (استعلامات أسرع) وأداء الكتابة (إدراج/تحديث أبطأ). أنظمة الإنتاج تحتاج هذا التوازن.
بعد ذلك، سنتدرب على أنماط مقابلات SQL الشائعة والحلول. :::