EduFlow
SQL للمحلّلين
⁦0%⁩

الوحدة 1 · الاستعلام بهدف واضح

منطق NULL وتعبيرات CASE

18 دقراءة 4 دتمرين SQL

الدرس في لمحة

توقّع سلوك NULL في المقارنات والشروط ودوال التجميع، واختبره بـ IS NULL وCOALESCE، وحوّل القيم الخام إلى تسميات مقروءة بتعبيرات CASE.

  1. 1«غير معروف» قيمة معدية
  2. 2حيث تُسقط NULL الصفوف بصمت
  3. 3NULL في دوال التجميع
  4. 4قرّر ما تعنيه كل NULL
  5. 5CASE: تسميات من المنطق

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

ترسل جوليا فيشر، مسؤولة دعم العملاء، سؤالين: «كم تذكرة ما زالت مفتوحة؟ وما متوسط تقييم الرضا لدينا؟». الإجابتان تعتمدان على القيمة نفسها، وهي القيمة التي تسبّب أرقامًا خاطئة في التحليل أكثر من أي قيمة أخرى: NULL.

في support_tickets تكون قيمة closed_at هي NULL ما دامت التذكرة مفتوحة، وتكون satisfaction هي NULL حين لا يقيّم العميل الخدمة. NULL لا تعني صفرًا ولا نصًا فارغًا، بل تعني غير معروف.

«غير معروف» قيمة معدية

جرّب أن تجد التذاكر المفتوحة كما تجد أي شيء آخر:

SQL
SELECT COUNT(*) AS open_tickets
FROM support_tickets
WHERE closed_at = NULL;

صفر. هل closed_at يساوي NULL؟ إجابة SQL هي «غير معروف»، لأن مقارنة أي شيء بقيمة غير معروفة تعطي نتيجة غير معروفة، حتى NULL = NULL. وWHERE لا يُبقي الصف إلا حين يكون شرطه TRUE، وليس حين يكون غير معروف. لاختبار القيم المفقودة صياغة خاصة:

SQL
SELECT COUNT(*) AS open_tickets
FROM support_tickets
WHERE closed_at IS NULL;

56 تذكرة مفتوحة. هذا هو المنطق ثلاثي القيم: كل شرط إمّا TRUE أو FALSE أو NULL. ويتبع AND وOR الجدولين أدناه. القاعدة العامة: NULL تغلب، إلا إذا حسم الطرف الآخر الإجابة وحده (FALSE مع AND، وTRUE مع OR).

جدولا الحقيقة لـ AND وOR على القيم TRUE وFALSE وNULL. FALSE مع AND وأي قيمة يعطي FALSE؛ وTRUE مع OR وأي قيمة يعطي TRUE؛ وكل تركيبة أخرى فيها NULL تعطي NULL. AND TRUE TRUE FALSE FALSE NULL NULL TRUE FALSE NULL FALSE FALSE FALSE NULL FALSE NULL OR TRUE TRUE FALSE FALSE NULL NULL TRUE TRUE TRUE TRUE FALSE NULL TRUE NULL NULL يُبقي WHERE الصف فقط حين يكون الشرط كله TRUE
المنطق ثلاثي القيم: ما يعيده AND وOR حين يكون أحد الطرفين NULL

حيث تُسقط NULL الصفوف بصمت

الحالة الخطرة ليست = NULL، فهي لا تعيد شيئًا ويلاحظها الجميع. الخطر في شرط «لا يساوي» على عمود فيه قيم NULL:

SQL
SELECT
  (SELECT COUNT(*) FROM orders WHERE coupon_code <> 'BLACKFRIDAY25') AS with_not_equal,
  (SELECT COUNT(*) FROM orders WHERE coupon_code IS DISTINCT FROM 'BLACKFRIDAY25') AS null_safe;

264 مقابل 1,942. «الطلبات التي لم تستخدم BLACKFRIDAY25» تشمل بديهيًا الطلبات الـ 1,678 التي بلا كوبون أصلًا، لكن <> أسقطها كلها. يعامل IS DISTINCT FROM قيمة NULL كقيمة قابلة للمقارنة، فهو النسخة الآمنة مع NULL من <>. يقبل SQLite أيضًا الصيغة الأقصر IS NOT 'BLACKFRIDAY25'؛ أما PostgreSQL فلا يقبل إلا IS DISTINCT FROM.

