التفكير بالمجموعات بدل الحلقات
أهم عادة في تحسين الأداء: صف النتيجة كلها بأمر واحد بدل حلقة. ⏱ نحو 40 دقيقة.
- تشرح لماذا المعالجة صفًّا بصف أبطأ من أمر المجموعة.
- تحوّل حلقة إلى
UPDATE…FROMأوINSERT…SELECT. - تستبدل
IFداخل الحلقة بـCASE. - تميز الحالات التي تبقى فيها الحلقة مناسبة.
١صف بصف: بطء تراه بعينك
المبرمج القادم من لغات مثل Python يميل إلى الحلقة: «لكل عميل، احسب مشترياته، ثم حدّثه». هذا يعمل، لكنه ينفّذ آلاف الاستعلامات الصغيرة. قاعدة البيانات مصممة لتعالج المجموعة كلها في أمر واحد. قارن الزمنين على 3000 عميل:
CREATE INDEX ON orders (customer_id);ALTER TABLE customers ADD COLUMN spent numeric; -- الطريقة الإجرائية: حلقة على 3000 عميلDO $$DECLARE r record;BEGIN FOR r IN SELECT id FROM customers LOOP UPDATE customers SET spent = ( SELECT coalesce(sum(i.qty * i.unit_price), 0) FROM orders o JOIN order_items i ON i.order_id = o.id WHERE o.customer_id = r.id) WHERE id = r.id; END LOOP;END $$;
3000 تكرار، وفي كل تكرار استعلام تجميع وأمر تعديل.
CREATE INDEX ON orders (customer_id);ALTER TABLE customers ADD COLUMN spent numeric; -- طريقة المجموعات: أمر واحدUPDATE customers cSET spent = coalesce(t.total, 0)FROM customers c2LEFT JOIN ( SELECT o.customer_id, sum(i.qty * i.unit_price) AS total FROM orders o JOIN order_items i ON i.order_id = o.id GROUP BY o.customer_id) t ON t.customer_id = c2.idWHERE c2.id = c.id;
أمر واحد يجمّع كل الطلبات مرة واحدة ثم يحدّث كل العملاء. قارن ⏱. وكلما كبر الجدول أو غاب الفهرس اتسع الفرق: بلا فهرس على customer_id تصبح الحلقة أبطأ بعشرات المرات.
٢أدوات التفكير بالمجموعات
| بدل… | اكتب… |
|---|---|
| حلقة فيها UPDATE لكل صف | UPDATE … FROM (استعلام تجميع) |
| حلقة فيها INSERT لكل صف | INSERT INTO t SELECT … |
| IF داخل الحلقة لاختيار قيمة | CASE WHEN … THEN … END داخل الأمر |
| حلقة تحذف صفوفًا تحقق شرطًا | DELETE … WHERE … أو WHERE EXISTS (…) |
| حلقة تبحث عن أعلى قيمة في كل مجموعة | دوال النوافذ (الدرس 25) |
DO $$DECLARE r record;BEGIN FOR r IN SELECT id, stock FROM products LOOP IF r.stock = 0 THEN UPDATE products SET price = price * 1.05 WHERE id = r.id; ELSIF r.stock > 100 THEN UPDATE products SET price = price * 0.90 WHERE id = r.id; END IF; END LOOP;END $$;SELECT name, stock, price FROM products WHERE stock = 0 OR stock > 100 ORDER BY id;
| name | stock | price |
|---|---|---|
| سماعة رأس | 0 | 220.5 |
| دفتر ملاحظات | 200 | 10.8 |
| أقلام ملونة | 150 | 16.2 |
UPDATE productsSET price = price * CASE WHEN stock = 0 THEN 1.05 ELSE 0.90 ENDWHERE stock = 0 OR stock > 100; SELECT name, stock, price FROM products WHERE stock = 0 OR stock > 100 ORDER BY id;
| name | stock | price |
|---|---|---|
| سماعة رأس | 0 | 220.5 |
| دفتر ملاحظات | 200 | 10.8 |
| أقلام ملونة | 150 | 16.2 |
النتيجة نفسها، والثاني أمر واحد أقصر وأوضح وأسرع.
٣متى تبقى الحلقة مناسبة؟
| الحالة | لماذا الحلقة؟ |
|---|---|
| معالجة على دفعات مع COMMIT | تقسيم ملايين الصفوف (الدرس 16) |
| كل صف يحتاج إجراءً خارجيًّا أو خطأً مستقلًّا | التقاط خطأ كل صف وحده (الدرس 18) |
| SQL ديناميكي لكل جدول | أسماء جداول مختلفة في كل تكرار (الدرس 23) |
| منطق معقد يعتمد على نتيجة الصف السابق | وإن كانت دوال النوافذ تحل كثيرًا منه |
توقّع وتحقّق
لماذا الحلقة صفًّا بصف بطيئة؟
بديل IF داخل الحلقة لاختيار القيمة:
لإدخال نتيجة استعلام في جدول دفعة واحدة:
مقابل UPDATE … FROM في Oracle:
الخلاصة
- أمر
SQLواحد على المجموعة أسرع وأوضح من آلاف الأوامر الصغيرة. UPDATE…FROMللتحديث من استعلام، وINSERT…SELECTللإدخال الجماعي.CASEبدلIFلاختيار القيمة لكل صف.- الحلقة للدفعات، والأخطاء المستقلة، و
SQLالديناميكي.
تحدٍّ برمجي: تصنيف العملاء بأمر واحد
متقدمأضف إلى customers عمود tier واملأه حسب مجموع مشتريات العميل (كل الطلبات): ذهبي gold من 3000 فأكثر، وفضي silver من 500 فأكثر، وإلا basic. بلا حلقات.
ALTER TABLE customers ADD COLUMN tier text;- أمر UPDATE واحد بـ CASE، والعملاء بلا طلبات basic.
- لا LOOP ولا FOR في الحل.
👀 اعرض الناتج المطلوب
- الكود يعمل بلا أخطاء
- تصنيف كل العملاء صحيح
- بلا حلقات
- CASE
💡 تلميح 1
coalesce(t.total, 0).💡 تلميح 2
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
ALTER TABLE customers ADD COLUMN tier text; UPDATE customers cSET tier = CASE WHEN coalesce(t.total, 0) >= 3000 THEN 'gold' WHEN coalesce(t.total, 0) >= 500 THEN 'silver' ELSE 'basic' ENDFROM customers c2LEFT JOIN ( SELECT o.customer_id, sum(i.qty * i.unit_price) AS total FROM orders o JOIN order_items i ON i.order_id = o.id GROUP BY o.customer_id) t ON t.customer_id = c2.idWHERE c2.id = c.id;