قواعد كتابة استعلام سريع
الصياغة نفسها تحدد إن كان الفهرس سيُستخدم أم لا. ⏱ نحو 30 دقيقة.
- تكتب شروطًا تترك العمود المفهرس عاريًا من الدوال والحسابات.
- تميّز
LIKEالتي تستفيد من الفهرس والتي لا تستفيد. - تستخدم
EXISTSوLIMITواختيار الأعمدة لتقليل العمل. - تتجنب مشكلة
N+1بـJOIN، وتتحقق أن التحسين لم يغيّر النتيجة.
١الفهرس وحده لا يكفي
قد يكون الفهرس موجودًا، ويرفض المحسّن استخدامه بسبب طريقة كتابة الاستعلام. القاعدة العامة: يستخدم النظام الفهرس حين يكون العمود في الشرط عاريًا، أي كما هو، دون دالة أو عملية حسابية عليه. في هذا الدرس سبع قواعد، ولكل قاعدة تجربة تقيسها بنفسك على قاعدة «المبيعات الكبيرة».
٢القاعدة 1: لا تلفّ العمود المفهرس بدالة
«مبيعات مارس 2025». الصياغة الأولى تبدو طبيعية، لكنها تحسب substr لكل صف من 200 ألف، فلا تستطيع القفز في الفهرس:
CREATE INDEX idx_sales_date ON sales (sale_date); EXPLAIN QUERY PLANSELECT COUNT(*) FROM salesWHERE substr(sale_date, 1, 7) = '2025-03';
SCAN sales USING COVERING INDEX idx_sales_date
والصياغة الصحيحة نطاق على العمود العاري؛ النتيجة نفسها، والخطة بحث في الفهرس:
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';
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%»:
CREATE INDEX idx_sales_amount ON sales (amount);EXPLAIN QUERY PLANSELECT * FROM sales WHERE amount * 1.15 > 1000;
SCAN sales
CREATE INDEX idx_sales_amount ON sales (amount);EXPLAIN QUERY PLANSELECT * FROM sales WHERE amount > 1000 / 1.15;
SEARCH sales USING INDEX idx_sales_amount (amount>?)
الشرطان متكافئان رياضيًّا، لكن الثاني يترك العمود عاريًا، فيستخدم الفهرس.
٤القاعدة 3: انتبه لـ LIKE التي تبدأ بـ %
الفهرس مرتب من أول الكلمة، مثل القاموس. فالبحث عن كلمات تبدأ بـ «جد» ممكن بالفهرس، أما كلمات تنتهي بـ «دة» فلا بد من قراءة كل شيء:
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 '%دة';
SEARCH sales USING COVERING INDEX idx_sales_city (city>? AND city<?)
SCAN sales USING COVERING INDEX idx_sales_city
في MySQL يستخدم الفهرس مع LIKE 'جد%' مباشرة. أما البحث في وسط النص على جداول ضخمة فله أدوات أخرى، مثل فهرس النص الكامل FULLTEXT.
٥القاعدة 4: اطلب ما تحتاجه فقط
- الأعمدة:
SELECT *يجلب كل الأعمدة، ويمنع الفهرس المغطّي، ويرسل بيانات لا تحتاجها عبر الشبكة. - الصفوف: صفحة من 20 نتيجة بـ
LIMIT 20أسرع من جلب 200 ألف صف ثم عرض 20 منها في التطبيق.
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;
SEARCH sales USING INDEX idx_sales_cust_amount (customer_id=?)
SEARCH sales USING COVERING INDEX idx_sales_cust_amount (customer_id=?)
٦القاعدة 5: للسؤال «هل يوجد؟» استخدم EXISTS
«هل اشترى العميل رقم 15000 شيئًا؟» لا نحتاج عدد مشترياته؛ يكفي أن نجد أول صف. COUNT(*) يكمل العدّ إلى آخر الجدول، و EXISTS يتوقف عند أول تطابق:
-- بلا فهرس: قارن زمن الاستعلامين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: صفحات النتائج بالفهرس
عرض المبيعات صفحةً صفحة مرتبة بالأحدث. حين يكون عمود الترتيب مفهرسًا يقرأ النظام الصفحة مباشرة من الفهرس بلا ترتيب إضافي:
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; -- الصفحة الثالثة
SCAN sales USING INDEX idx_sales_date
لا يوجد TEMP B-TREE: الفهرس مرتب بالتاريخ أصلًا، ومع LIMIT يتوقف النظام بعد 60 صفًّا فقط، فلا يمر على الفهرس كله فعلًا.
٨القاعدة 7: لا تسأل القاعدة داخل حلقة
خطأ شائع في التطبيقات: جلب قائمة الطلبات، ثم إرسال استعلام مستقل لكل طلب لجلب اسم عميله. 100 طلب = 101 استعلام، وكل استعلام رحلة ذهاب وعودة إلى الخادم. تسمى هذه مشكلة N+1. والحل استعلام واحد بـ JOIN:
-- في التطبيق: حلقة على كل طلب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;-- … وهكذا لكل طلب
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 > 1000 | WHERE amount > 1000 / 1.15 |
| 3. LIKE من أول النص | LIKE '%دة' | LIKE 'جد%' |
| 4. ما تحتاجه فقط | SELECT * | SELECT amount … LIMIT 20 |
| 5. الوجود بـ EXISTS | COUNT(*) > 0 | EXISTS (SELECT 1 …) |
| 6. الترتيب بعمود مفهرس | ORDER BY عمود بلا فهرس | ORDER BY عمود مفهرس |
| 7. استعلام واحد لا حلقة | استعلام لكل صف | JOIN |
توقّع وتحقّق
أي شرط يستخدم الفهرس على sale_date؟
للتحقق من وجود صف واحد على الأقل:
LIKE الذي لا يستفيد من الفهرس:
مشكلة N+1:
JOIN.الخلاصة
- العمود في الشرط عارٍ: لا دالة ولا حساب عليه.
- التاريخ نطاق:
>=أول الفترة و < أول الفترة التالية. LIKE 'نص%'يمكن أن تستفيد من الفهرس، وLIKE '%نص'لا.- اطلب الأعمدة والصفوف التي تحتاجها، و
EXISTSللوجود. - استعلام واحد بـ
JOINبدل استعلام لكل صف.
تحدٍّ برمجي: أعد صياغة الاستعلام البطيء
متقدمهذا الاستعلام يحسب مبيعات عام 2024 لكل مدينة، لكنه لا يستفيد من فهرس التاريخ. أعد كتابته ليستخدم الفهرس، بالنتيجة نفسها تمامًا.
- أبقِ سطر إنشاء الفهرس كما هو.
- لا تستخدم
substrولاstrftimeعلىsale_date. - اكتب الشرط نطاقًا: من أول 2024 إلى ما قبل أول 2025.
- النتيجة: المدينة وعدد مبيعاتها، مرتبة بالمدينة.
👀 اعرض النتيجة المطلوبة
| city | sales_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
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
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;