NULL في دوال التجميع

دوال التجميع تتخطّى قيم NULL، وهذا ما تريده عادةً، لكن يستحق أن تصرّح به دائمًا:

SQL
SELECT
  COUNT(*)                     AS tickets,
  COUNT(satisfaction)          AS rated,
  ROUND(AVG(satisfaction), 2)  AS avg_score,
  ROUND(AVG(COALESCE(satisfaction, 0)), 2) AS wrong_avg
FROM support_tickets;

COUNT(*) يعدّ الصفوف (873)؛ وCOUNT(satisfaction) يعدّ القيم المعروفة (588). ويقسم AVG على عدد القيم المعروفة، فيعطي 3.74. أما استبدال NULL بصفر باستخدام COALESCE فيهبط بالمتوسط إلى 2.52، مخترعًا 285 عميلًا غاضبًا لم يقولوا شيئًا أصلًا. أبلغ جوليا بالرقم 3.74 ومعه معدل الاستجابة، 588 من 873: المتوسط المحسوب على ثلثي التذاكر يجب أن يقول ذلك صراحة.

COALESCE(a, b, ...) يعيد أول وسيط قيمته ليست NULL، وهو مناسب للقيم الافتراضية عند العرض، مثل COALESCE(coupon_code, 'none'). وصورته المعكوسة هي NULLIF(a, b)، الذي يعيد NULL حين يكون a = b. القسمة على NULLIF(denominator, 0) تعطي NULL بدل خطأ القسمة على صفر في المحرّكات التي ترمي هذا الخطأ.

قرّر ما تعنيه كل NULL

NULL علامة واحدة لعدة حالات مختلفة، والتعامل الصحيح معها يعتمد على الحالة التي أمامك. في Cartwheel وحدها:

  • closed_at قيمته NULL لأن الحدث لم يقع بعد. احسب هذه التذاكر مفتوحة، واستبعدها من أوقات الحل.
  • satisfaction قيمته NULL لأن العميل لم يُجب. اعرض معدل الاستجابة بجانب المتوسط.
  • coupon_code قيمته NULL لأن الطلب بلا كوبون. هنا تعني NULL فعلًا «لا شيء»، فـ COALESCE(coupon_code, 'none') تسمية عادلة.
  • referred_by قيمته NULL لأن العميل لم تتم إحالته، أو لأن أحدًا لم يسجّل الإحالة. لا يمكنك أن تعرف أيّ الاحتمالين، ويجب أن تقول ذلك.

تؤثّر NULL في الترتيب أيضًا. يضع SQLite قيم NULL أولًا في الترتيب التصاعدي وأخيرًا في التنازلي؛ وPostgreSQL يفعل العكس. إذا كان موضعها مهمًا، فصرّح به بـ ORDER BY satisfaction NULLS LAST، الذي يقبله المحرّكان.

CASE: تسميات من المنطق

تريد جوليا أيضًا حالة مقروءة لكل تذكرة؛ وهذا تمرينك. وإليك الأداة نفسها على الطلبات، تصنّف طريقة تسعير كل طلب. يقيّم CASE شروط WHEN من الأعلى إلى الأسفل ويعيد أول تطابق:

SQL
SELECT
  order_id,
  coupon_code,
  shipping_fee,
  CASE
    WHEN coupon_code IS NULL AND shipping_fee = 0 THEN 'free shipping only'
    WHEN coupon_code IS NULL THEN 'full price'
    WHEN coupon_code = 'FREESHIP' THEN 'coupon: shipping'
    ELSE 'coupon: discount'
  END AS price_type
FROM orders
ORDER BY order_id
LIMIT 6;

