طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 121 · مبادئ قواعد البيانات
الدرس 25 · الوحدة 11: تحسين أداء استعلامات SQL

قواعد كتابة استعلام سريع

الصياغة نفسها تحدد إن كان الفهرس سيُستخدم أم لا. ⏱ نحو 30 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تكتب شروطًا تترك العمود المفهرس عاريًا من الدوال والحسابات.
  • تميّز LIKE التي تستفيد من الفهرس والتي لا تستفيد.
  • تستخدم EXISTS و LIMIT واختيار الأعمدة لتقليل العمل.
  • تتجنب مشكلة N+1 بـ JOIN، وتتحقق أن التحسين لم يغيّر النتيجة.

١الفهرس وحده لا يكفي

قد يكون الفهرس موجودًا، ويرفض المحسّن استخدامه بسبب طريقة كتابة الاستعلام. القاعدة العامة: يستخدم النظام الفهرس حين يكون العمود في الشرط عاريًا، أي كما هو، دون دالة أو عملية حسابية عليه. في هذا الدرس سبع قواعد، ولكل قاعدة تجربة تقيسها بنفسك على قاعدة «المبيعات الكبيرة».

٢القاعدة 1: لا تلفّ العمود المفهرس بدالة

«مبيعات مارس 2025». الصياغة الأولى تبدو طبيعية، لكنها تحسب substr لكل صف من 200 ألف، فلا تستطيع القفز في الفهرس:

query.sql
CREATE INDEX idx_sales_date ON sales (sale_date); EXPLAIN QUERY PLANSELECT COUNT(*) FROM salesWHERE substr(sale_date, 1, 7) = '2025-03';
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SCAN sales USING COVERING INDEX idx_sales_date

والصياغة الصحيحة نطاق على العمود العاري؛ النتيجة نفسها، والخطة بحث في الفهرس:

query.sql
CREATE INDEX idx_sales_date ON sales (sale_date); EXPLAIN QUERY PLANSELECT COUNT(*) FROM salesWHERE sale_date >= '2025-03-01' AND sale_date < '2025-04-01'; SELECT COUNT(*) AS march_sales FROM salesWHERE sale_date >= '2025-03-01' AND sale_date < '2025-04-01';
✓ أُنشئ الفهرس
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SEARCH sales USING COVERING INDEX idx_sales_date (sale_date>? AND sale_date<?)
النتيجة · صف واحد
march_sales
8494

وفي MySQL الخطأ نفسه شائع بالدالة YEAR(sale_date) = 2025، وصوابه sale_date >= '2025-01-01' AND sale_date < '2026-01-01'.

٣القاعدة 2: انقل الحساب إلى الطرف الآخر

«المبيعات التي تتجاوز 1000 ريال بعد ضريبة 15%»:

✗ خطأ
query.sql
CREATE INDEX idx_sales_amount ON sales (amount);EXPLAIN QUERY PLANSELECT * FROM sales WHERE amount * 1.15 > 1000;
✓ أُنشئ الفهرس
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SCAN sales
✓ صواب
query.sql
CREATE INDEX idx_sales_amount ON sales (amount);EXPLAIN QUERY PLANSELECT * FROM sales WHERE amount > 1000 / 1.15;
✓ أُنشئ الفهرس
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SEARCH sales USING INDEX idx_sales_amount (amount>?)

الشرطان متكافئان رياضيًّا، لكن الثاني يترك العمود عاريًا، فيستخدم الفهرس.

٤القاعدة 3: انتبه لـ LIKE التي تبدأ بـ %

الفهرس مرتب من أول الكلمة، مثل القاموس. فالبحث عن كلمات تبدأ بـ «جد» ممكن بالفهرس، أما كلمات تنتهي بـ «دة» فلا بد من قراءة كل شيء:

query.sql
CREATE INDEX idx_sales_city ON sales (city);PRAGMA case_sensitive_like = ON;   -- يلزم SQLite فقط EXPLAIN QUERY PLAN SELECT COUNT(*) FROM sales WHERE city LIKE 'جد%';EXPLAIN QUERY PLAN SELECT COUNT(*) FROM sales WHERE city LIKE '%دة';
✓ أُنشئ الفهرس
✓ تم
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SEARCH sales USING COVERING INDEX idx_sales_city (city>? AND city<?)
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SCAN sales USING COVERING INDEX idx_sales_city

في MySQL يستخدم الفهرس مع LIKE 'جد%' مباشرة. أما البحث في وسط النص على جداول ضخمة فله أدوات أخرى، مثل فهرس النص الكامل FULLTEXT.

٥القاعدة 4: اطلب ما تحتاجه فقط

query.sql
CREATE INDEX idx_sales_cust_amount ON sales (customer_id, amount); EXPLAIN QUERY PLAN SELECT * FROM sales WHERE customer_id = 777;EXPLAIN QUERY PLAN SELECT amount FROM sales WHERE customer_id = 777;
✓ أُنشئ الفهرس
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SEARCH sales USING INDEX idx_sales_cust_amount (customer_id=?)
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SEARCH sales USING COVERING INDEX idx_sales_cust_amount (customer_id=?)

