تحسين الاستعلامات (Query Optimization)

تحسين الاستعلامات (Query Optimization) هو فن وعلم جعل استعلامات SQL تعمل بأسرع وأكفأ طريقة ممكنة. الاستعلام البطيء يمكن أن يؤثر بشكل كبير على أداء التطبيق بالكامل، خاصة مع قواعد البيانات الكبيرة أو عدد المستخدمين الكبير. الفرق بين استعلام محسّن وآخر غير محسّن يمكن أن يكون من ميلي ثانية إلى عدة دقائق. في هذا الدرس الشامل والمفصل من سلسلة تعلم لغة SQL باللغة العربية، سنتعلم كل شيء عن تحسين الاستعلامات: كيفية تحليل الاستعلامات باستخدام EXPLAIN، تحسين استخدام الفهارس، تجنب الأخطاء الشائعة، تقنيات كتابة استعلامات فعالة، وأمثلة عملية تطبيقية مفصلة لتحويل الاستعلامات البطيئة إلى سريعة.

1. فهم كيفية تنفيذ الاستعلامات: EXPLAIN

أداة EXPLAIN هي أقوى أداة لتحليل وفهم كيفية تنفيذ MySQL للاستعلام. تعرض لك خطة التنفيذ (Execution Plan) التي ستستخدمها قاعدة البيانات.

استخدام EXPLAIN الأساسي
تحليل استعلام باستخدام EXPLAIN
-- تحليل استعلام بسيط
EXPLAIN SELECT * FROM employees WHERE department = 'IT';

-- تحليل استعلام مع JOIN
EXPLAIN SELECT e.first_name, d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE e.salary > 5000;
فهم نتائج EXPLAIN

الأعمدة المهمة في نتيجة EXPLAIN:

  • type: نوع الوصول للجدول (ALL, index, range, ref, eq_ref, const)
  • possible_keys: الفهارس التي يمكن استخدامها
  • key: الفهرس المستخدم فعلياً
  • rows: عدد الصفوف المتوقع فحصها
  • Extra: معلومات إضافية مهمة
أنواع الوصول (type) من الأسرع للأبطأ
النوع الوصف الأداء
system جدول يحتوي على صف واحد فقط ممتاز
const قراءة صف واحد باستخدام PRIMARY KEY أو UNIQUE ممتاز
eq_ref قراءة صف واحد لكل صف من الجدول السابق جيد جداً
ref قراءة صفوف متعددة باستخدام فهرس غير فريد جيد
range قراءة نطاق من الصفوف باستخدام فهرس مقبول
index فحص الفهرس بالكامل بطيء
ALL فحص الجدول بالكامل (Table Scan) بطيء جداً
تحذير مهم:

إذا رأيت type: ALL في EXPLAIN، فهذا يعني أن MySQL يفحص الجدول بالكامل. هذا بطيء جداً مع الجداول الكبيرة ويجب تحسينه بإضافة فهرس مناسب.

2. تحسين استخدام الفهارس

الفهارس هي المفتاح الأساسي لتحسين أداء الاستعلامات. لكن يجب استخدامها بحكمة.

مثال: قبل وبعد إضافة فهرس
تأثير الفهرس على الأداء
-- جدول بدون فهرس
CREATE TABLE employees (
    employee_id INT PRIMARY KEY,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    email VARCHAR(100),
    department VARCHAR(50),
    salary DECIMAL(10, 2)
);

-- استعلام بطيء (بدون فهرس)
EXPLAIN SELECT * FROM employees WHERE department = 'IT';
-- النتيجة: type: ALL, rows: 100000 (فحص كامل للجدول)

-- إضافة فهرس
CREATE INDEX idx_department ON employees(department);

-- نفس الاستعلام (مع فهرس)
EXPLAIN SELECT * FROM employees WHERE department = 'IT';
-- النتيجة: type: ref, rows: 5000 (استخدام الفهرس)

-- تحسين كبير في الأداء!
الفهارس المركبة (Composite Indexes)

عند البحث بعدة أعمدة معاً، استخدم فهرس مركب. ترتيب الأعمدة في الفهرس مهم جداً.

فهرس مركب فعال
-- استعلام شائع
SELECT * FROM employees 
WHERE department = 'IT' AND salary > 5000;