قاعدتان تجعلان CASE موثوقًا. رتّب فروع WHEN من الأكثر تحديدًا إلى الأعم، لأن أول فرع قيمته TRUE يفوز ولا يُفحص الباقي. واكتب ELSE عن قصد: بدونه تحصل الصفوف غير المطابقة على NULL، وتنتقل هذه القيمة إلى كل ما يأتي بعدها. لدى SQLite أيضًا IIF(condition, a, b) للاختيار بين احتمالين، لكن CASE هو SQL القياسي ويعمل في كل محرّك.

القيم المفقودة مصدر واحد للمفاجآت؛ والتواريخ هي المصدر الآخر، وهي موضوعنا التالي.

أهم النقاط

  • أي مقارنة مع NULL، بما فيها NULL = NULL، تعيد NULL (غير معروف)، وWHERE لا يُبقي إلا الصفوف التي يكون شرطها TRUE.
  • اختبر القيم المفقودة بـ IS NULL أو IS NOT NULL؛ واستخدم IS DISTINCT FROM (أو IS NOT في SQLite) لمقارنة «لا يساوي» آمنة مع NULL.
  • COUNT(*) يعدّ الصفوف، وCOUNT(column) يعدّ القيم غير الـ NULL، وAVG يتجاهل قيم NULL بدل أن يعاملها كأصفار.
  • تعبير CASE يعيد نتيجة أول WHEN قيمته TRUE، لذا ضع الشروط الأكثر تحديدًا أولًا وقرّر دائمًا ما يجب أن يكون عليه ELSE.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

قيمة orders.coupon_code هي NULL في معظم الطلبات. كم صفًا يُبقي WHERE coupon_code <> 'FREESHIP'؟

السؤال 2 من 4

جدول تذاكر فيه 10 صفوف؛ 4 منها قيمة satisfaction فيها NULL، ومتوسط الستة الباقية 4.0. ماذا يعيد AVG(satisfaction)؟

السؤال 3 من 4

ما التسمية التي تحصل عليها تذكرة قيمة satisfaction = 1 فيها؟

SQL
CASE
  WHEN satisfaction <= 3 THEN 'neutral'
  WHEN satisfaction <= 2 THEN 'unhappy'
  ELSE 'happy'
END
السؤال 4 من 4

أي تعبير يحسب refunds / orders بأمان حين يمكن أن تكون orders صفرًا؟

تدريب

تمرين SQL

صنّف تذاكر جوليا العاجلة

تريد جوليا كل تذكرة عاجلة (urgent) مع تسمية state، تُفحص بهذا الترتيب:

  1. 'open' حين لا يكون للتذكرة closed_at
  2. 'unrated' حين تكون مغلقة لكن بلا تقييم satisfaction
  3. 'unhappy' حين يكون التقييم 1 أو 2
  4. 'happy' في غير ذلك

أعِد ticket_id وstate، مرتّبة حسب ticket_id. الترتيب مهم. الكود المبدئي يصنّف معظم التذاكر ‘happy’. اكتشف السبب وأصلحه.

الجداول
customers
(customer_id, first_name, last_name, email, country, city, signup_date, segment, referred_by)
orders
(order_id, customer_id, order_date, status, channel, coupon_code, shipping_fee)
order_items
(order_item_id, order_id, product_id, quantity, unit_price, discount)
products
(product_id, name, category_id, unit_price, unit_cost, launched_on, is_active)
categories
(category_id, name, parent_id)
returns
(return_id, order_item_id, return_date, reason, refund_amount)
employees
(employee_id, first_name, last_name, title, department, manager_id, hire_date, salary)
support_tickets
(ticket_id, customer_id, order_id, opened_at, closed_at, priority, channel, satisfaction, agent_id)
web_sessions
(session_id, customer_id, started_at, source, device, pages_viewed, added_to_cart, purchased)
-- Urgent tickets with a readable state
SELECT
  ticket_id,
  CASE
    WHEN closed_at = NULL THEN 'open'
    WHEN satisfaction = NULL THEN 'unrated'
    WHEN satisfaction <= 2 THEN 'unhappy'
    ELSE 'happy'
  END AS state
FROM support_tickets
WHERE priority = 'urgent'
ORDER BY ticket_id;جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.