{"id":"l-sql-1-3","courseId":"sql-for-analysts","lang":"ar","kind":"lesson","summary":"توقّع سلوك NULL في المقارنات والشروط ودوال التجميع، واختبره بـ IS NULL وCOALESCE، وحوّل القيم الخام إلى تسميات مقروءة بتعبيرات CASE.","html":"<p>ترسل جوليا فيشر، مسؤولة دعم العملاء، سؤالين: «كم تذكرة ما زالت مفتوحة؟ وما متوسط تقييم الرضا لدينا؟». الإجابتان تعتمدان على القيمة نفسها، وهي القيمة التي تسبّب أرقامًا خاطئة في التحليل أكثر من أي قيمة أخرى: <code dir=\"ltr\">NULL</code>.</p>\n<p>في <code dir=\"ltr\">support_tickets</code> تكون قيمة <code dir=\"ltr\">closed_at</code> هي NULL ما دامت التذكرة مفتوحة، وتكون <code dir=\"ltr\">satisfaction</code> هي NULL حين لا يقيّم العميل الخدمة. NULL لا تعني صفرًا ولا نصًا فارغًا، بل تعني <em>غير معروف</em>.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">«غير معروف» قيمة معدية</h2>\n<p>جرّب أن تجد التذاكر المفتوحة كما تجد أي شيء آخر:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"sql\" data-run=\"sql\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">SQL</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"sql\" data-index=\"0\" aria-label=\"شغّل هذا المثال\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>تشغيل</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> COUNT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> open_tickets</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> support_tickets</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> closed_at </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> NULL</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p>صفر. هل <code dir=\"ltr\">closed_at</code> يساوي NULL؟ إجابة SQL هي «غير معروف»، لأن مقارنة أي شيء بقيمة غير معروفة تعطي نتيجة غير معروفة، حتى <code dir=\"ltr\">NULL = NULL</code>. وWHERE لا يُبقي الصف إلا حين يكون شرطه TRUE، وليس حين يكون غير معروف. لاختبار القيم المفقودة صياغة خاصة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"sql\" data-run=\"sql\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">SQL</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"sql\" data-index=\"1\" aria-label=\"شغّل هذا المثال\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>تشغيل</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> COUNT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> open_tickets</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> support_tickets</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> closed_at </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">IS</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> NULL</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre><div class=\"run-output\" data-run-output=\"1\" hidden></div></div>\n<p>56 تذكرة مفتوحة. هذا هو المنطق ثلاثي القيم: كل شرط إمّا TRUE أو FALSE أو NULL. ويتبع AND وOR الجدولين أدناه. القاعدة العامة: NULL تغلب، إلا إذا حسم الطرف الآخر الإجابة وحده (FALSE مع AND، وTRUE مع OR).</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 720 220\" role=\"img\" aria-labelledby=\"l-sql-1-3-t1\"><defs><marker id=\"arrow-l-sql-1-3-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-sql-1-3-t1\">جدولا الحقيقة لـ AND وOR على القيم TRUE وFALSE وNULL. FALSE مع AND وأي قيمة يعطي FALSE؛ وTRUE مع OR وأي قيمة يعطي TRUE؛ وكل تركيبة أخرى فيها NULL تعطي NULL.</title>\n  <text class=\"d-label-strong\" x=\"70\" y=\"63\" text-anchor=\"middle\">AND</text>\n  <text class=\"d-label-muted\" x=\"150\" y=\"63\" text-anchor=\"middle\">TRUE</text>\n  <text class=\"d-label-muted\" x=\"70\" y=\"99\" text-anchor=\"middle\">TRUE</text>\n  <text class=\"d-label-muted\" x=\"230\" y=\"63\" text-anchor=\"middle\">FALSE</text>\n  <text class=\"d-label-muted\" x=\"70\" y=\"135\" text-anchor=\"middle\">FALSE</text>\n  <text class=\"d-label-muted\" x=\"310\" y=\"63\" text-anchor=\"middle\">NULL</text>\n  <text class=\"d-label-muted\" x=\"70\" y=\"171\" text-anchor=\"middle\">NULL</text>\n  <rect class=\"d-box-success\" x=\"112\" y=\"78\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"150\" y=\"99\" text-anchor=\"middle\">TRUE</text>\n  <rect class=\"d-box\" x=\"192\" y=\"78\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"230\" y=\"99\" text-anchor=\"middle\">FALSE</text>\n  <rect class=\"d-box-warn\" x=\"272\" y=\"78\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"310\" y=\"99\" text-anchor=\"middle\">NULL</text>\n  <rect class=\"d-box\" x=\"112\" y=\"114\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"150\" y=\"135\" text-anchor=\"middle\">FALSE</text>\n  <rect class=\"d-box\" x=\"192\" y=\"114\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"230\" y=\"135\" text-anchor=\"middle\">FALSE</text>\n  <rect class=\"d-box\" x=\"272\" y=\"114\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"310\" y=\"135\" text-anchor=\"middle\">FALSE</text>\n  <rect class=\"d-box-warn\" x=\"112\" y=\"150\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"150\" y=\"171\" text-anchor=\"middle\">NULL</text>\n  <rect class=\"d-box\" x=\"192\" y=\"150\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"230\" y=\"171\" text-anchor=\"middle\">FALSE</text>\n  <rect class=\"d-box-warn\" x=\"272\" y=\"150\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"310\" y=\"171\" text-anchor=\"middle\">NULL</text>\n  <text class=\"d-label-strong\" x=\"420\" y=\"63\" text-anchor=\"middle\">OR</text>\n  <text class=\"d-label-muted\" x=\"500\" y=\"63\" text-anchor=\"middle\">TRUE</text>\n  <text class=\"d-label-muted\" x=\"420\" y=\"99\" text-anchor=\"middle\">TRUE</text>\n  <text class=\"d-label-muted\" x=\"580\" y=\"63\" text-anchor=\"middle\">FALSE</text>\n  <text class=\"d-label-muted\" x=\"420\" y=\"135\" text-anchor=\"middle\">FALSE</text>\n  <text class=\"d-label-muted\" x=\"660\" y=\"63\" text-anchor=\"middle\">NULL</text>\n  <text class=\"d-label-muted\" x=\"420\" y=\"171\" text-anchor=\"middle\">NULL</text>\n  <rect class=\"d-box-success\" x=\"462\" y=\"78\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"500\" y=\"99\" text-anchor=\"middle\">TRUE</text>\n  <rect class=\"d-box-success\" x=\"542\" y=\"78\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"580\" y=\"99\" text-anchor=\"middle\">TRUE</text>\n  <rect class=\"d-box-success\" x=\"622\" y=\"78\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"660\" y=\"99\" text-anchor=\"middle\">TRUE</text>\n  <rect class=\"d-box-success\" x=\"462\" y=\"114\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"500\" y=\"135\" text-anchor=\"middle\">TRUE</text>\n  <rect class=\"d-box\" x=\"542\" y=\"114\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"580\" y=\"135\" text-anchor=\"middle\">FALSE</text>\n  <rect class=\"d-box-warn\" x=\"622\" y=\"114\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"660\" y=\"135\" text-anchor=\"middle\">NULL</text>\n  <rect class=\"d-box-success\" x=\"462\" y=\"150\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"500\" y=\"171\" text-anchor=\"middle\">TRUE</text>\n  <rect class=\"d-box-warn\" x=\"542\" y=\"150\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"580\" y=\"171\" text-anchor=\"middle\">NULL</text>\n  <rect class=\"d-box-warn\" x=\"622\" y=\"150\" width=\"76\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"660\" y=\"171\" text-anchor=\"middle\">NULL</text>\n  <text class=\"d-label-muted\" x=\"360\" y=\"210\" text-anchor=\"middle\">يُبقي WHERE الصف فقط حين يكون الشرط كله TRUE</text>\n</svg><figcaption>المنطق ثلاثي القيم: ما يعيده AND وOR حين يكون أحد الطرفين NULL</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">حيث تُسقط NULL الصفوف بصمت</h2>\n<p>الحالة الخطرة ليست <code dir=\"ltr\">= NULL</code>، فهي لا تعيد شيئًا ويلاحظها الجميع. الخطر في شرط «لا يساوي» على عمود فيه قيم NULL:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"sql\" data-run=\"sql\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">SQL</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"sql\" data-index=\"2\" aria-label=\"شغّل هذا المثال\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>تشغيل</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> COUNT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> orders </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> coupon_code </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;></span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'BLACKFRIDAY25'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> with_not_equal,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> COUNT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> orders </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> coupon_code </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">IS</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> DISTINCT</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> FROM</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'BLACKFRIDAY25'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> null_safe;</span></span></code></pre><div class=\"run-output\" data-run-output=\"2\" hidden></div></div>\n<p>264 مقابل 1,942. «الطلبات التي لم تستخدم BLACKFRIDAY25» تشمل بديهيًا الطلبات الـ 1,678 التي بلا كوبون أصلًا، لكن <code dir=\"ltr\">&lt;&gt;</code> أسقطها كلها. يعامل <code dir=\"ltr\">IS DISTINCT FROM</code> قيمة NULL كقيمة قابلة للمقارنة، فهو النسخة الآمنة مع NULL من <code dir=\"ltr\">&lt;&gt;</code>. يقبل SQLite أيضًا الصيغة الأقصر <code dir=\"ltr\">IS NOT 'BLACKFRIDAY25'</code>؛ أما PostgreSQL فلا يقبل إلا <code dir=\"ltr\">IS DISTINCT FROM</code>.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>NOT IN مع NULL في القائمة</span></p>\n<p><code dir=\"ltr\">2 NOT IN (1, NULL)</code> قيمته NULL لا TRUE، لأن SQL لا يستطيع أن يستبعد احتمال أن تكون القيمة المجهولة هي 2. لذا فإن <code dir=\"ltr\">NOT IN (subquery)</code> الذي يعيد الـ subquery فيه قيمة NULL واحدة <strong>لا يُبقي أي صف على الإطلاق</strong>. ستقابل هذا فعليًا في درس الـ anti-join؛ أما الآن فتذكّر أن <code dir=\"ltr\">NOT IN</code> والأعمدة التي تقبل NULL لا يجتمعان.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">NULL في دوال التجميع</h2>\n<p>دوال التجميع تتخطّى قيم NULL، وهذا ما تريده عادةً، لكن يستحق أن تصرّح به دائمًا:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"sql\" data-run=\"sql\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">SQL</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"sql\" data-index=\"3\" aria-label=\"شغّل هذا المثال\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>تشغيل</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  COUNT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)                     </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tickets,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  COUNT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(satisfaction)          </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> rated,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  ROUND</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">AVG</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(satisfaction), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)  </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> avg_score,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  ROUND</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">AVG</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">COALESCE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(satisfaction, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> wrong_avg</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> support_tickets;</span></span></code></pre><div class=\"run-output\" data-run-output=\"3\" hidden></div></div>\n<p><code dir=\"ltr\">COUNT(*)</code> يعدّ الصفوف (873)؛ و<code dir=\"ltr\">COUNT(satisfaction)</code> يعدّ القيم المعروفة (588). ويقسم <code dir=\"ltr\">AVG</code> على عدد القيم المعروفة، فيعطي 3.74. أما استبدال NULL بصفر باستخدام <code dir=\"ltr\">COALESCE</code> فيهبط بالمتوسط إلى 2.52، مخترعًا 285 عميلًا غاضبًا لم يقولوا شيئًا أصلًا. أبلغ جوليا بالرقم 3.74 <em>ومعه</em> معدل الاستجابة، 588 من 873: المتوسط المحسوب على ثلثي التذاكر يجب أن يقول ذلك صراحة.</p>\n<p><code dir=\"ltr\">COALESCE(a, b, ...)</code> يعيد أول وسيط قيمته ليست NULL، وهو مناسب للقيم الافتراضية عند العرض، مثل <code dir=\"ltr\">COALESCE(coupon_code, 'none')</code>. وصورته المعكوسة هي <code dir=\"ltr\">NULLIF(a, b)</code>، الذي يعيد NULL حين يكون <code dir=\"ltr\">a = b</code>. القسمة على <code dir=\"ltr\">NULLIF(denominator, 0)</code> تعطي NULL بدل خطأ القسمة على صفر في المحرّكات التي ترمي هذا الخطأ.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">قرّر ما تعنيه كل NULL</h2>\n<p>NULL علامة واحدة لعدة حالات مختلفة، والتعامل الصحيح معها يعتمد على الحالة التي أمامك. في Cartwheel وحدها:</p>\n<ul>\n<li><code dir=\"ltr\">closed_at</code> قيمته NULL لأن الحدث <strong>لم يقع بعد</strong>. احسب هذه التذاكر مفتوحة، واستبعدها من أوقات الحل.</li>\n<li><code dir=\"ltr\">satisfaction</code> قيمته NULL لأن العميل <strong>لم يُجب</strong>. اعرض معدل الاستجابة بجانب المتوسط.</li>\n<li><code dir=\"ltr\">coupon_code</code> قيمته NULL لأن الطلب <strong>بلا كوبون</strong>. هنا تعني NULL فعلًا «لا شيء»، فـ <code dir=\"ltr\">COALESCE(coupon_code, 'none')</code> تسمية عادلة.</li>\n<li><code dir=\"ltr\">referred_by</code> قيمته NULL لأن العميل <strong>لم تتم إحالته</strong>، أو لأن أحدًا لم يسجّل الإحالة. لا يمكنك أن تعرف أيّ الاحتمالين، ويجب أن تقول ذلك.</li>\n</ul>\n<p>تؤثّر NULL في الترتيب أيضًا. يضع SQLite قيم NULL أولًا في الترتيب التصاعدي وأخيرًا في التنازلي؛ وPostgreSQL يفعل العكس. إذا كان موضعها مهمًا، فصرّح به بـ <code dir=\"ltr\">ORDER BY satisfaction NULLS LAST</code>، الذي يقبله المحرّكان.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">CASE: تسميات من المنطق</h2>\n<p>تريد جوليا أيضًا حالة مقروءة لكل تذكرة؛ وهذا تمرينك. وإليك الأداة نفسها على الطلبات، تصنّف طريقة تسعير كل طلب. يقيّم <code dir=\"ltr\">CASE</code> شروط WHEN من الأعلى إلى الأسفل ويعيد أول تطابق:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"sql\" data-run=\"sql\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">SQL</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"sql\" data-index=\"4\" aria-label=\"شغّل هذا المثال\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>تشغيل</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  order_id,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  coupon_code,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  shipping_fee,</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  CASE</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    WHEN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> coupon_code </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">IS</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> NULL</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> AND</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> shipping_fee </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> THEN</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'free shipping only'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    WHEN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> coupon_code </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">IS</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> NULL</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> THEN</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'full price'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    WHEN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> coupon_code </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'FREESHIP'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> THEN</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'coupon: shipping'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    ELSE</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'coupon: discount'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  END</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> price_type</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> orders</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ORDER BY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> order_id</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">LIMIT</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 6</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre><div class=\"run-output\" data-run-output=\"4\" hidden></div></div>\n<p>قاعدتان تجعلان CASE موثوقًا. رتّب فروع WHEN من الأكثر تحديدًا إلى الأعم، لأن أول فرع قيمته TRUE يفوز ولا يُفحص الباقي. واكتب ELSE عن قصد: بدونه تحصل الصفوف غير المطابقة على NULL، وتنتقل هذه القيمة إلى كل ما يأتي بعدها. لدى SQLite أيضًا <code dir=\"ltr\">IIF(condition, a, b)</code> للاختيار بين احتمالين، لكن CASE هو SQL القياسي ويعمل في كل محرّك.</p>\n<p>القيم المفقودة مصدر واحد للمفاجآت؛ والتواريخ هي المصدر الآخر، وهي موضوعنا التالي.</p>\n","toc":[{"id":"sec-1","text":"«غير معروف» قيمة معدية","level":2},{"id":"sec-2","text":"حيث تُسقط NULL الصفوف بصمت","level":2},{"id":"sec-3","text":"NULL في دوال التجميع","level":2},{"id":"sec-4","text":"قرّر ما تعنيه كل NULL","level":2},{"id":"sec-5","text":"CASE: تسميات من المنطق","level":2}],"takeaways":["أي مقارنة مع 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."],"further":[{"title":"NULL handling in SQLite versus other engines","url":"https://www.sqlite.org/nulls.html"},{"title":"SQLite CASE expression and IS DISTINCT FROM","url":"https://www.sqlite.org/lang_expr.html#the_case_expression"},{"title":"PostgreSQL comparison functions and operators","url":"https://www.postgresql.org/docs/current/functions-comparison.html"}],"quiz":[{"q":"<p>قيمة <code dir=\"ltr\">orders.coupon_code</code> هي NULL في معظم الطلبات. كم صفًا يُبقي <code dir=\"ltr\">WHERE coupon_code &lt;&gt; 'FREESHIP'</code>؟</p>","options":[{"text":"كل الطلبات ما عدا طلبات FREESHIP، بما فيها قيم NULL.","why":"هذه هي القراءة البديهية، لكن <code dir=\"ltr\">NULL &lt;&gt; 'FREESHIP'</code> قيمته NULL لا TRUE، فتُستبعد تلك الصفوف."},{"text":"الطلبات التي استخدمت كوبونًا آخر فقط؛ وتُستبعد الطلبات التي بلا كوبون.","why":"صحيح. المقارنة غير معروفة النتيجة للكوبونات التي قيمتها NULL، وWHERE لا يُبقي إلا صفوف TRUE. استخدم <code dir=\"ltr\">coupon_code IS DISTINCT FROM 'FREESHIP'</code> لإبقائها."},{"text":"لا صفوف، لأن العمود يحتوي على NULL.","why":"قيم NULL لا تؤثّر إلا على صفوفها. الطلبات التي لها كوبون غير NULL تُقارَن بشكل طبيعي."}],"answer":1},{"q":"<p>جدول تذاكر فيه 10 صفوف؛ 4 منها قيمة <code dir=\"ltr\">satisfaction</code> فيها NULL، ومتوسط الستة الباقية 4.0. ماذا يعيد <code dir=\"ltr\">AVG(satisfaction)</code>؟</p>","options":[{"text":"2.4","why":"هذا ما ستحصل عليه لو حُسبت قيم NULL أصفارًا (24 / 10). لكن AVG يتخطّى NULL."},{"text":"NULL، لأن بعض القيم مفقودة","why":"دوال التجميع تتجاهل المدخلات التي قيمتها NULL؛ ولا يعيد AVG قيمة NULL إلا إذا كانت كل القيم NULL."},{"text":"4.0","why":"صحيح. يقسم AVG مجموع التقييمات الستة المعروفة على 6. أما هل هذا هو الرقم الصحيح للتقرير، فيعتمد على سبب غياب الأربعة الأخرى."},{"text":"يعتمد على وجود COUNT(*) في الـ query","why":"سلوك AVG لا يعتمد على الأعمدة الأخرى في قائمة SELECT."}],"answer":2},{"q":"<p>ما التسمية التي تحصل عليها تذكرة قيمة <code dir=\"ltr\">satisfaction = 1</code> فيها؟</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"sql\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">SQL</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">CASE</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  WHEN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> satisfaction </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 3</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> THEN</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'neutral'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  WHEN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> satisfaction </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> THEN</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'unhappy'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  ELSE</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'happy'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">END</span></span></code></pre></div>\n","options":[{"text":"‘neutral’","why":"صحيح. يتوقف CASE عند أول فرع قيمته TRUE، و<code dir=\"ltr\">1 &lt;= 3</code> قيمته TRUE، فلا يُوصَل إلى فرع ‘unhappy’ أبدًا. رتّب WHEN من الأكثر تحديدًا إلى الأقل."},{"text":"‘unhappy’","why":"كان سيحدث هذا لو كانت فروع WHEN بالترتيب المعاكس. CASE لا يبحث عن أفضل تطابق، بل عن أول تطابق فقط."},{"text":"‘happy’","why":"ELSE لا يُطبَّق إلا حين لا يكون أي WHEN قيمته TRUE، والأول قيمته TRUE."}],"answer":0},{"q":"<p>أي تعبير يحسب <code dir=\"ltr\">refunds / orders</code> بأمان حين يمكن أن تكون <code dir=\"ltr\">orders</code> صفرًا؟</p>","options":[{"text":"<code dir=\"ltr\">refunds / orders</code>","why":"في SQLite تعيد القسمة على صفر NULL بدل خطأ، لكن محرّكات أخرى مثل PostgreSQL ترمي خطأً، كما أن النيّة لا تظهر لمن يقرأ الكود."},{"text":"<code dir=\"ltr\">COALESCE(refunds / orders, 0)</code>","why":"هذا يحوّل «لا يمكن الحساب» إلى 0، فيعرض نسبةً لا مقام لها على أنها صفر حقيقي."},{"text":"<code dir=\"ltr\">refunds / NULLIF(orders, 0)</code>","why":"صحيح. NULLIF يحوّل المقام الصفري إلى NULL، فتكون النتيجة NULL («لا بيانات») في كل المحرّكات، وهذا صادق وقابل للنقل."}],"answer":2}],"exercise":{"kind":"sql","starter":"-- Urgent tickets with a readable state\nSELECT\n  ticket_id,\n  CASE\n    WHEN closed_at = NULL THEN 'open'\n    WHEN satisfaction = NULL THEN 'unrated'\n    WHEN satisfaction <= 2 THEN 'unhappy'\n    ELSE 'happy'\n  END AS state\nFROM support_tickets\nWHERE priority = 'urgent'\nORDER BY ticket_id;","solution":"SELECT\n  ticket_id,\n  CASE\n    WHEN closed_at IS NULL THEN 'open'\n    WHEN satisfaction IS NULL THEN 'unrated'\n    WHEN satisfaction <= 2 THEN 'unhappy'\n    ELSE 'happy'\n  END AS state\nFROM support_tickets\nWHERE priority = 'urgent'\nORDER BY ticket_id;","orderMatters":true,"title":"صنّف تذاكر جوليا العاجلة","prompt":"<p>تريد جوليا كل تذكرة <strong>عاجلة</strong> (urgent) مع تسمية <code dir=\"ltr\">state</code>، تُفحص بهذا الترتيب:</p>\n<ol>\n<li><code dir=\"ltr\">'open'</code> حين لا يكون للتذكرة <code dir=\"ltr\">closed_at</code></li>\n<li><code dir=\"ltr\">'unrated'</code> حين تكون مغلقة لكن بلا تقييم <code dir=\"ltr\">satisfaction</code></li>\n<li><code dir=\"ltr\">'unhappy'</code> حين يكون التقييم 1 أو 2</li>\n<li><code dir=\"ltr\">'happy'</code> في غير ذلك</li>\n</ol>\n<p>أعِد <code dir=\"ltr\">ticket_id</code> و<code dir=\"ltr\">state</code>، مرتّبة حسب <code dir=\"ltr\">ticket_id</code>. الترتيب مهم. الكود المبدئي يصنّف معظم التذاكر ‘happy’. اكتشف السبب وأصلحه.</p>\n","hints":["ما قيمة <code dir=\"ltr\">closed_at = NULL</code> لأي صف؟","لا يُختار فرع في CASE إلا حين يكون شرطه TRUE. وNULL (غير معروف) ليست TRUE، فينزل الصف إلى ELSE.","استخدم <code dir=\"ltr\">IS NULL</code> في أول فرعين من WHEN."],"explanation":"<p>قيمة <code dir=\"ltr\">closed_at = NULL</code> هي NULL لكل صف، فلا يعمل الفرعان الأولان أبدًا، وتتجاوز التذكرة المفتوحة غير المقيّمة الشرط <code dir=\"ltr\">satisfaction &lt;= 2</code> (وقيمته NULL أيضًا) لتصل إلى ELSE: ‘happy’. مع <code dir=\"ltr\">IS NULL</code> تُلتقط التذاكر المفتوحة أولًا. ترتيب الفروع مهم كذلك: في مواضع أخرى من الجدول توجد تذاكر مفتوحة تحمل تقييمًا بالفعل، ولأن ‘open’ يُفحص أولًا فهي تُصنَّف مفتوحة، وهذا ما تقوله قواعد جوليا.</p>\n","checkLabels":{}},"words":684,"readMinutes":4}