-- فهرس مركب مثالي (الأكثر تحديداً أولاً)
CREATE INDEX idx_dept_salary ON employees(department, salary);

-- هذا الفهرس يعمل مع:
-- WHERE department = 'IT'
-- WHERE department = 'IT' AND salary > 5000

-- لكن لا يعمل بكفاءة مع:
-- WHERE salary > 5000 (فقط)
-- لأن salary ليس العمود الأول في الفهرس
متى لا تستخدم فهرس
  • الجداول الصغيرة جداً (أقل من 1000 صف)
  • الأعمدة التي تُحدّث كثيراً
  • الأعمدة ذات القيم المتكررة كثيراً (Low Cardinality)
  • عندما يكون الاستعلام يُرجع معظم صفوف الجدول

3. تجنب SELECT * (اختر الأعمدة المطلوبة فقط)

استخدام SELECT * هو أحد أكثر الأخطاء شيوعاً في كتابة استعلامات SQL.

مقارنة الأداء
-- بطيء: جلب جميع الأعمدة (بما فيها الأعمدة الكبيرة)
SELECT * FROM employees WHERE department = 'IT';
-- يجلب: employee_id, first_name, last_name, email, phone, address, 
--        bio (TEXT), profile_picture (BLOB), created_at, updated_at, etc.

-- سريع: جلب الأعمدة المطلوبة فقط
SELECT employee_id, first_name, last_name, email 
FROM employees 
WHERE department = 'IT';

-- الفوائد:
-- 1. تقليل حجم البيانات المنقولة
-- 2. استخدام أقل للذاكرة
-- 3. إمكانية استخدام Covering Index
Covering Index (الفهرس الشامل)
استخدام Covering Index
-- استعلام شائع
SELECT employee_id, first_name, last_name 
FROM employees 
WHERE department = 'IT';

-- إنشاء Covering Index (يحتوي على جميع الأعمدة المطلوبة)
CREATE INDEX idx_covering ON employees(department, employee_id, first_name, last_name);

-- الآن الاستعلام سريع جداً لأن جميع البيانات موجودة في الفهرس
-- لا حاجة للوصول للجدول الأساسي
EXPLAIN SELECT employee_id, first_name, last_name 
FROM employees 
WHERE department = 'IT';
-- Extra: Using index (ممتاز!)

4. تحسين استعلامات JOIN

استعلامات JOIN يمكن أن تكون بطيئة جداً إذا لم تُحسّن بشكل صحيح.

تأكد من وجود فهارس على أعمدة JOIN
تحسين JOIN بالفهارس
-- استعلام JOIN
SELECT e.first_name, d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id;

-- تأكد من وجود فهارس
-- PRIMARY KEY على departments.department_id (موجود تلقائياً)
-- فهرس على employees.department_id
CREATE INDEX idx_emp_dept ON employees(department_id);

-- الآن JOIN سريع جداً
استخدم الشروط في WHERE بدلاً من HAVING عندما يكون ممكناً
WHERE vs HAVING
-- بطيء: HAVING يُطبق بعد GROUP BY
SELECT department, COUNT(*) as emp_count
FROM employees
GROUP BY department
HAVING department = 'IT';

-- سريع: WHERE يُطبق قبل GROUP BY
SELECT department, COUNT(*) as emp_count
FROM employees
WHERE department = 'IT'
GROUP BY department;

-- WHERE يقلل عدد الصفوف قبل التجميع
تجنب JOIN غير الضروري
تبسيط الاستعلامات
-- إذا كنت تحتاج فقط لعدد الموظفين في كل قسم
-- بطيء: JOIN غير ضروري
SELECT d.department_name, COUNT(e.employee_id)
FROM departments d
LEFT JOIN employees e ON d.department_id = e.department_id
GROUP BY d.department_id;

-- سريع: استعلام مباشر
SELECT department, COUNT(*) 
FROM employees 
GROUP BY department;

5. تحسين استعلامات الاستعلامات الفرعية (Subqueries)

الاستعلامات الفرعية يمكن أن تكون بطيئة. في كثير من الحالات، يمكن استبدالها بـ JOIN.

