الدرس 35 المنطق الشرطي

تعبيرات CASE الشرطية

تعبير CASE هو أداة قوية في SQL تُمكنك من إضافة منطق شرطي (if-then-else) داخل استعلاماتك. يمكن استخدامه في SELECT، WHERE، ORDER BY، وحتى في عمليات UPDATE و INSERT.

⏱️ مدة القراءة: 35 دقيقة
📊 المستوى: متوسط
🎯 الوحدة: المنطق الشرطي

مقدمة في CASE

CASE يعمل مثل if-else في لغات البرمجة الأخرى. يُقيّم الشروط بالترتيب ويُرجع القيمة المناسبة.

📋 نوعان من CASE
  • Searched CASE: يقيّم شروط منطقية متعددة
  • Simple CASE: يقارن قيمة واحدة مع عدة احتمالات

Searched CASE Expression

الصيغة الأكثر مرونة - تسمح بتقييم شروط منطقية مختلفة.

SQL - الصيغة الأساسية
CASE
    WHEN condition1 THEN result1
    WHEN condition2 THEN result2
    WHEN condition3 THEN result3
    ELSE default_result
END

أمثلة عملية

SQL - تصنيف العملاء
-- تصنيف العملاء حسب إجمالي المشتريات
SELECT
    customer_name,
    total_purchases,
    CASE
        WHEN total_purchases >= 100000 THEN 'بلاتيني'
        WHEN total_purchases >= 50000 THEN 'ذهبي'
        WHEN total_purchases >= 10000 THEN 'فضي'
        WHEN total_purchases >= 1000 THEN 'برونزي'
        ELSE 'عادي'
    END AS customer_tier
FROM customers;

-- النتيجة:
-- customer_name | total_purchases | customer_tier
-- أحمد محمد     | 150000          | بلاتيني
-- سارة علي      | 45000           | فضي
-- خالد عبدالله  | 500             | عادي
SQL - تحديد حالة الطلب
SELECT
    order_id,
    order_date,
    ship_date,
    CASE
        WHEN ship_date IS NULL AND DATEDIFF(CURDATE(), order_date) > 7
            THEN 'متأخر - يحتاج متابعة'
        WHEN ship_date IS NULL
            THEN 'قيد المعالجة'
        WHEN DATEDIFF(ship_date, order_date) <= 2
            THEN 'تم الشحن بسرعة'
        WHEN DATEDIFF(ship_date, order_date) <= 5
            THEN 'تم الشحن في الوقت'
        ELSE 'تم الشحن متأخراً'
    END AS shipping_status
FROM orders;

Simple CASE Expression

مقارنة قيمة واحدة مع عدة احتمالات - أكثر إيجازاً للمقارنات البسيطة.

SQL - الصيغة
CASE expression
    WHEN value1 THEN result1
    WHEN value2 THEN result2
    WHEN value3 THEN result3
    ELSE default_result
END
SQL - ترجمة الحالات
-- ترجمة رمز الحالة
SELECT
    order_id,
    status,
    CASE status
        WHEN 'P' THEN 'قيد الانتظار'
        WHEN 'A' THEN 'تمت الموافقة'
        WHEN 'S' THEN 'تم الشحن'
        WHEN 'D' THEN 'تم التوصيل'
        WHEN 'C' THEN 'ملغي'
        WHEN 'R' THEN 'مرتجع'
        ELSE 'غير معروف'
    END AS status_arabic
FROM orders;

-- ترجمة أيام الأسبوع
SELECT
    order_date,
    DAYOFWEEK(order_date) AS day_num,
    CASE DAYOFWEEK(order_date)
        WHEN 1 THEN 'الأحد'
        WHEN 2 THEN 'الاثنين'
        WHEN 3 THEN 'الثلاثاء'
        WHEN 4 THEN 'الأربعاء'
        WHEN 5 THEN 'الخميس'
        WHEN 6 THEN 'الجمعة'
        WHEN 7 THEN 'السبت'
    END AS day_arabic
FROM orders;

CASE في أجزاء مختلفة من الاستعلام

في SELECT

SQL - إنشاء أعمدة محسوبة
SELECT
    product_name,
    price,
    stock_quantity,
    CASE
        WHEN stock_quantity = 0 THEN 'نفد المخزون'
        WHEN stock_quantity < 10 THEN 'مخزون منخفض'
        WHEN stock_quantity < 50 THEN 'مخزون متوسط'
        ELSE 'مخزون كافي'
    END AS stock_status,
    CASE
        WHEN stock_quantity = 0 THEN 'danger'
        WHEN stock_quantity < 10 THEN 'warning'
        ELSE 'success'
    END AS badge_class