٦القاعدة 5: للسؤال «هل يوجد؟» استخدم EXISTS

«هل اشترى العميل رقم 15000 شيئًا؟» لا نحتاج عدد مشترياته؛ يكفي أن نجد أول صف. COUNT(*) يكمل العدّ إلى آخر الجدول، و EXISTS يتوقف عند أول تطابق:

query.sql
-- بلا فهرس: قارن زمن الاستعلامينSELECT COUNT(*) > 0 AS bought FROM sales WHERE customer_id = 15000; SELECT EXISTS (SELECT 1 FROM sales WHERE customer_id = 15000) AS bought;
النتيجة · صف واحد
bought
1
النتيجة · صف واحد
bought
1

النتيجة 1 (نعم) في الحالتين. شغّل عدة مرات وقارن زمن كل استعلام بجانب نتيجته.

٧القاعدة 6: صفحات النتائج بالفهرس

عرض المبيعات صفحةً صفحة مرتبة بالأحدث. حين يكون عمود الترتيب مفهرسًا يقرأ النظام الصفحة مباشرة من الفهرس بلا ترتيب إضافي:

query.sql
CREATE INDEX idx_sales_date ON sales (sale_date); EXPLAIN QUERY PLANSELECT id, sale_date, amount FROM salesORDER BY sale_date DESCLIMIT 20 OFFSET 40;     -- الصفحة الثالثة
خطة التنفيذ (EXPLAIN QUERY PLAN)
  • SCAN sales USING INDEX idx_sales_date

لا يوجد TEMP B-TREE: الفهرس مرتب بالتاريخ أصلًا، ومع LIMIT يتوقف النظام بعد 60 صفًّا فقط، فلا يمر على الفهرس كله فعلًا.

٨القاعدة 7: لا تسأل القاعدة داخل حلقة

خطأ شائع في التطبيقات: جلب قائمة الطلبات، ثم إرسال استعلام مستقل لكل طلب لجلب اسم عميله. 100 طلب = 101 استعلام، وكل استعلام رحلة ذهاب وعودة إلى الخادم. تسمى هذه مشكلة N+1. والحل استعلام واحد بـ JOIN:

✗ خطأ
query.sql
-- في التطبيق: حلقة على كل طلبSELECT customer_id FROM orders;SELECT name FROM customers WHERE id = 1;SELECT name FROM customers WHERE id = 2;SELECT name FROM customers WHERE id = 1;-- … وهكذا لكل طلب
✓ صواب
query.sql
SELECT o.id, c.nameFROM orders oJOIN customers c ON c.id = o.customer_id;

٩ملخص القواعد

القاعدةبطيءسريع
1. العمود عارٍ من الدوالWHERE substr(d,1,7) = '2025-03'WHERE d >= '2025-03-01' AND d < '2025-04-01'
2. الحساب في الطرف الآخرWHERE amount * 1.15 > 1000WHERE amount > 1000 / 1.15
3. LIKE من أول النصLIKE '%دة'LIKE 'جد%'
4. ما تحتاجه فقطSELECT *SELECT amount … LIMIT 20
5. الوجود بـ EXISTSCOUNT(*) > 0EXISTS (SELECT 1 …)
6. الترتيب بعمود مفهرسORDER BY عمود بلا فهرسORDER BY عمود مفهرس
7. استعلام واحد لا حلقةاستعلام لكل صفJOIN

توقّع وتحقّق

أي شرط يستخدم الفهرس على sale_date؟

العمود عارٍ.

للتحقق من وجود صف واحد على الأقل:

يتوقف عند أول تطابق.

LIKE الذي لا يستفيد من الفهرس:

يبدأ بـ %.

مشكلة N+1:

الحل JOIN.

الخلاصة

تحدٍّ برمجي: أعد صياغة الاستعلام البطيء

متقدم

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

المطلوب:
  • أبقِ سطر إنشاء الفهرس كما هو.
  • لا تستخدم substr ولا strftime على sale_date.
  • اكتب الشرط نطاقًا: من أول 2024 إلى ما قبل أول 2025.
  • النتيجة: المدينة وعدد مبيعاتها، مرتبة بالمدينة.
👀 اعرض النتيجة المطلوبة
النتيجة المطلوبة
citysales_2024
أبها14326
الدمام14326
الرياض14326
المدينة المنورة14326
تبوك14326
جدة14326
مكة المكرمة14327
✓ التحقق الآلي من حلّك:
  • الكود يعمل بلا أخطاء
  • النتيجة نفسها: عدد مبيعات 2024 لكل مدينة
  • لا توجد دالة على sale_date
  • الشرط نطاق على العمود العاري

💡 تلميح 1
WHERE sale_date >= '2024-01-01' AND sale_date < '2025-01-01'
💡 تلميح 2
تأكد بـ EXPLAIN QUERY PLAN أن الخطة صارت SEARCH.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

solution.sql
CREATE INDEX idx_sales_date ON sales (sale_date); SELECT city, COUNT(*) AS sales_2024FROM salesWHERE sale_date >= '2024-01-01' AND sale_date < '2025-01-01'GROUP BY cityORDER BY city;
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس