{"id":"l-sql-2-2","courseId":"sql-for-analysts","lang":"en","kind":"lesson","summary":"Combine tables with INNER and LEFT JOIN on the right keys, qualify columns that share a name, and keep unmatched rows by putting right-table filters in the ON clause.","html":"<p>Karim from Finance asks: “What was our 2025 revenue by product category?” Revenue lives on <code dir=\"ltr\">order_items</code>, the year lives on <code dir=\"ltr\">orders</code>, and the category name lives on <code dir=\"ltr\">categories</code>, two steps away via <code dir=\"ltr\">products</code>. No single table answers the question. Joins put them together, and getting them right is mostly about knowing which rows each join keeps.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Following the keys</h2>\n<p>Each join needs a condition that says which rows belong together, almost always a key: <code dir=\"ltr\">order_items.order_id</code> points to <code dir=\"ltr\">orders.order_id</code>, <code dir=\"ltr\">order_items.product_id</code> to <code dir=\"ltr\">products.product_id</code>, and so on. For Cartwheel, revenue counts every order except cancelled ones.</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>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  c</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> category,</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\">DISTINCT</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">order_id</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\"> orders,</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\">SUM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">oi</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">quantity</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> oi</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">unit_price</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> -</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> oi</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">discount</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\"> revenue</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> o</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">JOIN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> order_items </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> oi </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ON</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> oi</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">order_id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">order_id</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">JOIN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> products    </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> p  </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ON</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> p</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">product_id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> oi</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">product_id</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">JOIN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> categories  </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> c  </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ON</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> c</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">category_id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> p</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">category_id</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x3C;></span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'cancelled'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  AND</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">order_date</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> >=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '2025-01-01'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  AND</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">order_date</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x3C;</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  '2026-01-01'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">GROUP BY</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> c</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">name</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ORDER BY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> revenue </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">DESC</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>Desks &amp; Chairs leads with about 77,000. Two rows look odd, though: “Kitchen” and “Outdoor” are top-level categories, and products are supposed to sit in sub-categories. Keep that in mind; it’s a data-quality finding you’ll chase in Section 4.</p>\n<p>Two habits make join queries readable and safe. Short aliases (<code dir=\"ltr\">o</code>, <code dir=\"ltr\">oi</code>, <code dir=\"ltr\">p</code>, <code dir=\"ltr\">c</code>) keep lines short. And qualifying every column with its alias removes guesswork, which matters here: <code dir=\"ltr\">order_items.unit_price</code> is the price charged on the day, <code dir=\"ltr\">products.unit_price</code> is today’s list price. An unqualified <code dir=\"ltr\">unit_price</code> in this query is an “ambiguous column name” error, and picking the wrong one silently changes revenue.</p>\n<p><code dir=\"ltr\">COUNT(DISTINCT o.order_id)</code> is deliberate too. After joining to items, each order appears once per item line, so <code dir=\"ltr\">COUNT(*)</code> would count lines. That repetition is the subject of the next lesson.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">INNER versus LEFT</h2>\n<p><code dir=\"ltr\">JOIN</code> means <code dir=\"ltr\">INNER JOIN</code>: a row survives only if it finds a match on the other side. That’s right for revenue, where an item without an order would be meaningless. It’s wrong when the question is about <em>everything on one side</em>, including things with no matches.</p>\n<p>Julia asks for each Operations employee’s ticket count, “including people who don’t handle tickets, so I can see who could help in peak season”:</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>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">employee_id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">first_name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">title</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</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:#0550AE;--shiki-dark:#79C0FF\">t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">ticket_id</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:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> employees </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> e</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">LEFT JOIN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> support_tickets </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> t</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  ON</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">agent_id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">employee_id</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">department</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Operations'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">GROUP BY</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">employee_id</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ORDER BY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tickets </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">DESC</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>Eight rows. The four agents have about 200 tickets each; Mark, Samir, Ben and Adam have 0. A <code dir=\"ltr\">LEFT JOIN</code> keeps every row of the left table, here <code dir=\"ltr\">employees</code>, and where no ticket matches, the ticket columns are NULL. <code dir=\"ltr\">COUNT(t.ticket_id)</code> skips those NULLs, which is why the non-agents show 0 rather than 1.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 720 230\" role=\"img\" aria-labelledby=\"l-sql-2-2-t1\"><defs><marker id=\"arrow-l-sql-2-2-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-2-2-t1\">Employees Julia, Tom and Mark on the left; tickets t1 and t2 handled by Julia and t3 by Tom on the right. The inner join result has three rows. The left join result has the same three rows plus Mark with NULL ticket.</title>\n  <text class=\"d-label-strong\" x=\"85\" y=\"28\" text-anchor=\"middle\">employees</text>\n  <rect class=\"d-box\" x=\"20\" y=\"40\" width=\"130\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"85\" y=\"62\" text-anchor=\"middle\">6 Julia</text>\n  <rect class=\"d-box\" x=\"20\" y=\"84\" width=\"130\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"85\" y=\"106\" text-anchor=\"middle\">8 Tom</text>\n  <rect class=\"d-box-warn\" x=\"20\" y=\"128\" width=\"130\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"85\" y=\"150\" text-anchor=\"middle\">2 Mark</text>\n  <text class=\"d-label-strong\" x=\"285\" y=\"28\" text-anchor=\"middle\">tickets</text>\n  <rect class=\"d-box\" x=\"220\" y=\"40\" width=\"130\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"285\" y=\"62\" text-anchor=\"middle\">t1 agent 6</text>\n  <rect class=\"d-box\" x=\"220\" y=\"84\" width=\"130\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"285\" y=\"106\" text-anchor=\"middle\">t2 agent 6</text>\n  <rect class=\"d-box\" x=\"220\" y=\"128\" width=\"130\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"285\" y=\"150\" text-anchor=\"middle\">t3 agent 8</text>\n  <path class=\"d-line\" d=\"M150 57 L220 57\"/>\n  <path class=\"d-line\" d=\"M150 57 L220 101\"/>\n  <path class=\"d-line\" d=\"M150 101 L220 145\"/>\n  <text class=\"d-label-strong\" x=\"490\" y=\"28\" text-anchor=\"middle\">INNER</text>\n  <rect class=\"d-box-success\" x=\"430\" y=\"40\" width=\"120\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"490\" y=\"62\" text-anchor=\"middle\">Julia t1</text>\n  <rect class=\"d-box-success\" x=\"430\" y=\"84\" width=\"120\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"490\" y=\"106\" text-anchor=\"middle\">Julia t2</text>\n  <rect class=\"d-box-success\" x=\"430\" y=\"128\" width=\"120\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"490\" y=\"150\" text-anchor=\"middle\">Tom t3</text>\n  <text class=\"d-label-strong\" x=\"640\" y=\"28\" text-anchor=\"middle\">LEFT</text>\n  <rect class=\"d-box-success\" x=\"580\" y=\"40\" width=\"120\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"640\" y=\"62\" text-anchor=\"middle\">Julia t1</text>\n  <rect class=\"d-box-success\" x=\"580\" y=\"84\" width=\"120\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"640\" y=\"106\" text-anchor=\"middle\">Julia t2</text>\n  <rect class=\"d-box-success\" x=\"580\" y=\"128\" width=\"120\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"640\" y=\"150\" text-anchor=\"middle\">Tom t3</text>\n  <rect class=\"d-box-warn\" x=\"580\" y=\"172\" width=\"120\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"640\" y=\"194\" text-anchor=\"middle\">Mark NULL</text>\n</svg><figcaption>INNER JOIN keeps matches; LEFT JOIN also keeps unmatched left rows</figcaption></figure>\n<h2 id=\"sec-3\" tabindex=\"-1\">ON versus WHERE in a LEFT JOIN</h2>\n<p>Now Julia narrows it: “Same list, but count only urgent tickets.” The natural edit adds <code dir=\"ltr\">AND t.priority = 'urgent'</code> to the WHERE clause, and the four non-agents disappear. Their rows have <code dir=\"ltr\">t.priority</code> NULL, the condition is NULL, and WHERE drops them. A right-table filter in WHERE turns a LEFT JOIN back into an INNER JOIN.</p>\n<p>The filter belongs in the <code dir=\"ltr\">ON</code> clause, where it decides which tickets <em>match</em>, not which rows <em>survive</em>:</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:#0550AE;--shiki-dark:#79C0FF\">  e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">first_name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</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:#0550AE;--shiki-dark:#79C0FF\">t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">ticket_id</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\"> urgent_tickets</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> employees </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> e</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">LEFT JOIN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> support_tickets </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> t</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  ON</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">agent_id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">employee_id</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> AND</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">priority</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'urgent'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">department</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Operations'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">GROUP BY</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">employee_id</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ORDER BY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> urgent_tickets </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">DESC</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre><div class=\"run-output\" data-run-output=\"2\" hidden></div></div>\n<p>Eight rows again, with zeros where they belong. The rule: conditions on the <strong>left</strong> table go in WHERE; conditions on the <strong>right</strong> table go in ON.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Choosing the join, and the starting table</h2>\n<p>A reliable way to decide: start <code dir=\"ltr\">FROM</code> the table whose rows you want in the answer, the one that matches the grain you wrote down. Then ask of every other table, “is this information required or optional?” Required information gets an inner join, because a row without it shouldn’t be in the answer. Optional information gets a LEFT JOIN, so missing matches show up as NULLs you can count, label or turn into zeros.</p>\n<p>For Julia’s list the answer is about employees, so <code dir=\"ltr\">employees</code> comes first, and tickets are optional. For Karim’s revenue the answer is about sales, so every table in the chain is required.</p>\n<p>SQLite also supports <code dir=\"ltr\">RIGHT JOIN</code> and <code dir=\"ltr\">FULL OUTER JOIN</code> (since version 3.39), and PostgreSQL has had them for decades. You’ll rarely need them: a right join is a left join with the tables swapped, and most teams write everything as LEFT JOIN so queries read the same way, top to bottom, from the main table outwards.</p>\n<p>Finally, check the row count after each join you add. If <code dir=\"ltr\">employees</code> filtered to Operations has 8 rows, the LEFT JOIN plus GROUP BY must also return 8. A number that grows or shrinks unexpectedly tells you a join is doing something you didn’t intend.</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>Forgetting the join condition</span></p>\n<p><code dir=\"ltr\">FROM orders o JOIN order_items oi</code> with no <code dir=\"ltr\">ON</code> is legal in SQLite. It pairs every order with every item, over eight million rows, and every sum becomes nonsense. If a join query is slow and its totals are enormous, look for a missing or wrong ON first.</p>\n</aside>\n<p>The category revenue had one more trap that didn’t bite, because revenue sat on the most detailed table. When a number lives on the <em>less</em> detailed side of a join, sums inflate. That’s next.</p>\n","toc":[{"id":"sec-1","text":"Following the keys","level":2},{"id":"sec-2","text":"INNER versus LEFT","level":2},{"id":"sec-3","text":"ON versus WHERE in a LEFT JOIN","level":2},{"id":"sec-4","text":"Choosing the join, and the starting table","level":2}],"takeaways":["An INNER JOIN keeps only rows that match on both sides; a LEFT JOIN keeps every row from the left table and fills unmatched right-side columns with NULL.","Give every table a short alias and qualify every column, especially names like unit_price that exist in more than one table.","A filter on the right table of a LEFT JOIN belongs in the ON clause; in WHERE it removes the NULL rows and turns the join back into an inner join.","Count a right-table column, not *, after a LEFT JOIN, so unmatched rows count as 0 instead of 1."],"further":[{"title":"SQLite SELECT, the FROM clause and joins","url":"https://www.sqlite.org/lang_select.html#fromclause"},{"title":"PostgreSQL tutorial on joins between tables","url":"https://www.postgresql.org/docs/current/tutorial-join.html"}],"quiz":[{"q":"<p><code dir=\"ltr\">orders</code> has 2,066 rows and <code dir=\"ltr\">order_items</code> has 3,991. Every order has at least one item. How many rows does <code dir=\"ltr\">orders o JOIN order_items oi ON oi.order_id = o.order_id</code> return?</p>","options":[{"text":"2,066, one per order","why":"An order with three items matches three item rows, so it appears three times. The join takes the grain of the many side."},{"text":"6,057, the two tables stacked","why":"Stacking rows is UNION ALL. A join combines columns side by side for matching rows."},{"text":"8,245,406, every combination","why":"That’s a cross join, which is what you’d get by forgetting the ON condition."},{"text":"3,991, one per order item","why":"Correct. Each item matches exactly one order, so the result has one row per item, with the order’s columns repeated."}],"answer":3},{"q":"<p>This should list every UK customer with their marketplace order count, zero included. Why do customers with no marketplace orders vanish?</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\">SELECT</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> c</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">customer_id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">COUNT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">order_id</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\"> marketplace_orders</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> customers c</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">LEFT JOIN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> orders o </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ON</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">customer_id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> c</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">customer_id</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> c</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">country</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'United Kingdom'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  AND</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">channel</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'marketplace'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">GROUP BY</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> c</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">customer_id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre></div>\n","options":[{"text":"<code dir=\"ltr\">COUNT(o.order_id)</code> should be <code dir=\"ltr\">COUNT(*)</code>.","why":"The counting expression isn’t the problem; the missing customers never reach the GROUP BY at all."},{"text":"The <code dir=\"ltr\">o.channel</code> test is NULL for customers without a matching order, so WHERE removes them.","why":"Correct. Move <code dir=\"ltr\">AND o.channel = 'marketplace'</code> into the ON clause; the country test stays in WHERE because it’s about the left table."},{"text":"LEFT JOIN only keeps unmatched rows from the right table.","why":"It’s the other way round. LEFT JOIN preserves every row of the left table, here <code dir=\"ltr\">customers</code>."}],"answer":1},{"q":"<p><code dir=\"ltr\">SELECT unit_price FROM order_items oi JOIN products p ON p.product_id = oi.product_id</code> fails. Why?</p>","options":[{"text":"<code dir=\"ltr\">unit_price</code> exists in both tables, so the name is ambiguous.","why":"Correct. Write <code dir=\"ltr\">oi.unit_price</code> (the price charged) or <code dir=\"ltr\">p.unit_price</code> (the list price). They’re different numbers after the March 2025 price rise."},{"text":"You can’t select a column that was used in a join.","why":"Any column can be selected; the join key isn’t involved here anyway."},{"text":"Aliases must be declared with AS.","why":"AS is optional for table aliases in SQLite and PostgreSQL."}],"answer":0},{"q":"<p>After <code dir=\"ltr\">employees e LEFT JOIN support_tickets t ON t.agent_id = e.employee_id</code>, which expression gives 0 tickets for employees who handled none?</p>","options":[{"text":"<code dir=\"ltr\">COUNT(*)</code>","why":"An unmatched employee still produces one row (with NULL ticket columns), so COUNT(*) returns 1."},{"text":"<code dir=\"ltr\">COUNT(e.employee_id)</code>","why":"The employee id is never NULL, so this also counts the unmatched row as 1."},{"text":"<code dir=\"ltr\">COUNT(t.ticket_id)</code>","why":"Correct. The ticket id is NULL on the unmatched row, and COUNT skips NULLs."},{"text":"<code dir=\"ltr\">SUM(t.ticket_id)</code>","why":"That adds up id numbers, which is meaningless, and returns NULL for unmatched employees."}],"answer":2}],"exercise":{"kind":"sql","starter":"-- Every Jordan customer with their 2025 orders (zero included)\nSELECT\n  c.customer_id,\n  c.first_name,\n  COUNT(o.order_id) AS orders_2025\nFROM customers AS c\nLEFT JOIN orders AS o\n  ON o.customer_id = c.customer_id\nWHERE c.country = 'Jordan'\n  AND o.status <> 'cancelled'\n  AND o.order_date >= '2025-01-01'\n  AND o.order_date < '2026-01-01'\nGROUP BY c.customer_id;","solution":"SELECT\n  c.customer_id,\n  c.first_name,\n  COUNT(o.order_id) AS orders_2025\nFROM customers AS c\nLEFT JOIN orders AS o\n  ON o.customer_id = c.customer_id\n AND o.status <> 'cancelled'\n AND o.order_date >= '2025-01-01'\n AND o.order_date < '2026-01-01'\nWHERE c.country = 'Jordan'\nGROUP BY c.customer_id, c.first_name;","orderMatters":false,"title":"Every Jordan customer, zeros included","prompt":"<p>Huda is planning a win-back email for Jordan and needs <strong>every customer in Jordan</strong> with the number of <strong>non-cancelled orders they placed in 2025</strong>, showing 0 for customers who didn’t order.</p>\n<p>Return <code dir=\"ltr\">customer_id</code>, <code dir=\"ltr\">first_name</code> and <code dir=\"ltr\">orders_2025</code>. Row order doesn’t matter. Jordan has 32 customers, so expect 32 rows; the starter returns fewer.</p>\n","hints":["Which customers are missing from the starter’s result? What are their <code dir=\"ltr\">o.status</code> and <code dir=\"ltr\">o.order_date</code> values after the LEFT JOIN?","Conditions on the right table (<code dir=\"ltr\">orders</code>) decide which orders match. They belong in the ON clause.","Keep <code dir=\"ltr\">c.country = 'Jordan'</code> in WHERE (it’s about the left table) and move the status and date tests into <code dir=\"ltr\">ON ... AND ...</code>."],"explanation":"<p>For a Jordan customer with no 2025 orders, the LEFT JOIN produces one row with every <code dir=\"ltr\">o.</code> column NULL. In the starter, <code dir=\"ltr\">o.status &lt;&gt; 'cancelled'</code> is NULL for that row, so WHERE throws it away and the customer disappears. Moving the order conditions into ON means they only decide which orders attach; the customer row survives either way, and <code dir=\"ltr\">COUNT(o.order_id)</code> counts 0 for it. Nine customers had no 2025 orders, exactly the ones a win-back email is for.</p>\n","checkLabels":{}},"words":775,"readMinutes":4}