FROM products;

في WHERE

SQL - فلترة ديناميكية
-- فلترة حسب متغير
SET @filter_type = 'expensive';

SELECT * FROM products
WHERE
    CASE @filter_type
        WHEN 'expensive' THEN price > 1000
        WHEN 'cheap' THEN price < 100
        WHEN 'in_stock' THEN stock_quantity > 0
        ELSE TRUE
    END;

-- فلترة معقدة
SELECT * FROM orders
WHERE
    CASE
        WHEN @user_role = 'admin' THEN TRUE
        WHEN @user_role = 'manager' THEN department_id = @user_dept
        ELSE customer_id = @user_id
    END;

في ORDER BY

SQL - ترتيب مخصص
-- ترتيب حسب الأولوية
SELECT * FROM tickets
ORDER BY
    CASE priority
        WHEN 'urgent' THEN 1
        WHEN 'high' THEN 2
        WHEN 'medium' THEN 3
        WHEN 'low' THEN 4
        ELSE 5
    END,
    created_at;

-- ترتيب مع NULL في النهاية
SELECT * FROM products
ORDER BY
    CASE WHEN price IS NULL THEN 1 ELSE 0 END,
    price;

-- ترتيب بناءً على شرط
SELECT * FROM customers
ORDER BY
    CASE
        WHEN is_vip = 1 THEN 0  -- VIP أولاً
        ELSE 1
    END,
    customer_name;

في GROUP BY

SQL - تجميع مخصص
-- تجميع حسب فئات السعر
SELECT
    CASE
        WHEN price < 100 THEN 'رخيص'
        WHEN price < 500 THEN 'متوسط'
        WHEN price < 1000 THEN 'مرتفع'
        ELSE 'فاخر'
    END AS price_category,
    COUNT(*) AS product_count,
    AVG(price) AS avg_price
FROM products
GROUP BY
    CASE
        WHEN price < 100 THEN 'رخيص'
        WHEN price < 500 THEN 'متوسط'
        WHEN price < 1000 THEN 'مرتفع'
        ELSE 'فاخر'
    END;

CASE مع دوال التجميع

من أقوى استخدامات CASE - تحويل الصفوف لأعمدة (Pivot).

SQL - العد الشرطي
-- عدد الطلبات حسب الحالة
SELECT
    COUNT(*) AS total_orders,
    COUNT(CASE WHEN status = 'pending' THEN 1 END) AS pending_count,
    COUNT(CASE WHEN status = 'shipped' THEN 1 END) AS shipped_count,
    COUNT(CASE WHEN status = 'delivered' THEN 1 END) AS delivered_count,
    COUNT(CASE WHEN status = 'cancelled' THEN 1 END) AS cancelled_count
FROM orders;

-- أو باستخدام SUM
SELECT
    SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending,
    SUM(CASE WHEN status = 'shipped' THEN 1 ELSE 0 END) AS shipped,
    SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) AS delivered
FROM orders;

Pivot Table - تحويل الصفوف لأعمدة

SQL - تقرير مبيعات شهرية
-- مبيعات كل فئة لكل شهر
SELECT
    category,
    SUM(CASE WHEN MONTH(sale_date) = 1 THEN amount ELSE 0 END) AS jan,
    SUM(CASE WHEN MONTH(sale_date) = 2 THEN amount ELSE 0 END) AS feb,
    SUM(CASE WHEN MONTH(sale_date) = 3 THEN amount ELSE 0 END) AS mar,
    SUM(CASE WHEN MONTH(sale_date) = 4 THEN amount ELSE 0 END) AS apr,
    SUM(CASE WHEN MONTH(sale_date) = 5 THEN amount ELSE 0 END) AS may,
    SUM(CASE WHEN MONTH(sale_date) = 6 THEN amount ELSE 0 END) AS jun,
    SUM(amount) AS total
FROM sales
WHERE YEAR(sale_date) = 2025
GROUP BY category;

-- النتيجة:
-- category   | jan    | feb    | mar    | ... | total
-- إلكترونيات | 50000  | 45000  | 60000  | ... | 350000
-- ملابس      | 30000  | 25000  | 35000  | ... | 200000

حساب النسب المئوية

SQL - نسب الحالات
SELECT
    COUNT(*) AS total,
    ROUND(
        100.0 * SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) / COUNT(*),
        1
    ) AS delivery_rate,
    ROUND(
        100.0 * SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) / COUNT(*),
        1
    ) AS cancellation_rate,
    ROUND(
        100.0 * SUM(CASE WHEN status = 'returned' THEN 1 ELSE 0 END) / COUNT(*),
        1
    ) AS return_rate