تحويل Subquery إلى JOIN
-- بطيء: استعلام فرعي في WHERE
SELECT first_name, last_name
FROM employees
WHERE department_id IN (
    SELECT department_id 
    FROM departments 
    WHERE location = 'الرياض'
);

-- سريع: استخدام JOIN
SELECT e.first_name, e.last_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE d.location = 'الرياض';

-- JOIN عادةً أسرع من IN مع استعلام فرعي
استخدم EXISTS بدلاً من IN عند التحقق من الوجود
EXISTS vs IN
-- بطيء: IN مع استعلام فرعي كبير
SELECT d.department_name
FROM departments d
WHERE d.department_id IN (
    SELECT DISTINCT department_id FROM employees
);

-- سريع: EXISTS يتوقف عند أول تطابق
SELECT d.department_name
FROM departments d
WHERE EXISTS (
    SELECT 1 FROM employees e 
    WHERE e.department_id = d.department_id
);

-- EXISTS أسرع لأنه لا يحتاج لجلب جميع النتائج

6. تحسين استعلامات COUNT

استعلامات COUNT يمكن أن تكون بطيئة جداً مع الجداول الكبيرة.

تحسين COUNT
-- بطيء جداً مع الجداول الكبيرة
SELECT COUNT(*) FROM employees;

-- إذا كنت تحتاج فقط لتقدير تقريبي
SELECT TABLE_ROWS 
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'company_db' AND TABLE_NAME = 'employees';

-- استخدم COUNT(column) بدلاً من COUNT(*) إذا كان هناك فهرس
SELECT COUNT(employee_id) FROM employees;

-- للعد مع شرط، تأكد من وجود فهرس
SELECT COUNT(*) FROM employees WHERE department = 'IT';
-- يحتاج فهرس على department

7. استخدام LIMIT بحكمة

LIMIT يمكن أن يحسن الأداء بشكل كبير عندما لا تحتاج لجميع النتائج.

استخدام LIMIT
-- بدون LIMIT: يجلب جميع الصفوف
SELECT * FROM employees ORDER BY salary DESC;

-- مع LIMIT: يتوقف بعد 10 صفوف
SELECT * FROM employees ORDER BY salary DESC LIMIT 10;

-- لكن احذر من OFFSET الكبير
-- بطيء: يفحص 100000 صف ثم يتخطاها
SELECT * FROM employees LIMIT 100000, 10;

-- أفضل: استخدم WHERE مع آخر ID
SELECT * FROM employees 
WHERE employee_id > 100000 
ORDER BY employee_id 
LIMIT 10;

8. تجنب الدوال على الأعمدة في WHERE

استخدام الدوال على الأعمدة في WHERE يمنع استخدام الفهارس.

تجنب الدوال في WHERE
-- بطيء: الدالة على العمود تمنع استخدام الفهرس
SELECT * FROM employees 
WHERE YEAR(hire_date) = 2023;

-- سريع: استخدم نطاق بدلاً من الدالة
SELECT * FROM employees 
WHERE hire_date >= '2023-01-01' AND hire_date < '2024-01-01';

-- بطيء: LOWER على العمود
SELECT * FROM employees 
WHERE LOWER(email) = 'ahmed@example.com';

-- سريع: احفظ البيانات بصيغة موحدة أو استخدم فهرس خاص
SELECT * FROM employees 
WHERE email = 'ahmed@example.com';

9. مثال عملي شامل: تحسين استعلام بطيء

لنأخذ مثالاً واقعياً ونحسنه خطوة بخطوة.

الاستعلام الأصلي (بطيء)
استعلام بطيء
-- استعلام بطيء جداً (وقت التنفيذ: 5 ثواني)
SELECT *
FROM orders o
WHERE YEAR(order_date) = 2023
AND customer_id IN (
    SELECT customer_id 
    FROM customers 
    WHERE city = 'الرياض'
)
ORDER BY order_date DESC;

-- تحليل المشاكل:
EXPLAIN SELECT * FROM orders o
WHERE YEAR(order_date) = 2023
AND customer_id IN (SELECT customer_id FROM customers WHERE city = 'الرياض')
ORDER BY order_date DESC;

