{"id":"l-sql-1-3","courseId":"sql-for-analysts","lang":"en","kind":"lesson","summary":"Predict how NULL behaves in comparisons, filters and aggregates, test for it with IS NULL and COALESCE, and turn raw values into readable labels with CASE expressions.","html":"<p>Julia Fischer, who leads customer support, sends two questions: “How many tickets are still open? And what’s our average satisfaction score?” Both answers depend on the same value, and it’s the one that causes more wrong numbers in analytics than any other: <code dir=\"ltr\">NULL</code>.</p>\n<p>In <code dir=\"ltr\">support_tickets</code>, <code dir=\"ltr\">closed_at</code> is NULL while a ticket is open, and <code dir=\"ltr\">satisfaction</code> is NULL when the customer never rated it. NULL doesn’t mean zero or empty text. It means <em>unknown</em>.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Unknown is contagious</h2>\n<p>Try finding open tickets the way you’d find anything else:</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=\"Run this example\"><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>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><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>Copy</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>Zero. Is <code dir=\"ltr\">closed_at</code> equal to NULL? SQL’s answer is “unknown”, because comparing anything with an unknown value gives an unknown result, even <code dir=\"ltr\">NULL = NULL</code>. And <code dir=\"ltr\">WHERE</code> keeps a row only when its condition is TRUE, never when it’s unknown. The test for missing values has its own syntax:</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=\"Run this example\"><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>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><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>Copy</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 open tickets. This is three-valued logic: every condition is TRUE, FALSE or NULL. AND and OR follow the tables below. The rule of thumb: NULL wins unless the other side settles the answer on its own (FALSE for AND, TRUE for 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\">Truth tables for AND and OR over TRUE, FALSE and NULL. FALSE AND anything is FALSE; TRUE OR anything is TRUE; every other combination involving NULL is 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 keeps a row only when the whole condition is TRUE</text>\n</svg><figcaption>Three-valued logic: what AND and OR return when one side is NULL</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">Where NULL quietly drops rows</h2>\n<p>The dangerous case isn’t <code dir=\"ltr\">= NULL</code>, which returns nothing and gets noticed. It’s a “not equal” filter on a column that has NULLs:</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=\"Run this example\"><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>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><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>Copy</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 versus 1,942. “Orders that didn’t use BLACKFRIDAY25” obviously includes the 1,678 orders with no coupon at all, but <code dir=\"ltr\">&lt;&gt;</code> dropped every one of them. <code dir=\"ltr\">IS DISTINCT FROM</code> treats NULL as a comparable value, so it’s the NULL-safe version of <code dir=\"ltr\">&lt;&gt;</code>. SQLite also accepts the shorter <code dir=\"ltr\">IS NOT 'BLACKFRIDAY25'</code>; PostgreSQL only accepts <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\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>NOT IN with a NULL in the list</span></p>\n<p><code dir=\"ltr\">2 NOT IN (1, NULL)</code> is NULL, not TRUE, because SQL can’t rule out that the unknown value is 2. So a <code dir=\"ltr\">NOT IN (subquery)</code> whose subquery returns a single NULL keeps <strong>no rows at all</strong>. You’ll meet this for real in the anti-join lesson; for now, remember that <code dir=\"ltr\">NOT IN</code> and nullable columns don’t mix.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">NULL in aggregates</h2>\n<p>Aggregates skip NULLs, which is usually what you want but always worth stating:</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=\"Run this example\"><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>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><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>Copy</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> counts rows (873); <code dir=\"ltr\">COUNT(satisfaction)</code> counts known values (588). <code dir=\"ltr\">AVG</code> divides by the known count, giving 3.74. Replacing NULL with 0 using <code dir=\"ltr\">COALESCE</code> drags the average to 2.52, inventing 285 furious customers who never said anything. Report Julia the 3.74 <em>and</em> the response rate, 588 of 873: an average over two-thirds of tickets should say so.</p>\n<p><code dir=\"ltr\">COALESCE(a, b, ...)</code> returns its first non-NULL argument and is right for display defaults, like <code dir=\"ltr\">COALESCE(coupon_code, 'none')</code>. Its mirror image is <code dir=\"ltr\">NULLIF(a, b)</code>, which returns NULL when <code dir=\"ltr\">a = b</code>. Dividing by <code dir=\"ltr\">NULLIF(denominator, 0)</code> gives NULL instead of a divide-by-zero error in engines that raise one.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Decide what each NULL means</h2>\n<p>NULL is one marker for several different situations, and the right treatment depends on which one you’re looking at. In Cartwheel alone:</p>\n<ul>\n<li><code dir=\"ltr\">closed_at</code> is NULL because the event <strong>hasn’t happened yet</strong>. Count these as open, and exclude them from resolution times.</li>\n<li><code dir=\"ltr\">satisfaction</code> is NULL because the customer <strong>didn’t answer</strong>. Report the response rate next to the average.</li>\n<li><code dir=\"ltr\">coupon_code</code> is NULL because the order had <strong>no coupon</strong>. Here NULL really means “none”, so <code dir=\"ltr\">COALESCE(coupon_code, 'none')</code> is a fair label.</li>\n<li><code dir=\"ltr\">referred_by</code> is NULL because the customer <strong>wasn’t referred</strong>, or because nobody recorded it. You can’t tell which, and you should say so.</li>\n</ul>\n<p>NULLs also affect sorting. SQLite puts NULLs first in ascending order and last in descending order; PostgreSQL does the opposite. If the position matters, say it explicitly with <code dir=\"ltr\">ORDER BY satisfaction NULLS LAST</code>, which both engines accept.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">CASE: labels from logic</h2>\n<p>Julia also wants a readable status for each ticket; that one is your exercise. Here is the same tool on orders, labelling how each one was priced. <code dir=\"ltr\">CASE</code> evaluates its WHEN conditions top to bottom and returns the first match:</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=\"Run this example\"><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>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><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>Copy</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>Two rules make CASE reliable. Order the WHENs from most specific to most general, because the first TRUE branch wins and the rest are never checked. And write an ELSE on purpose: without one, unmatched rows get NULL, which then flows into everything downstream. SQLite also has <code dir=\"ltr\">IIF(condition, a, b)</code> for two-way choices, but CASE is standard SQL and works in every engine.</p>\n<p>Missing values are one source of surprise; dates are the other, and they’re next.</p>\n","toc":[{"id":"sec-1","text":"Unknown is contagious","level":2},{"id":"sec-2","text":"Where NULL quietly drops rows","level":2},{"id":"sec-3","text":"NULL in aggregates","level":2},{"id":"sec-4","text":"Decide what each NULL means","level":2},{"id":"sec-5","text":"CASE: labels from logic","level":2}],"takeaways":["Any comparison with NULL, including NULL = NULL, returns NULL (unknown), and WHERE keeps only rows where the condition is TRUE.","Test for missing values with IS NULL or IS NOT NULL; use IS DISTINCT FROM (or IS NOT in SQLite) for a NULL-safe “not equal”.","COUNT(*) counts rows, COUNT(column) counts non-NULL values, and AVG ignores NULLs instead of treating them as zero.","A CASE expression returns the result of the first WHEN that is TRUE, so put the most specific conditions first and always decide what ELSE should be."],"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> is NULL for most orders. How many rows does <code dir=\"ltr\">WHERE coupon_code &lt;&gt; 'FREESHIP'</code> keep?</p>","options":[{"text":"Every order except the FREESHIP ones, NULLs included.","why":"That’s the intuitive reading, but <code dir=\"ltr\">NULL &lt;&gt; 'FREESHIP'</code> is NULL, not TRUE, so those rows are dropped."},{"text":"Only orders that used some other coupon; orders without a coupon are dropped.","why":"Correct. The comparison is unknown for NULL coupons, and WHERE keeps only TRUE rows. Use <code dir=\"ltr\">coupon_code IS DISTINCT FROM 'FREESHIP'</code> to keep them."},{"text":"No rows, because the column contains NULLs.","why":"NULLs only affect their own rows. Orders with a non-NULL coupon still compare normally."}],"answer":1},{"q":"<p>A ticket table has 10 rows; 4 have <code dir=\"ltr\">satisfaction</code> NULL and the other 6 average 4.0. What does <code dir=\"ltr\">AVG(satisfaction)</code> return?</p>","options":[{"text":"2.4","why":"That’s what you’d get if NULLs counted as zero (24 / 10). AVG skips NULLs instead."},{"text":"NULL, because some values are missing","why":"Aggregates ignore NULL inputs; AVG only returns NULL when every value is NULL."},{"text":"4.0","why":"Correct. AVG divides the sum of the 6 known scores by 6. Whether that’s the right number to report depends on why the other 4 are missing."},{"text":"It depends on whether you use COUNT(*) in the query","why":"AVG’s behaviour doesn’t depend on other columns in the SELECT list."}],"answer":2},{"q":"<p>What label does a ticket with <code dir=\"ltr\">satisfaction = 1</code> get?</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=\"Copy code\"><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>Copy</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":"Correct. CASE stops at the first TRUE branch, and <code dir=\"ltr\">1 &lt;= 3</code> is TRUE, so the ‘unhappy’ branch is never reached. Order WHENs from most to least specific."},{"text":"‘unhappy’","why":"It would be if the WHENs were in the other order. CASE doesn’t look for the best match, only the first."},{"text":"‘happy’","why":"ELSE only applies when no WHEN is TRUE, and the first one is."}],"answer":0},{"q":"<p>Which expression safely computes <code dir=\"ltr\">refunds / orders</code> when <code dir=\"ltr\">orders</code> can be 0?</p>","options":[{"text":"<code dir=\"ltr\">refunds / orders</code>","why":"In SQLite, dividing by zero returns NULL rather than an error, but other engines like PostgreSQL throw, and the intent isn’t visible to a reader."},{"text":"<code dir=\"ltr\">COALESCE(refunds / orders, 0)</code>","why":"This turns “can’t compute” into 0, which misreports a ratio that has no denominator as a real zero."},{"text":"<code dir=\"ltr\">refunds / NULLIF(orders, 0)</code>","why":"Correct. NULLIF turns a zero denominator into NULL, so the result is NULL (“no data”) in every engine, which is honest and portable."}],"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":"Label Julia's urgent tickets","prompt":"<p>Julia wants every <strong>urgent</strong> ticket with a <code dir=\"ltr\">state</code> label, checked in this order:</p>\n<ol>\n<li><code dir=\"ltr\">'open'</code> when the ticket has no <code dir=\"ltr\">closed_at</code></li>\n<li><code dir=\"ltr\">'unrated'</code> when it’s closed but has no <code dir=\"ltr\">satisfaction</code> score</li>\n<li><code dir=\"ltr\">'unhappy'</code> when the score is 1 or 2</li>\n<li><code dir=\"ltr\">'happy'</code> otherwise</li>\n</ol>\n<p>Return <code dir=\"ltr\">ticket_id</code> and <code dir=\"ltr\">state</code>, sorted by <code dir=\"ltr\">ticket_id</code>. Order matters. The starter labels almost everything ‘happy’. Find out why and fix it.</p>\n","hints":["What does <code dir=\"ltr\">closed_at = NULL</code> evaluate to, for any row?","A CASE branch is taken only when its condition is TRUE. NULL (unknown) isn’t TRUE, so the row falls through to ELSE.","Use <code dir=\"ltr\">IS NULL</code> in the first two WHEN branches."],"explanation":"<p><code dir=\"ltr\">closed_at = NULL</code> is NULL for every row, so the first two branches never fire, and an open, unrated ticket falls past <code dir=\"ltr\">satisfaction &lt;= 2</code> (also NULL) into ELSE: ‘happy’. With <code dir=\"ltr\">IS NULL</code>, open tickets are caught first. The branch order also matters: elsewhere in the table a few open tickets already carry a score, and because ‘open’ is checked first they’re labelled open, which is what Julia’s rules say.</p>\n","checkLabels":{}},"words":729,"readMinutes":3}