{"id":"l-sql-1-4","courseId":"sql-for-analysts","lang":"en","kind":"lesson","summary":"Filter, group and measure time in SQLite using ISO date text, half-open date ranges, strftime() for date parts and julianday() for durations, without losing the last day of a period.","html":"<p>Mark Evans, VP Operations, is preparing a November review. He wants two numbers: how many orders Cartwheel took in November 2025, and how quickly support resolved tickets. Both are date questions, and SQLite handles dates differently from most databases. Once you see how, it’s simple and predictable.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Dates are text, and that’s fine</h2>\n<p>SQLite has no separate date type. Cartwheel stores <code dir=\"ltr\">order_date</code> as ISO-8601 text: <code dir=\"ltr\">'2025-11-28 19:55:30'</code>. Because the format runs from largest unit to smallest, alphabetical order <em>is</em> chronological order. Comparing strings compares moments in time, and an index on the column works for date ranges.</p>\n<p>Here’s the obvious November query:</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\"> november_orders</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:#1F2328;--shiki-dark:#E6EDF3\"> order_date </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">BETWEEN</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '2025-11-01'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> AND</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '2025-11-30'</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>336 orders. It’s wrong by 11.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 720 200\" role=\"img\" aria-labelledby=\"l-sql-1-4-t1\"><defs><marker id=\"arrow-l-sql-1-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-1-4-t1\">A timeline from 29 November to 1 December. BETWEEN '2025-11-01' AND '2025-11-30' stops at midnight at the start of 30 November, missing an order at 19:55 that day. The half-open range, less than '2025-12-01', covers all of 30 November.</title>\n  <path class=\"d-line\" d=\"M40 90 L680 90\"/>\n  <path class=\"d-line\" d=\"M120 80 L120 100\"/>\n  <path class=\"d-line\" d=\"M360 80 L360 100\"/>\n  <path class=\"d-line\" d=\"M600 80 L600 100\"/>\n  <text class=\"d-code\" x=\"120\" y=\"125\" text-anchor=\"middle\">11-29</text>\n  <text class=\"d-code\" x=\"360\" y=\"125\" text-anchor=\"middle\">11-30 00:00</text>\n  <text class=\"d-code\" x=\"600\" y=\"125\" text-anchor=\"middle\">12-01 00:00</text>\n  <circle class=\"d-dot\" cx=\"560\" cy=\"90\" r=\"7\"/>\n  <text class=\"d-label\" x=\"560\" y=\"70\" text-anchor=\"middle\">order 19:55</text>\n  <rect class=\"d-box-warn\" x=\"40\" y=\"140\" width=\"320\" height=\"26\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"200\" y=\"158\" text-anchor=\"middle\">BETWEEN … AND '2025-11-30'</text>\n  <rect class=\"d-box-success\" x=\"40\" y=\"170\" width=\"560\" height=\"26\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"320\" y=\"188\" text-anchor=\"middle\">&gt;= '2025-11-01' AND &lt; '2025-12-01'</text>\n  <rect class=\"d-box-primary\" x=\"40\" y=\"20\" width=\"200\" height=\"30\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"140\" y=\"40\" text-anchor=\"middle\">time runs right</text>\n</svg><figcaption>A date-only upper bound means midnight at the start of the last day</figcaption></figure>\n<p><code dir=\"ltr\">'2025-11-30'</code> is shorter than <code dir=\"ltr\">'2025-11-30 19:55:30'</code>, and a shorter string that matches the start sorts first. So the upper bound is effectively midnight at the <em>start</em> of 30 November, and everything later that day falls outside. The fix is a half-open range: include the start, exclude the start of the next period.</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\"> november_orders</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:#1F2328;--shiki-dark:#E6EDF3\"> order_date </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">>=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '2025-11-01'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  AND</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> order_date </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  '2025-12-01'</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>347, the real number. On the busiest days of the Black Friday weekend, those 11 missing orders would be a visible hole in Mark’s chart.</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>BETWEEN on datetimes</span></p>\n<p><code dir=\"ltr\">BETWEEN '2025-11-01' AND '2025-11-30'</code> looks right and is quietly wrong whenever the column holds a time. Make half-open ranges your default for every period filter: they work for days, months and years, with or without times, and in every database.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">Pulling dates apart</h2>\n<p><code dir=\"ltr\">strftime(format, value)</code> returns parts of a date as <strong>text</strong>:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Format</th>\n<th>Returns</th>\n<th>Example</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code dir=\"ltr\">'%Y-%m'</code></td>\n<td>year and month</td>\n<td><code dir=\"ltr\">'2025-11'</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">'%Y'</code></td>\n<td>year</td>\n<td><code dir=\"ltr\">'2025'</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">'%w'</code></td>\n<td>weekday, 0 = Sunday</td>\n<td><code dir=\"ltr\">'5'</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">'%H'</code></td>\n<td>hour, 00–23</td>\n<td><code dir=\"ltr\">'19'</code></td>\n</tr>\n</tbody>\n</table></div><p><code dir=\"ltr\">'%Y-%m'</code> is the workhorse: it turns a timestamp into a month label you can group by in the next lesson. Because the result is text, compare it with text: <code dir=\"ltr\">strftime('%w', order_date) = '0'</code> finds Sunday orders, while <code dir=\"ltr\">= 0</code> finds nothing.</p>\n<p><code dir=\"ltr\">date()</code> and <code dir=\"ltr\">datetime()</code> normalise a value and accept <strong>modifiers</strong>, which make period boundaries easy:</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:#CF222E;--shiki-dark:#FF7B72\">  date</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'2025-11-28 19:55:30'</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\"> just_the_date,</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  date</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'2025-11-28'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'start of month'</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_start,</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  date</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'2025-11-28'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'start of month'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'+1 month'</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\"> next_month_start,</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  date</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'2025-11-28'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'start of month'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'+1 month'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'-1 day'</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_end,</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  date</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'2025-11-28'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'-6 days'</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\"> week_ago;</span></span></code></pre><div class=\"run-output\" data-run-output=\"2\" hidden></div></div>\n<p>Modifiers apply left to right. <code dir=\"ltr\">'start of month', '+1 month'</code> is exactly the exclusive upper bound a half-open range needs.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Rolling windows and weekdays</h2>\n<p>“Orders in the last 30 days” is a rolling window, and it hides an off-by-one question: does “last 30 days” as of 31 December include the 31st? Usually yes, which means the window starts on 2 December, 29 days earlier. Build both bounds from the anchor date so the window moves with it:</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 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\"> orders_last_30_days</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:#1F2328;--shiki-dark:#E6EDF3\"> order_date </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">>=</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> date</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'2025-12-31'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'-29 days'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  AND</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> order_date </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  date</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'2025-12-31'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'+1 day'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span></code></pre><div class=\"run-output\" data-run-output=\"3\" hidden></div></div>\n<p>189 orders. The upper bound is the start of the day <em>after</em> the anchor, the same half-open idea as before. Write the window’s first and last day in a comment above the query; whoever reviews it will check exactly that.</p>\n<p>Weekday analysis combines <code dir=\"ltr\">strftime('%w', ...)</code> with the CASE expressions from the previous lesson. Mapping ‘0’ and ‘6’ to weekend labels is a common first cut when marketing asks whether weekend campaigns are worth it:</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\">  order_date,</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  CASE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> strftime(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'%w'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, order_date)</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    WHEN</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '0'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> THEN</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'weekend'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    WHEN</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '6'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> THEN</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'weekend'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    ELSE</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'weekday'</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\"> day_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_date </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">DESC</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">LIMIT</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 5</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>This CASE uses the <em>simple</em> form, <code dir=\"ltr\">CASE value WHEN x THEN ...</code>, which compares one value against a list. It’s handy for mappings like this one; use the searched form, <code dir=\"ltr\">CASE WHEN condition THEN ...</code>, whenever a branch needs a real condition.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Measuring durations</h2>\n<p><code dir=\"ltr\">julianday()</code> converts a date to a number of days (with fractions for the time). Subtract two and you have a duration in days; multiply by 24 for hours. Here are December’s urgent tickets with their resolution times:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"sql\" data-run=\"sql\" data-index=\"5\"><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=\"5\" 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\">  ticket_id,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  opened_at,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  closed_at,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  ROUND</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((julianday(closed_at) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> julianday(opened_at)) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 24</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\"> hours_to_close</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\"> 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\">  AND</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> opened_at </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">>=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '2025-12-01'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ORDER BY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> opened_at;</span></span></code></pre><div class=\"run-output\" data-run-output=\"5\" hidden></div></div>\n<p>Ticket 691 has no <code dir=\"ltr\">closed_at</code>, so its duration is NULL. That’s correct: it isn’t zero hours, it’s unfinished, and any average you compute later will skip it rather than flatter the team.</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>Pick an “as of” date</span></p>\n<p><code dir=\"ltr\">julianday('now')</code> changes every second, so a tenure or “days since last order” metric drifts each time it’s run. Cartwheel’s data ends on 2025-12-31; write <code dir=\"ltr\">julianday('2025-12-31')</code> (or a single CTE holding the as-of date, as you’ll see later) so results are reproducible.</p>\n</aside>\n<p>You can now filter and measure time. Mark’s other question, average resolution time <em>per priority</em>, needs one more idea: collapsing many rows into one per group. That’s next.</p>\n","toc":[{"id":"sec-1","text":"Dates are text, and that’s fine","level":2},{"id":"sec-2","text":"Pulling dates apart","level":2},{"id":"sec-3","text":"Rolling windows and weekdays","level":2},{"id":"sec-4","text":"Measuring durations","level":2}],"takeaways":["SQLite stores dates as ISO-8601 text, which sorts chronologically, so plain string comparison works as a date filter.","Filter a period with a half-open range (&gt;= start AND &lt; next start); BETWEEN with a date-only upper bound drops everything after midnight on the last day.","strftime() extracts parts like the month (‘%Y-%m’), weekday (‘%w’, 0 = Sunday) or hour (‘%H’) as text.","julianday(later) - julianday(earlier) gives a duration in fractional days; multiply by 24 for hours.","Anchor “as of” calculations on a fixed date instead of ‘now’ so the same query gives the same answer tomorrow."],"further":[{"title":"SQLite date and time functions","url":"https://www.sqlite.org/lang_datefunc.html"},{"title":"PostgreSQL date/time functions and operators","url":"https://www.postgresql.org/docs/current/functions-datetime.html"}],"quiz":[{"q":"<p><code dir=\"ltr\">order_date</code> holds values like ‘2025-11-30 19:55:30’. Which filter returns every order placed in November 2025?</p>","options":[{"text":"<code dir=\"ltr\">order_date BETWEEN '2025-11-01' AND '2025-11-30'</code>","why":"‘2025-11-30 19:55:30’ sorts after ‘2025-11-30’, so every order on 30 November after midnight is dropped."},{"text":"<code dir=\"ltr\">order_date &gt;= '2025-11-01' AND order_date &lt; '2025-12-01'</code>","why":"Correct. A half-open range includes all of the last day, whatever the time, and works the same for any period length."},{"text":"<code dir=\"ltr\">order_date &gt; '2025-11-01' AND order_date &lt; '2025-11-30'</code>","why":"This loses all of 30 November: every ‘2025-11-30 …’ value sorts after ‘2025-11-30’, so <code dir=\"ltr\">&lt; '2025-11-30'</code> rejects it. The first day survives only because times sort after the bare date."},{"text":"<code dir=\"ltr\">order_date = '2025-11'</code>","why":"Equality compares the full text, and no value is exactly ‘2025-11’."}],"answer":1},{"q":"<p>What does <code dir=\"ltr\">strftime('%m', '2025-03-09 14:00:00')</code> return?</p>","options":[{"text":"The number 3","why":"strftime always returns text. That’s why comparing it with the integer 3 fails; compare with ‘03’."},{"text":"The text ‘03’","why":"Correct. ‘%m’ is the two-digit month as text. Use <code dir=\"ltr\">CAST(... AS INTEGER)</code> if you need a number."},{"text":"The text ‘March’","why":"SQLite’s strftime has no month-name format; you’d build names with a CASE expression."}],"answer":1},{"q":"<p>A ticket opened at ‘2025-12-04 17:56:18’ and closed at ‘2025-12-05 01:56:18’. What does <code dir=\"ltr\">(julianday(closed_at) - julianday(opened_at)) * 24</code> return?</p>","options":[{"text":"1, because the dates are one day apart","why":"julianday keeps the time of day, so the difference is a fraction of a day, not a calendar-day count."},{"text":"0.33","why":"That’s the difference in days (8 / 24). Multiplying by 24 converts it to hours."},{"text":"8.0","why":"Correct. The difference is a third of a day, and times 24 that’s 8 hours."},{"text":"An error, because you can’t subtract text","why":"julianday() converts the text to a number of days first, so the subtraction is numeric."}],"answer":2},{"q":"<p>You compute customer tenure with <code dir=\"ltr\">julianday('now') - julianday(signup_date)</code>. Why might a colleague object?</p>","options":[{"text":"julianday() doesn’t accept ‘now’.","why":"It does; ‘now’ is a valid time value in every SQLite date function."},{"text":"The result changes every day, so last week’s report can’t be reproduced.","why":"Correct. For a dataset that ends on 2025-12-31, anchor on that date (or a parameter) so the number means the same thing every time it’s run."},{"text":"Tenure must be computed in months, not days.","why":"The unit is a reporting choice; days are fine and easy to convert."}],"answer":1}],"exercise":{"kind":"sql","starter":"-- November 2025 tickets and how long they took to close\nSELECT\n  ticket_id,\n  priority,\n  ROUND(julianday(closed_at) - julianday(opened_at), 1) AS hours_to_close\nFROM support_tickets\nWHERE opened_at BETWEEN '2025-11-01' AND '2025-11-30'\nORDER BY ticket_id;","solution":"SELECT\n  ticket_id,\n  priority,\n  ROUND((julianday(closed_at) - julianday(opened_at)) * 24, 1) AS hours_to_close\nFROM support_tickets\nWHERE opened_at >= '2025-11-01'\n  AND opened_at < '2025-12-01'\nORDER BY ticket_id;","orderMatters":true,"title":"November resolution times","prompt":"<p>Mark wants every ticket <strong>opened in November 2025</strong> with its resolution time in <strong>hours</strong>.</p>\n<p>Return <code dir=\"ltr\">ticket_id</code>, <code dir=\"ltr\">priority</code> and <code dir=\"ltr\">hours_to_close</code> (rounded to 1 decimal; leave it NULL for tickets that are still open), sorted by <code dir=\"ltr\">ticket_id</code>. Order matters.</p>\n<p>The starter has two bugs: one drops tickets, the other gets the unit wrong.</p>\n","hints":["Compare the starter’s row count with a count using <code dir=\"ltr\">opened_at &lt; '2025-12-01'</code>. Which day goes missing?","Replace BETWEEN with a half-open range: <code dir=\"ltr\">&gt;= '2025-11-01' AND &lt; '2025-12-01'</code>.","<code dir=\"ltr\">julianday()</code> differences are in days. Multiply the difference by 24 before rounding."],"explanation":"<p>With BETWEEN, the upper bound ‘2025-11-30’ sorts before any time on 30 November, so tickets opened that day vanish: 112 rows instead of 116. The half-open range keeps them. <code dir=\"ltr\">julianday(closed_at) - julianday(opened_at)</code> is a fraction of a day, so multiplying by 24 gives hours. Open tickets have a NULL <code dir=\"ltr\">closed_at</code>, and the arithmetic passes that NULL through, which is the honest answer for an unfinished ticket.</p>\n","checkLabels":{}},"words":682,"readMinutes":3}