FROM orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY);

CASE في UPDATE و INSERT

SQL - تحديث شرطي
-- تحديث الأسعار حسب الفئة
UPDATE products
SET price = price * CASE category
    WHEN 'electronics' THEN 1.10  -- زيادة 10%
    WHEN 'clothing' THEN 1.05     -- زيادة 5%
    WHEN 'food' THEN 1.03         -- زيادة 3%
    ELSE 1.00                     -- بدون تغيير
END
WHERE active = 1;

-- تحديث حالة الطلب
UPDATE orders
SET status = CASE
    WHEN ship_date IS NOT NULL AND delivery_date IS NOT NULL THEN 'delivered'
    WHEN ship_date IS NOT NULL THEN 'shipped'
    WHEN payment_date IS NOT NULL THEN 'paid'
    ELSE 'pending'
END;

-- تحديث تصنيف العملاء
UPDATE customers
SET tier = CASE
    WHEN total_purchases >= 100000 THEN 'platinum'
    WHEN total_purchases >= 50000 THEN 'gold'
    WHEN total_purchases >= 10000 THEN 'silver'
    ELSE 'bronze'
END;

التعامل مع NULL

SQL - معالجة القيم الفارغة
-- COALESCE: أول قيمة غير فارغة
SELECT
    customer_name,
    COALESCE(phone, mobile, email, 'لا توجد معلومات اتصال') AS contact
FROM customers;

-- IFNULL (MySQL) / ISNULL (SQL Server)
SELECT
    product_name,
    IFNULL(description, 'لا يوجد وصف') AS description
FROM products;

-- NULLIF: يُرجع NULL إذا تساوت القيمتان
SELECT
    product_name,
    NULLIF(discount_price, 0) AS discount_price  -- يحول 0 إلى NULL
FROM products;

-- CASE مع NULL
SELECT
    customer_name,
    CASE
        WHEN last_order_date IS NULL THEN 'لم يطلب أبداً'
        WHEN DATEDIFF(CURDATE(), last_order_date) > 365 THEN 'عميل غير نشط'
        WHEN DATEDIFF(CURDATE(), last_order_date) > 90 THEN 'عميل خامل'
        ELSE 'عميل نشط'
    END AS activity_status
FROM customers;
⚠️ تنبيه مهم

في Simple CASE، المقارنة مع NULL لا تعمل كما هو متوقع:

-- ❌ لن يعمل كما هو متوقع
CASE column_name
    WHEN NULL THEN 'فارغ'  -- لن يتطابق أبداً!
    ELSE 'موجود'
END

-- ✅ استخدم Searched CASE بدلاً من ذلك
CASE
    WHEN column_name IS NULL THEN 'فارغ'
    ELSE 'موجود'
END

تجنب القسمة على صفر

SQL - حماية من القسمة على صفر
-- ❌ قد يسبب خطأ
SELECT total_sales / total_orders AS avg_order_value
FROM summary;

-- ✅ باستخدام CASE
SELECT
    CASE
        WHEN total_orders = 0 THEN 0
        ELSE total_sales / total_orders
    END AS avg_order_value
FROM summary;

-- ✅ باستخدام NULLIF
SELECT
    total_sales / NULLIF(total_orders, 0) AS avg_order_value
FROM summary;

-- حساب نسبة التحويل بأمان
SELECT
    campaign_name,
    clicks,
    conversions,
    CASE
        WHEN clicks = 0 THEN 0
        ELSE ROUND(100.0 * conversions / clicks, 2)
    END AS conversion_rate
FROM campaigns;

CASE متداخلة

SQL - تداخل CASE
-- منطق متداخل معقد
SELECT
    product_name,
    category,
    price,
    stock_quantity,
    CASE
        WHEN category = 'electronics' THEN
            CASE
                WHEN price > 5000 THEN 'إلكترونيات فاخرة'
                WHEN price > 1000 THEN 'إلكترونيات متوسطة'
                ELSE 'إلكترونيات اقتصادية'
            END
        WHEN category = 'clothing' THEN
            CASE
                WHEN price > 500 THEN 'ملابس فاخرة'
                ELSE 'ملابس عادية'
            END
        ELSE 'فئة أخرى'
    END AS detailed_category
FROM products;

-- بديل أكثر وضوحاً
SELECT
    product_name,
    CASE
        WHEN category = 'electronics' AND price > 5000 THEN 'إلكترونيات فاخرة'
        WHEN category = 'electronics' AND price > 1000 THEN 'إلكترونيات متوسطة'
        WHEN category = 'electronics' THEN 'إلكترونيات اقتصادية'
        WHEN category = 'clothing' AND price > 500 THEN 'ملابس فاخرة'
        WHEN category = 'clothing' THEN 'ملابس عادية'
        ELSE 'فئة أخرى'
    END AS detailed_category
