طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 122 · برمجة قواعد البيانات
الدرس 6 · الوحدة 3: تحسين الأداء وتنفيذ استعلامات محسّنة

قياس الأداء وأخطاؤه الشائعة

قِس قبل أن تحسّن، واعرف الأخطاء التي تجعل الفهرس بلا فائدة. ⏱ نحو 40 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تقيس الاستعلام بـ EXPLAIN ANALYZE وتقرأ الزمن والصفوف الفعلية.
  • تقارن المتوقع بالفعلي لتكتشف الإحصاءات القديمة.
  • تتعرف على الأخطاء التي تمنع استخدام الفهرس.
  • تعيد كتابة الشرط ليستفيد من الفهرس دون تغيير النتيجة.

١EXPLAIN ANALYZE: نفّذ وقِس

EXPLAIN وحده يُريك التقدير. أما EXPLAIN ANALYZE فينفّذ الاستعلام فعلًا ويضيف إلى كل عقدة الزمن الحقيقي وعدد الصفوف الحقيقي:

query.sql
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42;
خطة التنفيذ (EXPLAIN)
  • Seq Scan on orders (cost=0.00..373.00 rows=7 width=18) (actual time=0.393..2.203 rows=6.00 loops=1)
  • Filter: (customer_id = 42)
  • Rows Removed by Filter: 19994
  • Buffers: shared hit=123
  • Planning:
  • Buffers: shared hit=30
  • Planning Time: 0.211 ms
  • Execution Time: 2.377 ms

الأرقام الجديدة: actual time بالملّي ثانية، وrows الفعلية، وRows Removed by Filter (صفوف قُرئت ثم رُميت: هدر)، وفي الأسفل Execution Time الكلي. الأزمنة تختلف قليلًا في كل تشغيل.

ونقارن المتوقع rows=7 بـالفعلي: إن اختلفا كثيرًا (مثل 10 مقابل 10 آلاف) فالإحصاءات قديمة، والمخطِّط يختار خطة سيئة. الحل ANALYZE اسم_الجدول; (الدرس 14).

٢خطأ 1: دالة على عمود الشرط

عندنا فهرس على تاريخ الطلب. لكن هذا الاستعلام لا يستخدمه:

query.sql
CREATE INDEX ix_order_date ON orders (order_date); EXPLAINSELECT count(*) FROM ordersWHERE extract(month FROM order_date) = 9 AND extract(year FROM order_date) = 2025;
✓ أُنشئ الفهرس
خطة التنفيذ (EXPLAIN)
  • Aggregate (cost=523.00..523.01 rows=1 width=8)
  • -> Seq Scan on orders (cost=0.00..523.00 rows=1 width=0)
  • Filter: ((EXTRACT(month FROM order_date) = '9'::numeric) AND (EXTRACT(year FROM order_date) = '2025'::numeric))

الفهرس مرتب على قيم التاريخ، لا على «رقم الشهر». حين نلفّ العمود بدالة يضطر المحرك لحسابها لكل صف: Seq Scan.

الحل: اترك العمود عاريًا واجعل الشرط مدى من القيم:

query.sql
CREATE INDEX ix_order_date ON orders (order_date); EXPLAINSELECT count(*) FROM ordersWHERE order_date >= '2025-09-01' AND order_date < '2025-10-01';
✓ أُنشئ الفهرس
خطة التنفيذ (EXPLAIN)
  • Aggregate (cost=153.65..153.66 rows=1 width=8)
  • -> Bitmap Heap Scan on orders (cost=14.03..151.28 rows=950 width=0)
  • Recheck Cond: ((order_date >= '2025-09-01'::date) AND (order_date < '2025-10-01'::date))
  • -> Bitmap Index Scan on ix_order_date (cost=0.00..13.79 rows=950 width=0)
  • Index Cond: ((order_date >= '2025-09-01'::date) AND (order_date < '2025-10-01'::date))

الآن Bitmap Index Scan on ix_order_date. والنتيجة نفسها تمامًا. لاحظ < '2025-10-01' لا <= '2025-09-30': أدق مع أنواع الوقت.

٣أخطاء أخرى شائعة

الخطألماذا يبطئ؟البديل
WHERE email LIKE '%42@mail.sa'البداية مجهولة؛ الفهرس مرتب من أول حرف فلا يفيدابحث ببداية معروفة، أو فهرس نصي خاص
WHERE lower(email) = …دالة على العمودفهرس على التعبير نفسه: CREATE INDEX … (lower(email))
SELECT *ينقل أعمدة لا تحتاجها، ويمنع Index Only Scanاذكر الأعمدة المطلوبة فقط
WHERE id::text = '42'تحويل نوع العمودحوّل القيمة لا العمود: WHERE id = 42
استعلام داخل حلقةرحلة لكل صف (الدرس 22)استعلام واحد على المجموعة كلها
✗ خطأ
query.sql
EXPLAINSELECT * FROM customers WHERE email LIKE '%42@mail.sa';
خطة التنفيذ (EXPLAIN)
  • Seq Scan on customers (cost=0.00..66.50 rows=30 width=44)
  • Filter: (email ~~ '%42@mail.sa'::text)
✓ صواب
query.sql
CREATE INDEX ix_email_lower ON customers (lower(email));EXPLAINSELECT id, name FROM customers WHERE lower(email) = 'c42@mail.sa';
✓ أُنشئ الفهرس
خطة التنفيذ (EXPLAIN)
  • Bitmap Heap Scan on customers (cost=4.40..29.46 rows=15 width=17)
  • Recheck Cond: (lower(email) = 'c42@mail.sa'::text)
  • -> Bitmap Index Scan on ix_email_lower (cost=0.00..4.39 rows=15 width=0)
  • Index Cond: (lower(email) = 'c42@mail.sa'::text)

الفهرس على تعبير lower(email) يستخدمه أي استعلام يكتب التعبير نفسه حرفيًّا.

توقّع وتحقّق

الفرق بين EXPLAIN و EXPLAIN ANALYZE:

لماذا لا يُستخدم الفهرس مع WHERE extract(year FROM d) = 2025؟

Rows Removed by Filter الكبير يدل على:

أي شرط يستفيد من فهرس على email؟

الخلاصة

تحدٍّ برمجي: تقرير مبيعات سبتمبر

متوسط

هذا الاستعلام يحسب طلبات سبتمبر 2025 حسب الحالة، لكنه يقرأ الجدول كله رغم وجود فهرس على التاريخ. أعد كتابته ليستفيد من الفهرس، دون تغيير النتيجة.

المطلوب:
  • النتيجة نفسها: عدد الطلبات لكل حالة في سبتمبر 2025.
  • بلا دوال على العمود order_date (لا extract ولا date_part ولا to_char).
  • الشرط مدى: order_date >= … AND order_date < …
👀 اعرض النتيجة المطلوبة
النتيجة المطلوبة
statusn
cancelled223
new268
paid229
shipped223
✓ التحقق الآلي من حلّك:
  • الكود يعمل بلا أخطاء
  • النتيجة نفسها (العدد لكل حالة)
  • لا دوال على order_date
  • شرط مدى على order_date
  • الشرط بالمدى يستخدم الفهرس

💡 تلميح 1
أول يوم في سبتمبر: '2025-09-01'، وأول يوم بعده: '2025-10-01'.
💡 تلميح 2
جرّب EXPLAIN قبل الاستعلام لترى الفرق.

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

solution.sql
CREATE INDEX ix_order_date ON orders (order_date); SELECT status, count(*) AS nFROM ordersWHERE order_date >= '2025-09-01' AND order_date < '2025-10-01'GROUP BY status ORDER BY status;
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

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