تحسين الاستعلامات

تحسين الاستعلامات

💡 تشبيه من الحياة

تخيل أنك تبحث عن كتاب في مكتبة:

📖 المفاهيم الأساسية

1. خطة تنفيذ EXPLAIN

EXPLAIN هو أداة التحسين الأساسية. يخبرك كيف تنفذ قاعدة البيانات جملة SQL.

SQL
EXPLAIN SELECT * FROM users WHERE username = 'alice';

تفسير الحقول الرئيسية:

الحقل المعنى التركيز
type نوع الوصول ALL(مسح كامل) →indexrangerefeq_refconst، كلما اتجهت يميناً كان أفضل
key الفهرس المستخدم فعلياً NULL يعني لا فهرس مستخدم
rows الصفوف المقدرة للمسح كلما قلّ كان أفضل
Extra معلومات إضافية Using filesort(فرز مطلوب)، Using temporary(جدول مؤقت مطلوب) تتطلب انتباه
possible_keys الفهارس المحتملة تساعد في تحليل ما إذا كانت الفهارس تُختار
SQL
-- عرض خطة التنفيذ
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;

أنواع الوصول مشروحة:

TEXT
من الأسوأ إلى الأفضل:
ALL         → مسح كامل للجدول (يجب التحسين)
index       → مسح كامل للفهرس (أفضل قليلاً من ALL)
range       → مسح نطاق الفهرس (WHERE id > 100)
ref         → بحث فهرس غير فريد (WHERE username = 'alice')
eq_ref      → بحث فهرس فريد (JOIN على المفتاح الأساسي)
const       → بحث ثابت (WHERE id = 1، الأسرع)
system      → جدول النظام (نادراً ما يظهر)

2. استراتيجيات تحسين الفهارس

SQL
-- 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. تجنب المسح الكامل للجدول

SQL
-- ✅أنماط تسبب مسحاً كاملاً للجدول

-- 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. تقنيات إعادة كتابة الاستعلامات

SQL
-- 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. سجل الاستعلام البطيء

SQL
-- عرض إعدادات سجل الاستعلام البطيء
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 للأرقام اختر الأنواع المناسبة

💡 الصياغة الأساسية

SQL
-- الاستخدام الأساسي لـ 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

SQL
-- 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;

المفتاح الأساسي التلقائي

SQL
-- 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));

دوال النصوص

SQL
-- ربط النصوص
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');   -- جميع قواعد البيانات

دوال التاريخ

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

التعبيرات الشرطية

SQL
-- تعبير 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);                                                        -- جميع قواعد البيانات

النوع المنطقي

SQL
-- 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 (تحديث إذا موجود، إدراج إذا غير موجود)

SQL
-- 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);

دعم دوال النافذة

SQL
-- جميع قواعد البيانات الرئيسية تدعمها (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

مثال: تحديد وتحسين استعلام بطيء (الصعوبة 🔥

الاستعلام البطيء الأصلي:

SQL
-- استعلام إجمالي مبلغ الطلب لكل مستخدم في آخر 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 يكشف:

خطوات التحسين:

SQL
-- 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: انخفض بشكل ملحوظ

مثال: تحسين التصفح العميق (الصعوبة ⭐⭐)

الاستعلام المشكل:

SQL
-- استعلام صفحة 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: تصفح بالمؤشر (موصى به)

SQL
-- تذكر آخر 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: ارتباط مؤجّل

SQL
-- أولاً استعلم عن المفاتيح الأساسية، ثم اربط للحصول على البيانات الكاملة
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: تحسين طبقة العمل

SQL
-- إذا سُمح بذلك، حدّد أقصى عمق للتصفح
-- اسمح فقط بعرض أول 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: تحسين استعلام قائمة منتجات التجارة الإلكترونية

SQL
-- الاستعلام الأصلي: تصفية متعددة الشروط + فرز + تصفح
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: تحسين استعلام التقرير الإحصائي

SQL
-- الاستعلام الأصلي: إحصائيات الطلبات الشهرية (بيانات كبيرة، بطيء في كل مرة)
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:

📝 تمارين

  1. شغّل تحليل 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

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%