FROM products;

أمثلة شاملة من العالم الحقيقي

1. لوحة معلومات KPI

SQL - مؤشرات الأداء
SELECT
    DATE_FORMAT(order_date, '%Y-%m') AS month,
    COUNT(*) AS total_orders,
    SUM(total_amount) AS revenue,
    AVG(total_amount) AS avg_order_value,

    -- معدل الإلغاء
    ROUND(100.0 * SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) / COUNT(*), 1)
        AS cancellation_rate,

    -- تصنيف الأداء
    CASE
        WHEN SUM(total_amount) >= 100000 THEN '🟢 ممتاز'
        WHEN SUM(total_amount) >= 50000 THEN '🟡 جيد'
        WHEN SUM(total_amount) >= 25000 THEN '🟠 مقبول'
        ELSE '🔴 يحتاج تحسين'
    END AS performance_rating
FROM orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY month;

2. نظام التسعير الديناميكي

SQL - حساب السعر النهائي
SELECT
    p.product_name,
    p.base_price,
    c.tier AS customer_tier,
    o.quantity,

    -- خصم الكمية
    CASE
        WHEN o.quantity >= 100 THEN 0.15
        WHEN o.quantity >= 50 THEN 0.10
        WHEN o.quantity >= 20 THEN 0.05
        ELSE 0
    END AS quantity_discount,

    -- خصم العميل
    CASE c.tier
        WHEN 'platinum' THEN 0.10
        WHEN 'gold' THEN 0.07
        WHEN 'silver' THEN 0.05
        ELSE 0
    END AS tier_discount,

    -- السعر النهائي
    ROUND(
        p.base_price * o.quantity *
        (1 - CASE WHEN o.quantity >= 100 THEN 0.15
                  WHEN o.quantity >= 50 THEN 0.10
                  WHEN o.quantity >= 20 THEN 0.05 ELSE 0 END) *
        (1 - CASE c.tier WHEN 'platinum' THEN 0.10
                         WHEN 'gold' THEN 0.07
                         WHEN 'silver' THEN 0.05 ELSE 0 END),
        2
    ) AS final_price
FROM order_items o
JOIN products p ON o.product_id = p.product_id
JOIN orders ord ON o.order_id = ord.order_id
JOIN customers c ON ord.customer_id = c.customer_id;

3. تقرير تحليل RFM

SQL - تحليل RFM للعملاء
WITH customer_rfm AS (
    SELECT
        customer_id,
        DATEDIFF(CURDATE(), MAX(order_date)) AS recency,
        COUNT(*) AS frequency,
        SUM(total_amount) AS monetary
    FROM orders
    GROUP BY customer_id
)
SELECT
    customer_id,
    recency,
    frequency,
    monetary,

    -- تصنيف Recency
    CASE
        WHEN recency <= 30 THEN 5
        WHEN recency <= 60 THEN 4
        WHEN recency <= 90 THEN 3
        WHEN recency <= 180 THEN 2
        ELSE 1
    END AS r_score,

    -- تصنيف Frequency
    CASE
        WHEN frequency >= 20 THEN 5
        WHEN frequency >= 10 THEN 4
        WHEN frequency >= 5 THEN 3
        WHEN frequency >= 2 THEN 2
        ELSE 1
    END AS f_score,

    -- تصنيف Monetary
    CASE
        WHEN monetary >= 50000 THEN 5
        WHEN monetary >= 20000 THEN 4
        WHEN monetary >= 10000 THEN 3
        WHEN monetary >= 5000 THEN 2
        ELSE 1
    END AS m_score,

    -- شريحة العميل
    CASE
        WHEN recency <= 30 AND frequency >= 10 AND monetary >= 20000 THEN 'أبطال'
        WHEN recency <= 60 AND frequency >= 5 THEN 'عملاء مخلصون'
        WHEN recency <= 30 AND frequency < 3 THEN 'جدد واعدون'
        WHEN recency > 180 AND monetary >= 20000 THEN 'بحاجة للتنشيط'
        WHEN recency > 180 THEN 'في خطر الفقدان'
        ELSE 'عادي'
    END AS customer_segment
FROM customer_rfm;

اختبر معلوماتك

السؤال 1: ما الفرق بين Simple CASE و Searched CASE؟

السؤال 2: لتجنب القسمة على صفر، نستخدم:

السؤال 3: ماذا يحدث إذا لم يتطابق أي شرط ولا يوجد ELSE؟