تحسين الاستعلامات
تحسين الاستعلامات
💡 تشبيه من الحياة
تخيل أنك تبحث عن كتاب في مكتبة:
- مسح الجدول بالكامل: ابحث في كل رف في المكتبة → بطيء
- الفهرس: افحص أولاً بطاقة الدليل، اجد رقم الرف وامشِ مباشرة → سريع
- خطة التنفيذ: أمين المكتبة يخبرك مسبقاً "البحث بفهرس المؤلف أسرع من البحث بالعنوان" → EXPLAIN
- سجل الاستعلام البطيء: تسجل المكتبة "الكتب التي استغرق العثور عليها أكثر من 10 دقائق" لتحسين الرفوف → سجل الاستعلام البطيء
- إعادة كتابة الاستعلام: بدلاً من السؤال "جميع الكتب التي تحتوي على 'علوم'"، ضيّق أولاً حسب الفئة ثم ابحث بدقة → تحسين طريقة الاستعلام
📖 المفاهيم الأساسية
1. خطة تنفيذ EXPLAIN
EXPLAIN هو أداة التحسين الأساسية. يخبرك كيف تنفذ قاعدة البيانات جملة SQL.
EXPLAIN SELECT * FROM users WHERE username = 'alice';
تفسير الحقول الرئيسية:
| الحقل | المعنى | التركيز |
|---|---|---|
type |
نوع الوصول | ALL(مسح كامل) →index →range →ref →eq_ref →const، كلما اتجهت يميناً كان أفضل |
key |
الفهرس المستخدم فعلياً | NULL يعني لا فهرس مستخدم |
rows |
الصفوف المقدرة للمسح | كلما قلّ كان أفضل |
Extra |
معلومات إضافية | Using filesort(فرز مطلوب)، Using temporary(جدول مؤقت مطلوب) تتطلب انتباه |
possible_keys |
الفهارس المحتملة | تساعد في تحليل ما إذا كانت الفهارس تُختار |
-- عرض خطة التنفيذ
EXPLAIN SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2024-01-01'
GROUP BY u.id, u.name;
-- MySQL 8.0+ يمكن عرض إحصائيات التنفيذ الفعلية
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 100;
أنواع الوصول مشروحة:
من الأسوأ إلى الأفضل:
ALL → مسح كامل للجدول (يجب التحسين)
index → مسح كامل للفهرس (أفضل قليلاً من ALL)
range → مسح نطاق الفهرس (WHERE id > 100)
ref → بحث فهرس غير فريد (WHERE username = 'alice')
eq_ref → بحث فهرس فريد (JOIN على المفتاح الأساسي)
const → بحث ثابت (WHERE id = 1، الأسرع)
system → جدول النظام (نادراً ما يظهر)
2. استراتيجيات تحسين الفهارس
-- 1. إنشاء فهارس لحقول WHERE و JOIN و ORDER BY الشائعة
CREATE INDEX idx_user_email ON users(email);
CREATE INDEX idx_order_user_created ON orders(user_id, created_at);
-- 2. فهرس شامل: حقول الاستعلام كلها في الفهرس، لا حاجة للبحث في الجدول
-- إذا كان الاستعلام التالي متكرراً: SELECT id, user_id, created_at FROM orders WHERE user_id = ?
CREATE INDEX idx_order_covering ON orders(user_id, created_at, id);
-- 3. فهرس البادئة: فهرسة أول N حرف فقط من الحقول النصية الطويلة
CREATE INDEX idx_user_name_prefix ON users(username(10));
-- 4. الفهرس المركب يتبع قاعدة أقصى اليسار
CREATE INDEX idx_abc ON table_name(a, b, c);
-- يمكن مطابقة: WHERE a=1 | WHERE a=1 AND b=2 | WHERE a=1 AND b=2 AND c=3
-- لا يمكن مطابقة: WHERE b=2 | WHERE c=3 | WHERE b=2 AND c=3
-- 5. عرض فهارس جدول
SHOW INDEX FROM orders;
-- 6. عرض عدد القيم الفريدة للفهرس (كلما زاد كان أفضل تمييزاً)
SELECT
INDEX_NAME,
COLUMN_NAME,
CARDINALITY
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_NAME = 'orders';
3. تجنب المسح الكامل للجدول
-- ✅أنماط تسبب مسحاً كاملاً للجدول
-- 1. استخدام دوال على أعمدة مفهرسة
SELECT * FROM orders WHERE YEAR(created_at) = 2024;
-- ✅أعد الكتابة كاستعلام نطاق
SELECT * FROM orders WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
-- 2. إجراء عمليات حسابية على أعمدة مفهرسة
SELECT * FROM orders WHERE id + 1 = 100;
-- ✅أعد الكتابة كـ
SELECT * FROM orders WHERE id = 99;
-- 3. التحويل الضمني للنوع (الهاتف VARCHAR، تمرير INT)
SELECT * FROM users WHERE phone = 13800138000;
-- ✅أعد الكتابة كـ
SELECT * FROM users WHERE phone = '13800138000';
-- 4. LIKE مع حرف بدل رائد
SELECT * FROM users WHERE name LIKE '%alice%';
-- ✅إذا كان البحث الضبابي ضرورياً، فكّر في فهرس النص الكامل
ALTER TABLE users ADD FULLTEXT INDEX ft_name(name);
SELECT * FROM users WHERE MATCH(name) AGAINST('alice' IN BOOLEAN MODE);
-- 5. شروط OR قد تسبب فشل الفهرس
SELECT * FROM users WHERE status = 1 OR age > 25;
-- ✅استخدم UNION بدلاً منه
SELECT * FROM users WHERE status = 1
UNION
SELECT * FROM users WHERE age > 25;
-- 6. NOT IN / NOT EXISTS قد يسببان مسحاً كاملاً
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM orders);
-- ✅استخدم LEFT JOIN + IS NULL بدلاً منه
SELECT u.* FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.id IS NULL;
4. تقنيات إعادة كتابة الاستعلامات
-- 1. استخدم EXISTS بدلاً من IN (أكثر كفاءة لمجموعات البيانات الكبيرة)
-- بطيء
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000);
-- سريع
SELECT * FROM users u WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.amount > 1000
);
-- 2. استخدم UNION ALL بدلاً من UNION (عندما لا حاجة لإزالة التكرار)
-- UNION يزيل التكرار ويفرز؛ UNION ALL لا يفعل
SELECT name FROM users_2023 UNION ALL SELECT name FROM users_2024;
-- 3. تجنب SELECT *، اختر فقط الأعمدة المطلوبة
-- بطيء
SELECT * FROM orders WHERE user_id = 1;
-- سريع
SELECT id, order_no, total_amount, status FROM orders WHERE user_id = 1;
-- 4. تحسين التصفح (مشكلة التصفح العميق)
-- بطيء (OFFSET 100000 يتطلب مسح 100100 صف)
SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 100000;
-- سريع (تصفح بالمؤشر، تذكر آخر id من الصفحة السابقة)
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 10;
-- 5. العمليات المجمعة بدلاً من العمليات في حلقة
-- بطيء (إدراج في حلقة)
INSERT INTO logs (msg) VALUES ('a');
INSERT INTO logs (msg) VALUES ('b');
INSERT INTO logs (msg) VALUES ('c');
-- سريع (إدراج مجمع)
INSERT INTO logs (msg) VALUES ('a'), ('b'), ('c');
-- 6. تجنب استخدام <> أو != على أعمدة مفهرسة في WHERE
SELECT * FROM users WHERE status != 0;
-- ✅إذا كانت قيم الحالة قليلة، أعد الكتابة كـ
SELECT * FROM users WHERE status IN (1, 2, 3);
5. سجل الاستعلام البطيء
-- عرض إعدادات سجل الاستعلام البطيء
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- تفعيل سجل الاستعلام البطيء
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- تسجيل الاستعلامات التي تتجاوز ثانية واحدة
SET GLOBAL log_queries_not_using_indexes = 'ON'; -- تسجيل الاستعلامات التي لا تستخدم الفهارس
-- تحليل سجل الاستعلام البطيء
-- باستخدام أداة mysqldumpslow
-- mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
-- باستخدام pt-query-digest (Percona Toolkit)
-- pt-query-digest /var/log/mysql/slow.log
6. قائمة التحقق من الضبط
| الخطوة | الإجراء | الأداة |
|---|---|---|
| 1 | فعّل سجل الاستعلام البطيء، اعثر على SQL البطيء | slow_query_log |
| 2 | حلّل خطة التنفيذ بـ EXPLAIN | EXPLAIN |
| 3 | تحقق مما إذا كانت الفهارس تُضرب | حقل type |
| 4 | تحقق من filesort/temporary | حقل Extra |
| 5 | أعد كتابة SQL أو أضف فهارس | DDL / إعادة كتابة SQL |
| 6 | تحقق من نتائج التحسين | قارن أوقات التنفيذ |
7. مزالق الأداء الشائعة
| المزالق | الوصف | الحل |
|---|---|---|
| SELECT * | يختار جميع الأعمدة، لا يمكن استخدام الفهرس الشامل | اختر فقط الأعمدة المطلوبة |
| OFFSET كبير | التصفح العميق يمسح كميات كبيرة من البيانات | تصفح بالمؤشر |
| استعلامات N+1 | استعلام البيانات ذات الصلة واحداً تلو الآخر في حلقة | استعلام مجمع أو JOIN |
| JOIN بدون فهارس | ارتباط جداول كبيرة بدون فهارس | أضف فهارس على أعمدة JOIN |
| معاملات كبيرة | الاحتفاظ بالقفل لوقت طويل | تقليل نطاق المعاملة |
| نوع بيانات خاطئ | استخدام VARCHAR للأرقام | اختر الأنواع المناسبة |
💡 الصياغة الأساسية
-- الاستخدام الأساسي لـ EXPLAIN
EXPLAIN SELECT ...;
EXPLAIN ANALYZE SELECT ...; -- MySQL 8.0+
-- عرض وقت تنفيذ الاستعلام
SET profiling = 1;
SELECT ...;
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;
-- عرض الفهارس
SHOW INDEX FROM table_name;
-- فرض استخدام فهرس محدد
SELECT * FROM orders FORCE INDEX (idx_user_id) WHERE user_id = 100;
-- تجاهل الفهرس (للمقارنة)
SELECT * FROM orders IGNORE INDEX (idx_user_id) WHERE user_id = 100;
EXPLAIN هو أفضل صديق لك.
مقارنة لهجات قواعد البيانات
لقواعد البيانات المختلفة اختلافات كبيرة في الصياغة. إليك مقارنة للعمليات الشائعة:
تصفح LIMIT
-- MySQL / PostgreSQL / SQLite
SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 20;
-- SQL Server
SELECT * FROM orders ORDER BY id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
-- قبل SQL Server 2012
SELECT TOP 10 * FROM orders WHERE id NOT IN (SELECT TOP 20 id FROM orders ORDER BY id);
-- Oracle 12c+
SELECT * FROM orders ORDER BY id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
-- قبل Oracle 12c
SELECT * FROM (SELECT o.*, ROWNUM rn FROM orders o WHERE ROWNUM <= 30) WHERE rn > 20;
المفتاح الأساسي التلقائي
-- MySQL
CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50));
-- أو استخدم القيمة الافتراضية
INSERT INTO users (name) VALUES ('Alice'); -- id يُنشأ تلقائياً
-- PostgreSQL
CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(50));
-- PostgreSQL 10+
CREATE TABLE users (id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name VARCHAR(50));
-- SQLite
CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);
-- SQLite يدعم أيضاً ROWID
INSERT INTO users (name) VALUES ('Alice');
-- SQL Server
CREATE TABLE users (id INT IDENTITY(1,1) PRIMARY KEY, name NVARCHAR(50));
-- Oracle
CREATE TABLE users (id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name VARCHAR2(50));
دوال النصوص
-- ربط النصوص
SELECT CONCAT('Hello', ' ', 'World'); -- MySQL, PostgreSQL
SELECT 'Hello' || ' ' || 'World'; -- PostgreSQL, SQLite, Oracle
SELECT name + ' ' + email FROM users; -- SQL Server
-- سلسلة فرعية
SELECT SUBSTRING('Hello World', 1, 5); -- MySQL, SQL Server
SELECT SUBSTR('Hello World', 1, 5); -- PostgreSQL, SQLite, Oracle
-- طول النص
SELECT LENGTH('Hello'); -- MySQL, PostgreSQL, SQLite
SELECT LEN('Hello'); -- SQL Server
-- تحويل الحالة
SELECT UPPER('hello'), LOWER('HELLO'); -- جميع قواعد البيانات
SELECT UCASE('hello'), LCASE('HELLO'); -- MySQL يدعم أيضاً
-- قص المسافات
SELECT TRIM(' Hello '); -- جميع قواعد البيانات
SELECT LTRIM(' Hello'), RTRIM('Hello '); -- MySQL, SQL Server, PostgreSQL
-- استبدال
SELECT REPLACE('Hello World', 'World', 'SQL'); -- جميع قواعد البيانات
دوال التاريخ
-- الوقت الحالي
SELECT NOW(); -- MySQL, PostgreSQL
SELECT CURRENT_TIMESTAMP; -- جميع قواعد البيانات
SELECT GETDATE(); -- SQL Server
SELECT datetime('now'); -- SQLite
-- العمليات الحسابية على التواريخ
SELECT DATE_ADD('2024-01-01', INTERVAL 30 DAY); -- MySQL
SELECT '2024-01-01'::DATE + INTERVAL '30 days'; -- PostgreSQL
SELECT DATEADD(DAY, 30, '2024-01-01'); -- SQL Server
SELECT date('2024-01-01', '+30 days'); -- SQLite
-- استخراج سنة/شهر/يوم
SELECT YEAR(created_at), MONTH(created_at), DAY(created_at) FROM orders; -- MySQL, SQL Server
SELECT EXTRACT(YEAR FROM created_at) FROM orders; -- PostgreSQL, MySQL 8.0+
SELECT strftime('%Y', created_at) FROM orders; -- SQLite
-- تنسيق التاريخ
SELECT DATE_FORMAT(created_at, '%Y-%m-%d') FROM orders; -- MySQL
SELECT TO_CHAR(created_at, 'YYYY-MM-DD') FROM orders; -- PostgreSQL, Oracle
SELECT FORMAT(created_at, 'yyyy-MM-dd') FROM orders; -- SQL Server
SELECT strftime('%Y-%m-%d', created_at) FROM orders; -- SQLite
التعبيرات الشرطية
-- تعبير IF
SELECT IF(score >= 60, 'ناجح', 'راسب') FROM exams; -- MySQL
SELECT IIF(score >= 60, 'ناجح', 'راسب') FROM exams; -- SQL Server
SELECT CASE WHEN score >= 60 THEN 'ناجح' ELSE 'راسب' END FROM exams; -- جميع قواعد البيانات
-- COALESCE (يعيد أول قيمة غير NULL)
SELECT COALESCE(nickname, username, 'مجهول') FROM users; -- جميع قواعد البيانات
-- NULLIF (يعيد NULL عند التساوي)
SELECT NULLIF(a, b); -- جميع قواعد البيانات
النوع المنطقي
-- MySQL: لا يوجد BOOLEAN أصلي، استخدم TINYINT(1) بدلاً منه
CREATE TABLE users (is_active TINYINT(1) DEFAULT 1);
SELECT * FROM users WHERE is_active = TRUE; -- TRUE يساوي 1
-- PostgreSQL: BOOLEAN أصلي
CREATE TABLE users (is_active BOOLEAN DEFAULT TRUE);
SELECT * FROM users WHERE is_active = TRUE;
-- SQLite: لا يوجد BOOLEAN أصلي، استخدم INTEGER
CREATE TABLE users (is_active INTEGER DEFAULT 1);
SELECT * FROM users WHERE is_active = 1;
-- SQL Server: BIT أصلي
CREATE TABLE users (is_active BIT DEFAULT 1);
SELECT * FROM users WHERE is_active = 1;
UPSERT (تحديث إذا موجود، إدراج إذا غير موجود)
-- MySQL
INSERT INTO stats (article_id, view_count) VALUES (1, 1)
ON DUPLICATE KEY UPDATE view_count = view_count + 1;
-- PostgreSQL
INSERT INTO stats (article_id, view_count) VALUES (1, 1)
ON CONFLICT (article_id) DO UPDATE SET view_count = stats.view_count + 1;
-- SQLite
INSERT INTO stats (article_id, view_count) VALUES (1, 1)
ON CONFLICT(article_id) DO UPDATE SET view_count = view_count + 1;
-- SQL Server
MERGE INTO stats AS target
USING (SELECT 1 AS article_id, 1 AS view_count) AS source
ON target.article_id = source.article_id
WHEN MATCHED THEN UPDATE SET view_count = target.view_count + 1
WHEN NOT MATCHED THEN INSERT (article_id, view_count) VALUES (source.article_id, source.view_count);
دعم دوال النافذة
-- جميع قواعد البيانات الرئيسية تدعمها (MySQL 8.0+, PostgreSQL, SQL Server, SQLite 3.25+)
SELECT
name,
score,
RANK() OVER (ORDER BY score DESC) AS ranking,
ROW_NUMBER() OVER (PARTITION BY class_id ORDER BY score DESC) AS class_rank
FROM students;
-- MySQL 5.7 وما قبله لا يدعم دوال النافذة؛ استخدم المتغيرات للمحاكاة
-- GROUP_CONCAT / STRING_AGG
SELECT category_id, GROUP_CONCAT(name SEPARATOR ',') FROM products GROUP BY category_id; -- MySQL
SELECT category_id, STRING_AGG(name, ',') FROM products GROUP BY category_id; -- PostgreSQL
مثال: تحديد وتحسين استعلام بطيء (الصعوبة 🔥
الاستعلام البطيء الأصلي:
-- استعلام إجمالي مبلغ الطلب لكل مستخدم في آخر 30 يوماً (افترض جدول المستخدمين 100K صف، جدول الطلبات 1M صف)
EXPLAIN
SELECT u.name, u.email, SUM(o.total_amount) AS total_spent
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.name, u.email
ORDER BY total_spent DESC
LIMIT 20;
تحليل EXPLAIN يكشف:
type: ALL(مسح كامل لجدول الطلبات)rows: 1000000(مسح ملايين الصفوف)Extra: Using temporary; Using filesort
خطوات التحسين:
-- 1. إضافة فهرس مركب على جدول الطلبات
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at);
-- 2. تحسين الاستعلام: تصفية أولاً، ثم ارتباط
SELECT u.name, u.email, sub.total_spent
FROM users u
INNER JOIN (
SELECT user_id, SUM(total_amount) AS total_spent
FROM orders
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY user_id
ORDER BY total_spent DESC
LIMIT 20
) sub ON u.id = sub.user_id
ORDER BY sub.total_spent DESC;
-- 3. EXPLAIN مرة أخرى لتأكيد تأثير التحسين
EXPLAIN SELECT u.name, u.email, sub.total_spent ...
-- type: ref، rows: انخفض بشكل ملحوظ
مثال: تحسين التصفح العميق (الصعوبة ⭐⭐)
الاستعلام المشكل:
-- استعلام صفحة 10000 (20 في الصفحة)، OFFSET 200000
SELECT id, title, created_at
FROM articles
WHERE status = 1
ORDER BY created_at DESC
LIMIT 20 OFFSET 200000;
-- حتى مع فهرس، يتطلب مسح 200020 صف، بطيء جداً
الحل 1: تصفح بالمؤشر (موصى به)
-- تذكر آخر created_at و id من الصفحة السابقة
-- افترض آخر عنصر في الصفحة السابقة: created_at='2024-03-15 10:30:00', id=50001
SELECT id, title, created_at
FROM articles
WHERE status = 1
AND (created_at < '2024-03-15 10:30:00'
OR (created_at = '2024-03-15 10:30:00' AND id < 50001))
ORDER BY created_at DESC, id DESC
LIMIT 20;
-- يمسح 20 صف فقط، سريع للغاية
الحل 2: ارتباط مؤجّل
-- أولاً استعلم عن المفاتيح الأساسية، ثم اربط للحصول على البيانات الكاملة
SELECT a.id, a.title, a.created_at
FROM articles a
INNER JOIN (
SELECT id FROM articles
WHERE status = 1
ORDER BY created_at DESC
LIMIT 20 OFFSET 200000
) b ON a.id = b.id;
-- الاستعلام الفرعي يستخدم الفهرس الشامل؛ الاستعلام الرئيسي يجلب البيانات بالمفتاح الأساسي
الحل 3: تحسين طبقة العمل
-- إذا سُمح بذلك، حدّد أقصى عمق للتصفح
-- اسمح فقط بعرض أول 1000 نتيجة، وانبه المستخدمين thu>استخدم thu>البحث thu>لتضييق النطاق
SELECT id, title, created_at
FROM articles
WHERE status = 1
AND category_id = 5 -- إضافة شروط تصفية
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;
🔧 السيناريو 1: تحسين استعلام قائمة منتجات التجارة الإلكترونية
-- الاستعلام الأصلي: تصفية متعددة الشروط + فرز + تصفح
SELECT p.id, p.name, p.price, p.sales_count, c.name AS category_name
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
WHERE p.status = 1
AND p.category_id IN (10, 11, 12, 13)
AND p.price BETWEEN 50 AND 500
ORDER BY p.sales_count DESC
LIMIT 20;
-- التحسين:
-- 1. إنشاء فهرس مركب يغطي شروط التصفية الرئيسية
CREATE INDEX idx_product_filter ON products(status, category_id, price, sales_count);
-- 2. إذا أظهر EXPLAIN filesort، اضبط ترتيب الفهرس
CREATE INDEX idx_product_sort ON products(status, category_id, sales_count DESC, price);
-- 3. إذا كان جدول الفئات صغيراً، ألغِ تطبيع اسم الفئة في جدول المنتجات
ALTER TABLE products ADD COLUMN category_name VARCHAR(50);
-- يُحدّث عند INSERT/UPDATE
🔧 السيناريو 2: تحسين استعلام التقرير الإحصائي
-- الاستعلام الأصلي: إحصائيات الطلبات الشهرية (بيانات كبيرة، بطيء في كل مرة)
SELECT
DATE_FORMAT(created_at, '%Y-%m') AS month,
COUNT(*) AS order_count,
SUM(total_amount) AS revenue
FROM orders
WHERE created_at >= '2023-01-01'
GROUP BY month
ORDER BY month;
-- خيار التحسين 1: جدول ملخص محسوب مسبقاً
CREATE TABLE monthly_order_stats (
month_key VARCHAR(7) PRIMARY KEY COMMENT 'الصيغة: 2024-01',
order_count INT NOT NULL DEFAULT 0,
revenue DECIMAL(15,2) NOT NULL DEFAULT 0,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- مهمة مجدولة تُحدّث يومياً (حساب تراكمي)
INSERT INTO monthly_order_stats (month_key, order_count, revenue)
SELECT
DATE_FORMAT(created_at, '%Y-%m'),
COUNT(*),
SUM(total_amount)
FROM orders
WHERE created_at >= CURDATE() - INTERVAL 1 DAY
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
ON DUPLICATE KEY UPDATE
order_count = VALUES(order_count),
revenue = VALUES(revenue),
updated_at = NOW();
-- استعلام جدول الملخص (استجابة بالملي ثانية)
SELECT * FROM monthly_order_stats ORDER BY month_key;
❓ أسئلة شائعة
س: هل المزيد من الفهارس يعني دائماً أفضل؟ ج: لا. الفهارس تستهلك مساحة تخزين وتبطئ عمليات INSERT/UPDATE/DELETE (كل عملية كتابة يجب أن تحدّث الفهارس). أنشئ فهارس فقط للاستعلامات التي تحتاج فعلاً للتسريع، ونظّف الفهارس غير المستخدمة بانتظام.
س: كيف أختار ترتيب الحقول للفهرس المركب؟ ج: ضع الحقول ذات القيمة الفريدة العالية أولاً (مثل user_id أعلى من status)؛ ضع حقول المساواة قبل حقول النطاق؛ قرر بناءً على تركيبات شروط الاستعلام الفعلية.
س: استعلامي سريع بالفعل، هل يجب أن أحسّنه؟ ج: إذا كان وقت الاستجابة ضمن النطاق المقبول، لا حاجة للتحسين المفرط. لكن كن على دراية بأن الأداء قد يتدهور مع نمو البيانات؛ خطّط مسبقاً باختبارات الحمولة واستراتيجية الفهرسة.
س: هل الصفوف التي يعرضها EXPLAIN دقيقة؟ ج: ليست دقيقة تماماً؛ هي تقديرات مبنية على الإحصائيات. التنفيذ الفعلي قد يمسح صفوفاً أكثر أو أقل. تشغيل
ANALYZE TABLEلتحديث الإحصائيات يمكن أن يحسن دقة التقدير.
📖 ملخص
غطي هذا الدرس بشكل منهجي طرق تحسين استعلامات SQL:
- EXPLAIN هو الأداة الأساسية لتحليل خطط التنفيذ؛ ركّز على
typeوkeyوrowsوExtra - تحسين الفهارس: أنشئ الفهارس بحكمة، اتبع قاعدة أقصى اليسار، استخدم الفهارس الشاملة
- تجنب المسح الكامل: لا تستخدم دوال/عمليات حسابية على أعمدة مفهرسة، انتبه للتحويل الضمني للنوع، استخدم LIKE %xxx% بحذر
- إعادة كتابة الاستعلام: EXISTS بدلاً من IN، تصفح بالمؤشر بدلاً من OFFSET الكبير، عمليات مجمعة بدلاً من الحلقات
- سجلات الاستعلام البطيء: حدد SQL ذا العنقود في الزجاجة للتحسين المستهدف
- لهجات قواعد البيانات: MySQL/PostgreSQL/SQLite/SQL Server لها اختلافات كبيرة في الصياغة؛ انتبه عند كتابة SQL متعدد القواعد
📝 تمارين
- شغّل تحليل EXPLAIN على SQL التالي وحسّنه:SQL
SELECT * FROM orders WHERE YEAR(created_at) = 2024 AND user_id IN (SELECT id FROM users WHERE status = 1);
2.صمم استراتيجية فهرسة مناسبة لصفحة قائمة المقالات (دعم تصفية الفئة، فرز زمني، وتصفح).
3. أعد كتابة استعلام يستخدم LIMIT 20 OFFSET 100000 لاستخدام التصفح بالمؤشر.
الدرس التالي ←28-project.md