-- المشاكل:
-- 1. SELECT * (يجلب أعمدة غير ضرورية)
-- 2. YEAR(order_date) يمنع استخدام الفهرس
-- 3. استعلام فرعي بطيء
-- 4. لا يوجد فهرس على city
-- 5. type: ALL (فحص كامل للجدول)
الخطوة 1: إضافة الفهارس المناسبة
إضافة فهارس
-- إضافة فهارس
CREATE INDEX idx_order_date ON orders(order_date);
CREATE INDEX idx_customer_city ON customers(city);
CREATE INDEX idx_order_customer ON orders(customer_id, order_date);
الخطوة 2: تحسين الاستعلام
استعلام محسّن
-- استعلام محسّن (وقت التنفيذ: 0.05 ثانية)
SELECT 
    o.order_id,
    o.customer_id,
    o.order_date,
    o.total_amount,
    o.status
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE c.city = 'الرياض'
  AND o.order_date >= '2023-01-01' 
  AND o.order_date < '2024-01-01'
ORDER BY o.order_date DESC;

-- التحسينات:
-- 1. اختيار الأعمدة المطلوبة فقط
-- 2. استبدال YEAR() بنطاق تاريخ
-- 3. استبدال IN بـ JOIN
-- 4. استخدام الفهارس المضافة
-- النتيجة: تحسين 100x في السرعة!

10. أدوات مراقبة الأداء

تفعيل Slow Query Log
تتبع الاستعلامات البطيئة
-- تفعيل سجل الاستعلامات البطيئة
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- الاستعلامات أبطأ من ثانية
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

-- عرض الإعدادات
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
استخدام SHOW PROFILE
تحليل تفصيلي للأداء
-- تفعيل Profiling
SET profiling = 1;

-- تنفيذ الاستعلام
SELECT * FROM employees WHERE department = 'IT';

-- عرض الملفات الشخصية
SHOW PROFILES;

-- عرض تفاصيل استعلام معين
SHOW PROFILE FOR QUERY 1;

-- عرض تفاصيل CPU و I/O
SHOW PROFILE CPU, BLOCK IO FOR QUERY 1;

11. قائمة التحقق من الأداء

قبل النشر، تحقق من
  • هل استخدمت EXPLAIN لتحليل جميع الاستعلامات المهمة؟
  • هل أضفت فهارس على الأعمدة المستخدمة في WHERE و JOIN؟
  • هل تجنبت SELECT * واخترت الأعمدة المطلوبة فقط؟
  • هل تجنبت الدوال على الأعمدة في WHERE؟
  • هل استبدلت الاستعلامات الفرعية بـ JOIN عندما يكون ممكناً؟
  • هل استخدمت LIMIT عند عدم الحاجة لجميع النتائج؟
  • هل type في EXPLAIN أفضل من ALL؟
  • هل عدد rows في EXPLAIN معقول؟
ملخص الدرس

في هذا الدرس الشامل، تعلمنا كل شيء عن تحسين الاستعلامات في SQL:

  • استخدام EXPLAIN لتحليل خطة التنفيذ
  • تحسين استخدام الفهارس والفهارس المركبة
  • تجنب SELECT * واستخدام Covering Index
  • تحسين استعلامات JOIN
  • استبدال الاستعلامات الفرعية بـ JOIN
  • تحسين COUNT و LIMIT
  • تجنب الدوال على الأعمدة في WHERE
  • مثال عملي شامل لتحسين استعلام بطيء
  • أدوات مراقبة الأداء

تحسين الاستعلامات هو مهارة أساسية لأي مطور يعمل مع قواعد البيانات. الفرق بين استعلام محسّن وآخر غير محسّن يمكن أن يكون من ميلي ثانية إلى دقائق، مما يؤثر بشكل كبير على تجربة المستخدم وتكلفة البنية التحتية.

الخطوة التالية: أفضل الممارسات (Best Practices)

أكمل رحلتك التعليمية وانتقل إلى الدرس التالي لتعلم أفضل الممارسات (Best Practices) وتطوير مهاراتك في قواعد البيانات.

الانتقال إلى الدرس التالي
المحرر الذكي

اكتب الكود وشاهد النتيجة فوراً

جرب الآن مجاناً
قناة ديف عربي

تابع أحدث الدروس والتحديثات مباشرة على واتساب

انضم الآن