قياس الأداء وأخطاؤه الشائعة
قِس قبل أن تحسّن، واعرف الأخطاء التي تجعل الفهرس بلا فائدة. ⏱ نحو 40 دقيقة.
- تقيس الاستعلام بـ
EXPLAIN ANALYZEوتقرأ الزمن والصفوف الفعلية. - تقارن المتوقع بالفعلي لتكتشف الإحصاءات القديمة.
- تتعرف على الأخطاء التي تمنع استخدام الفهرس.
- تعيد كتابة الشرط ليستفيد من الفهرس دون تغيير النتيجة.
١EXPLAIN ANALYZE: نفّذ وقِس
EXPLAIN وحده يُريك التقدير. أما EXPLAIN ANALYZE فينفّذ الاستعلام فعلًا ويضيف إلى كل عقدة الزمن الحقيقي وعدد الصفوف الحقيقي:
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42;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: 19994Buffers: shared hit=123Planning:Buffers: shared hit=30Planning Time: 0.211 msExecution Time: 2.377 ms
الأرقام الجديدة: actual time بالملّي ثانية، وrows الفعلية، وRows Removed by Filter (صفوف قُرئت ثم رُميت: هدر)، وفي الأسفل Execution Time الكلي. الأزمنة تختلف قليلًا في كل تشغيل.
ونقارن المتوقع rows=7 بـالفعلي: إن اختلفا كثيرًا (مثل 10 مقابل 10 آلاف) فالإحصاءات قديمة، والمخطِّط يختار خطة سيئة. الحل ANALYZE اسم_الجدول; (الدرس 14).
٢خطأ 1: دالة على عمود الشرط
عندنا فهرس على تاريخ الطلب. لكن هذا الاستعلام لا يستخدمه:
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;
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.
الحل: اترك العمود عاريًا واجعل الشرط مدى من القيم:
CREATE INDEX ix_order_date ON orders (order_date); EXPLAINSELECT count(*) FROM ordersWHERE order_date >= '2025-09-01' AND order_date < '2025-10-01';
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) | استعلام واحد على المجموعة كلها |
EXPLAINSELECT * FROM customers WHERE email LIKE '%42@mail.sa';
Seq Scan on customers (cost=0.00..66.50 rows=30 width=44)Filter: (email ~~ '%42@mail.sa'::text)
CREATE INDEX ix_email_lower ON customers (lower(email));EXPLAINSELECT id, name FROM customers WHERE lower(email) = 'c42@mail.sa';
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؟
الخلاصة
EXPLAINيقدّر، وEXPLAIN ANALYZEينفّذ ويقيس.- فرق كبير بين
rowsالمتوقعة والفعلية = إحصاءات قديمة. - لا تلفّ عمود الشرط بدالة؛ اجعل الشرط مدى على العمود العاري.
LIKEببداية مجهولة لا تستفيد من الفهرس العادي.- اذكر الأعمدة المطلوبة بدل
SELECT*.
تحدٍّ برمجي: تقرير مبيعات سبتمبر
متوسطهذا الاستعلام يحسب طلبات سبتمبر 2025 حسب الحالة، لكنه يقرأ الجدول كله رغم وجود فهرس على التاريخ. أعد كتابته ليستفيد من الفهرس، دون تغيير النتيجة.
- النتيجة نفسها: عدد الطلبات لكل حالة في سبتمبر 2025.
- بلا دوال على العمود
order_date(لا extract ولا date_part ولا to_char). - الشرط مدى:
order_date >= … AND order_date < …
👀 اعرض النتيجة المطلوبة
| status | n |
|---|---|
| cancelled | 223 |
| new | 268 |
| paid | 229 |
| shipped | 223 |
- الكود يعمل بلا أخطاء
- النتيجة نفسها (العدد لكل حالة)
- لا دوال على order_date
- شرط مدى على order_date
- الشرط بالمدى يستخدم الفهرس
💡 تلميح 1
'2025-09-01'، وأول يوم بعده: '2025-10-01'.💡 تلميح 2
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
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;