{"id":"l-sql-3-4","courseId":"sql-for-analysts","lang":"en","kind":"lesson","summary":"Build a conversion funnel from session events with conditional aggregation, then group customers into monthly cohorts by first order and measure how many come back in later months.","html":"<p>Two requests sit at the top of the queue. Huda: “Our paid search budget is up for review. Which traffic sources actually turn visits into orders, and where do people drop off?” Rania: “We acquired a lot of customers in Black Friday months. Do they come back as often as everyone else?” The first is a <strong>funnel</strong>, the second a <strong>cohort retention</strong> analysis. Both are built from things you already know: conditional aggregation, dates and CTEs.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">A funnel from session events</h2>\n<p>Each row of <code dir=\"ltr\">web_sessions</code> is one visit, with flags for the steps it reached: <code dir=\"ltr\">added_to_cart</code> and <code dir=\"ltr\">purchased</code> (0 or 1). Summing a 0/1 flag counts the sessions that reached the step, so a funnel is one GROUP BY:</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:#1F2328;--shiki-dark:#E6EDF3\">  source,</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:#CF222E;--shiki-dark:#FF7B72\"> sessions</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  SUM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(added_to_cart)  </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> carts,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  SUM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(purchased)      </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> purchases,</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\">100</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> SUM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(added_to_cart) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</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:#0550AE;--shiki-dark:#79C0FF\">1</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\"> cart_rate,</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\">100</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> SUM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(purchased) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> NULLIF</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\">(added_to_cart), </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\">1</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\"> checkout_rate,</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\">100</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> SUM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(purchased) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</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:#0550AE;--shiki-dark:#79C0FF\">1</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\"> conversion</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> web_sessions</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> started_at </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\">GROUP BY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> source</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ORDER BY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> conversion </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>Email converts best overall, 10.9% of sessions, and referral worst at 7.1%. The step rates tell the more useful story. Referral visitors rarely add to cart (13.3%), but those who do check out more often than anyone (53.6%). Email is the reverse. Those are different problems with different fixes: referral needs better landing pages, email needs a smoother checkout.</p>\n<p>That’s why a funnel reports <strong>step rates</strong>, each step divided by the previous one, not only end-to-end conversion. Make each denominator explicit in the column name; “conversion rate” without a denominator is the most argued-about number in any marketing meeting. <code dir=\"ltr\">NULLIF</code> guards the division for a source with no carts.</p>\n<aside class=\"callout callout-tip\" 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=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Check the funnel is a funnel</span></p>\n<p>Each step should be a subset of the previous one. Before reporting, run <code dir=\"ltr\">SELECT COUNT(*) FROM web_sessions WHERE purchased = 1 AND added_to_cart = 0</code>. On Cartwheel’s data it’s 0. If it weren’t, a purchase without a cart would be a tracking bug, and your step rates could exceed 100%.</p>\n</aside>\n<p>Swap <code dir=\"ltr\">source</code> for <code dir=\"ltr\">device</code> and a sharper finding appears: desktop sessions that reach the cart check out 56% of the time, mobile sessions only 37%. Mobile brings the most traffic, so that gap is worth more than any change to the ad budget.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Cohorts: grouping customers by when they started</h2>\n<p>A <strong>cohort</strong> is a group of customers who share a starting period. Here, the month of their first non-cancelled order. Retention then asks: of the customers who started in month M, what share ordered again 1, 2, 3 months later?</p>\n<p>Three steps, three CTEs. First, give every order a <strong>month index</strong>, <code dir=\"ltr\">year * 12 + month</code>, so that month differences are simple subtraction even across a year boundary. Second, find each customer’s first month. Third, list each customer’s active months relative to their cohort:</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\">WITH</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> orders_ok </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  SELECT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> customer_id, order_date,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">         CAST</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(strftime(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'%Y'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, order_date) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> INTEGER</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 12</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">           +</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> CAST</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(strftime(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'%m'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, order_date) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> INTEGER</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\"> month_index</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\">  WHERE</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> 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:#1F2328;--shiki-dark:#E6EDF3\">),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">firsts </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  -- grain: one row per customer</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  SELECT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> customer_id,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">         MIN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(month_index) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cohort_index,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">         strftime(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'%Y-%m'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">MIN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(order_date)) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cohort</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> orders_ok</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  GROUP BY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> customer_id</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">activity </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  -- grain: one row per customer per active month</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  SELECT DISTINCT</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> f</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">cohort</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">f</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">cohort_index</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\">customer_id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">         o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">month_index</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> -</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> f</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">cohort_index</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> month_number</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> orders_ok </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\"> firsts </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> f </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ON</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> f</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\"> o</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:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  cohort,</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\">FILTER</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> month_number </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</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\"> customers,</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\">100</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</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\">FILTER</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> month_number </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        /</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\">FILTER</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> month_number </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</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\">1</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\"> m1_pct,</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  CASE</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> WHEN</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> MAX</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(cohort_index) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x3C;=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 2025</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 12</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> +</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 12</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> THEN</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\">100</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</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\">FILTER</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> month_number </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">          /</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\">FILTER</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> month_number </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</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\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</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\"> m2_pct</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> activity</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cohort </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">>=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '2025-07'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">GROUP BY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cohort</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ORDER BY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cohort;</span></span></code></pre><div class=\"run-output\" data-run-output=\"1\" hidden></div></div>\n<p>The final SELECT pivots month numbers into columns with <code dir=\"ltr\">FILTER</code>. The <code dir=\"ltr\">DISTINCT</code> in <code dir=\"ltr\">activity</code> is essential: retention counts <em>people</em>, and a customer who orders three times in a month is one active customer.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 720 230\" role=\"img\" aria-labelledby=\"l-sql-3-4-t1\"><defs><marker id=\"arrow-l-sql-3-4-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-3-4-t1\">Cohorts from September to December 2025 against months 0 to 3. September has all four months measured; October three; November two; December only month 0. The cells after the end of the data are unknown, not zero.</title>\n  <text class=\"d-label-muted\" x=\"250\" y=\"30\" text-anchor=\"middle\">m0</text>\n  <text class=\"d-label-muted\" x=\"370\" y=\"30\" text-anchor=\"middle\">m1</text>\n  <text class=\"d-label-muted\" x=\"490\" y=\"30\" text-anchor=\"middle\">m2</text>\n  <text class=\"d-label-muted\" x=\"610\" y=\"30\" text-anchor=\"middle\">m3</text>\n  <text class=\"d-code\" x=\"110\" y=\"68\" text-anchor=\"middle\">2025-09</text>\n  <text class=\"d-code\" x=\"110\" y=\"113\" text-anchor=\"middle\">2025-10</text>\n  <text class=\"d-code\" x=\"110\" y=\"158\" text-anchor=\"middle\">2025-11</text>\n  <text class=\"d-code\" x=\"110\" y=\"203\" text-anchor=\"middle\">2025-12</text>\n  <rect class=\"d-box-primary\" x=\"195\" y=\"45\" width=\"110\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"250\" y=\"68\" text-anchor=\"middle\">100%</text>\n  <rect class=\"d-box-accent\" x=\"315\" y=\"45\" width=\"110\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"370\" y=\"68\" text-anchor=\"middle\">43.5%</text>\n  <rect class=\"d-box-accent\" x=\"435\" y=\"45\" width=\"110\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"490\" y=\"68\" text-anchor=\"middle\">39.1%</text>\n  <rect class=\"d-box-accent\" x=\"555\" y=\"45\" width=\"110\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"610\" y=\"68\" text-anchor=\"middle\">30.4%</text>\n  <rect class=\"d-box-primary\" x=\"195\" y=\"90\" width=\"110\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"250\" y=\"113\" text-anchor=\"middle\">100%</text>\n  <rect class=\"d-box-accent\" x=\"315\" y=\"90\" width=\"110\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"370\" y=\"113\" text-anchor=\"middle\">44.4%</text>\n  <rect class=\"d-box-accent\" x=\"435\" y=\"90\" width=\"110\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"490\" y=\"113\" text-anchor=\"middle\">27.8%</text>\n  <rect class=\"d-box d-dashed\" x=\"555\" y=\"90\" width=\"110\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-label-muted\" x=\"610\" y=\"113\" text-anchor=\"middle\">not yet</text>\n  <rect class=\"d-box-primary\" x=\"195\" y=\"135\" width=\"110\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"250\" y=\"158\" text-anchor=\"middle\">100%</text>\n  <rect class=\"d-box-accent\" x=\"315\" y=\"135\" width=\"110\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"370\" y=\"158\" text-anchor=\"middle\">36.2%</text>\n  <rect class=\"d-box d-dashed\" x=\"435\" y=\"135\" width=\"110\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-label-muted\" x=\"490\" y=\"158\" text-anchor=\"middle\">not yet</text>\n  <rect class=\"d-box d-dashed\" x=\"555\" y=\"135\" width=\"110\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-label-muted\" x=\"610\" y=\"158\" text-anchor=\"middle\">not yet</text>\n  <rect class=\"d-box-primary\" x=\"195\" y=\"180\" width=\"110\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"250\" y=\"203\" text-anchor=\"middle\">100%</text>\n  <rect class=\"d-box d-dashed\" x=\"315\" y=\"180\" width=\"110\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-label-muted\" x=\"370\" y=\"203\" text-anchor=\"middle\">not yet</text>\n  <rect class=\"d-box d-dashed\" x=\"435\" y=\"180\" width=\"110\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-label-muted\" x=\"490\" y=\"203\" text-anchor=\"middle\">not yet</text>\n  <rect class=\"d-box d-dashed\" x=\"555\" y=\"180\" width=\"110\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-label-muted\" x=\"610\" y=\"203\" text-anchor=\"middle\">not yet</text>\n</svg><figcaption>A cohort table is a triangle: recent cohorts haven’t lived long enough to measure</figcaption></figure>\n<h2 id=\"sec-3\" tabindex=\"-1\">Unknown is not zero</h2>\n<p>Look at the December 2025 row in the query’s output: <code dir=\"ltr\">m1_pct</code> is 0.0. That isn’t churn. Month 1 for December’s cohort is January 2026, and the data ends on 31 December. The <code dir=\"ltr\">m2_pct</code> column shows the fix: a CASE expression checks whether the cohort has lived through that month (<code dir=\"ltr\">cohort_index + 2</code> is still within December 2025) and returns NULL otherwise. A retention chart with honest blanks is a triangle; one with zeros shows a fake collapse in every recent cohort.</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>Counting orders instead of customers</span></p>\n<p>Drop the DISTINCT from <code dir=\"ltr\">activity</code> and a single loyal customer with four orders in October adds 4 to October’s “active customers”. Retention climbs, sometimes past 100%. Whenever a rate is about people, check that its numerator and denominator are counts of distinct people, at the same grain.</p>\n</aside>\n<p>Back to Rania’s question. Change the filter to 2024 and the November 2024 cohort, Cartwheel’s first Black Friday crowd and the largest of that year at 35 customers, retains only 14.3% in month 1, below most 2024 cohorts. Deep-discount customers tend to come for the deal. That’s an answer she can use when planning the next promotion. The exercise asks a related question: what share of each cohort comes back at all within three months?</p>\n<p>This closes the analytical toolkit. The final section is about trust: making sure the data and your queries deserve it.</p>\n","toc":[{"id":"sec-1","text":"A funnel from session events","level":2},{"id":"sec-2","text":"Cohorts: grouping customers by when they started","level":2},{"id":"sec-3","text":"Unknown is not zero","level":2}],"takeaways":["A funnel counts how many units reach each step and divides each step by the one before it, so you can see exactly where people drop off.","Always state the denominator of a rate; checkout rate (purchases per cart) and conversion rate (purchases per session) answer different questions.","A cohort groups customers by the period of their first event, and retention counts distinct customers active N periods later divided by the cohort size.","Months a recent cohort hasn’t lived through yet are unknown, not zero; show them as NULL so nobody reads them as churn."],"further":[{"title":"SQLite aggregate functions and FILTER","url":"https://www.sqlite.org/lang_aggfunc.html"},{"title":"SQLite date and time functions","url":"https://www.sqlite.org/lang_datefunc.html"}],"quiz":[{"q":"<p>Email sessions: 404 sessions, 107 add to cart, 44 purchase. Which number is the checkout rate?</p>","options":[{"text":"10.9%, purchases divided by sessions","why":"That’s the overall conversion rate. It mixes two steps and hides where people drop off."},{"text":"26.5%, carts divided by sessions","why":"That’s the cart rate, the first step of the funnel."},{"text":"73.5%, sessions that didn’t add to cart","why":"That’s the drop-off at the first step, not a checkout measure."},{"text":"41.1%, purchases divided by carts","why":"Correct. Checkout rate measures the step from cart to purchase, so its denominator is carts."}],"answer":3},{"q":"<p>Why does the cohort query use <code dir=\"ltr\">SELECT DISTINCT cohort, customer_id, month_number</code> before counting?</p>","options":[{"text":"So a customer who orders three times in one month counts as one active customer, not three.","why":"Correct. Retention is about people. Without DISTINCT, frequent buyers inflate the active count and can push retention above 100%."},{"text":"Because COUNT doesn’t work without DISTINCT in a CTE.","why":"COUNT works anywhere; DISTINCT is about the grain of the activity table, not syntax."},{"text":"To sort the cohorts in chronological order.","why":"DISTINCT removes duplicates; it doesn’t promise any order."}],"answer":0},{"q":"<p>The December 2025 cohort shows 0% retention in month 1. The data ends on 2025-12-31. What should the table show?</p>","options":[{"text":"0%, because none of them ordered in month 1","why":"Month 1 for the December cohort is January 2026, which isn’t in the data. Zero would claim they all churned."},{"text":"The same value as the November cohort, as an estimate.","why":"Filling in another cohort’s value invents data and hides that the period hasn’t happened."},{"text":"NULL or blank, because that month hasn’t been observed yet.","why":"Correct. Unobserved cells are unknown. A CASE expression that checks whether the cohort month plus N is within the data range makes this explicit."}],"answer":2},{"q":"<p>To compute “months since first order” across a year boundary, why convert dates to <code dir=\"ltr\">year * 12 + month</code> instead of subtracting <code dir=\"ltr\">strftime('%m', ...)</code> values?</p>","options":[{"text":"strftime can’t return the month.","why":"It can, with ‘%m’. The problem is what happens when you subtract months from different years."},{"text":"December 2024 to January 2025 is 1 month apart, but 1 - 12 gives -11; a single month index makes the difference correct.","why":"Correct. Turning each month into one increasing number makes month differences simple subtraction, across any year boundary."},{"text":"It’s faster to compute.","why":"Speed isn’t the issue; the subtraction of plain month numbers is wrong across years."}],"answer":1}],"exercise":{"kind":"sql","starter":"-- Share of each 2025 cohort that ordered again within 3 months\nWITH orders_ok AS (\n  SELECT customer_id,\n         order_date,\n         CAST(strftime('%Y', order_date) AS INTEGER) * 12\n           + CAST(strftime('%m', order_date) AS INTEGER) AS month_index\n  FROM orders\n  WHERE status <> 'cancelled'\n),\nfirsts AS (\n  SELECT customer_id,\n         MIN(month_index) AS cohort_index,\n         strftime('%Y-%m', MIN(order_date)) AS cohort\n  FROM orders_ok\n  GROUP BY customer_id\n),\nactivity AS (\n  SELECT f.cohort, o.customer_id, o.month_index - f.cohort_index AS month_number\n  FROM orders_ok AS o\n  JOIN firsts AS f ON f.customer_id = o.customer_id\n)\nSELECT\n  cohort,\n  COUNT(*) AS customers,\n  ROUND(100.0 * COUNT(*) FILTER (WHERE month_number BETWEEN 1 AND 3)\n        / COUNT(*), 1) AS repeat_3m_pct\nFROM activity\nWHERE cohort BETWEEN '2025-01' AND '2025-09'\nGROUP BY cohort\nORDER BY cohort;","solution":"WITH orders_ok AS (\n  SELECT customer_id,\n         order_date,\n         CAST(strftime('%Y', order_date) AS INTEGER) * 12\n           + CAST(strftime('%m', order_date) AS INTEGER) AS month_index\n  FROM orders\n  WHERE status <> 'cancelled'\n),\nfirsts AS (\n  SELECT customer_id,\n         MIN(month_index) AS cohort_index,\n         strftime('%Y-%m', MIN(order_date)) AS cohort\n  FROM orders_ok\n  GROUP BY customer_id\n),\nactivity AS (\n  SELECT f.cohort, o.customer_id, o.month_index - f.cohort_index AS month_number\n  FROM orders_ok AS o\n  JOIN firsts AS f ON f.customer_id = o.customer_id\n)\nSELECT\n  cohort,\n  COUNT(DISTINCT customer_id) AS customers,\n  ROUND(100.0 * COUNT(DISTINCT customer_id) FILTER (WHERE month_number BETWEEN 1 AND 3)\n        / COUNT(DISTINCT customer_id), 1) AS repeat_3m_pct\nFROM activity\nWHERE cohort BETWEEN '2025-01' AND '2025-09'\nGROUP BY cohort\nORDER BY cohort;","orderMatters":true,"title":"Three-month repeat rate by cohort","prompt":"<p>Rania wants one number per cohort: of the customers whose <strong>first order</strong> was in a given month, what percentage ordered again <strong>in any of the next three months</strong> (month numbers 1, 2 or 3)?</p>\n<p>Return <code dir=\"ltr\">cohort</code> (‘YYYY-MM’), <code dir=\"ltr\">customers</code> (cohort size) and <code dir=\"ltr\">repeat_3m_pct</code> (rounded to 1 decimal) for the cohorts from 2025-01 to 2025-09, which have all three months observed. Sort by <code dir=\"ltr\">cohort</code>. Order matters.</p>\n<p>The CTEs are correct. The final SELECT counts the wrong thing.</p>\n","hints":["What is the grain of <code dir=\"ltr\">activity</code>? One row per order, so a customer with five orders appears five times.","Both the cohort size and the repeat count are about people. Count <code dir=\"ltr\">DISTINCT customer_id</code>, not rows.","<code dir=\"ltr\">COUNT(DISTINCT customer_id) FILTER (WHERE month_number BETWEEN 1 AND 3)</code> counts each returning customer once, however many times they ordered."],"explanation":"<p><code dir=\"ltr\">activity</code> has one row per order, so <code dir=\"ltr\">COUNT(*)</code> counted orders: the cohort size included every order its customers ever placed, and a customer who ordered in months 1, 2 and 3 counted three times in the numerator. Counting distinct customers in both places puts numerator and denominator at the same grain, people. Repeat rates range from about 31% (February) to 67% (June); with cohorts of 16 to 29 customers, a difference of one or two people moves the rate by several points, so report the cohort size alongside it.</p>\n","checkLabels":{}},"words":701,"readMinutes":3}