الوحدة 1 · الاستعلام بهدف واضح
منطق NULL وتعبيرات CASE
18 دقراءة 4 دتمرين SQL
الدرس في لمحة
توقّع سلوك NULL في المقارنات والشروط ودوال التجميع، واختبره بـ IS NULL وCOALESCE، وحوّل القيم الخام إلى تسميات مقروءة بتعبيرات CASE.
- 1«غير معروف» قيمة معدية
- 2حيث تُسقط NULL الصفوف بصمت
- 3NULL في دوال التجميع
- 4قرّر ما تعنيه كل NULL
- 5CASE: تسميات من المنطق
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
ترسل جوليا فيشر، مسؤولة دعم العملاء، سؤالين: «كم تذكرة ما زالت مفتوحة؟ وما متوسط تقييم الرضا لدينا؟». الإجابتان تعتمدان على القيمة نفسها، وهي القيمة التي تسبّب أرقامًا خاطئة في التحليل أكثر من أي قيمة أخرى: NULL.
في support_tickets تكون قيمة closed_at هي NULL ما دامت التذكرة مفتوحة، وتكون satisfaction هي NULL حين لا يقيّم العميل الخدمة. NULL لا تعني صفرًا ولا نصًا فارغًا، بل تعني غير معروف.
«غير معروف» قيمة معدية
جرّب أن تجد التذاكر المفتوحة كما تجد أي شيء آخر:
SELECT COUNT(*) AS open_tickets
FROM support_tickets
WHERE closed_at = NULL;صفر. هل closed_at يساوي NULL؟ إجابة SQL هي «غير معروف»، لأن مقارنة أي شيء بقيمة غير معروفة تعطي نتيجة غير معروفة، حتى NULL = NULL. وWHERE لا يُبقي الصف إلا حين يكون شرطه TRUE، وليس حين يكون غير معروف. لاختبار القيم المفقودة صياغة خاصة:
SELECT COUNT(*) AS open_tickets
FROM support_tickets
WHERE closed_at IS NULL;56 تذكرة مفتوحة. هذا هو المنطق ثلاثي القيم: كل شرط إمّا TRUE أو FALSE أو NULL. ويتبع AND وOR الجدولين أدناه. القاعدة العامة: NULL تغلب، إلا إذا حسم الطرف الآخر الإجابة وحده (FALSE مع AND، وTRUE مع OR).
حيث تُسقط NULL الصفوف بصمت
الحالة الخطرة ليست = NULL، فهي لا تعيد شيئًا ويلاحظها الجميع. الخطر في شرط «لا يساوي» على عمود فيه قيم NULL:
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، وهذا ما تريده عادةً، لكن يستحق أن تصرّح به دائمًا:
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 من الأعلى إلى الأسفل ويعيد أول تطابق:
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.