{"id":"l-sql-1-2","courseId":"sql-for-analysts","lang":"en","kind":"lesson","summary":"Write WHERE clauses that say exactly what you mean with AND, OR, IN, BETWEEN and LIKE, and sort results deterministically with multi-key ORDER BY, LIMIT and OFFSET.","html":"<p>Huda Saleh, VP Marketing, writes: “Can you pull the orders that used SPRING15 or WELCOME10 on our own channels, web or the app? I want to see whether the welcome offer pulls people into the app.” Her sentence contains two ORs and an AND. That’s the shape of request that produces silently wrong numbers, so it’s worth getting <code dir=\"ltr\">WHERE</code> exactly right.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">AND before OR</h2>\n<p>Here’s the first draft most people type, translating her words left to right:</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\"> 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\"> channel </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'web'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> OR</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> channel </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'mobile_app'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  AND</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> coupon_code </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">IN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'SPRING15'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'WELCOME10'</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>1,191 orders. That’s more than half of all orders in two years, for two niche coupons. The number is the alarm: <code dir=\"ltr\">AND</code> binds tighter than <code dir=\"ltr\">OR</code>, so SQL read this as “every web order, plus app orders that used a coupon”. Every web order is in there, coupon or not.</p>\n<p>Parentheses say what Huda meant:</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\"> 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\"> (channel </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'web'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> OR</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> channel </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'mobile_app'</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\"> coupon_code </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">IN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'SPRING15'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'WELCOME10'</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>110 orders. Better still, the channel test can use <code dir=\"ltr\">IN</code> too, which removes the OR entirely: <code dir=\"ltr\">channel IN ('web', 'mobile_app')</code>. <code dir=\"ltr\">IN</code> checks one column against a list, and a list can’t be mis-grouped.</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>Trusting a query because it ran</span></p>\n<p>The wrong query didn’t error. It returned a plausible-looking number. Whenever a filter mixes AND and OR, put parentheses around each OR group, and glance at the count: if a “niche” filter keeps half the table, the logic is wrong.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">The rest of the filtering toolkit</h2>\n<p>A few operators cover nearly every filter you’ll write:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Operator</th>\n<th>Example</th>\n<th>Note</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code dir=\"ltr\">IN (...)</code></td>\n<td><code dir=\"ltr\">status IN ('cancelled', 'returned')</code></td>\n<td><code dir=\"ltr\">NOT IN</code> excludes; beware NULLs (next lesson)</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">BETWEEN a AND b</code></td>\n<td><code dir=\"ltr\">shipping_fee BETWEEN 5 AND 10</code></td>\n<td>Includes both ends</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">LIKE</code></td>\n<td><code dir=\"ltr\">name LIKE '%mat%'</code></td>\n<td><code dir=\"ltr\">%</code> is any run of characters, <code dir=\"ltr\">_</code> is exactly one</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">&lt;&gt;</code> / <code dir=\"ltr\">!=</code></td>\n<td><code dir=\"ltr\">status &lt;&gt; 'cancelled'</code></td>\n<td>Both mean “not equal”</td>\n</tr>\n</tbody>\n</table></div><p>Text comparison has a SQLite-specific twist. <code dir=\"ltr\">=</code> is case-sensitive, so <code dir=\"ltr\">'a' = 'A'</code> is false, while <code dir=\"ltr\">LIKE</code> ignores case for ASCII letters, so <code dir=\"ltr\">'a' LIKE 'A'</code> is true. That’s why <code dir=\"ltr\">name LIKE '%mat%'</code> finds both “Desk Mat XL” and “Cork Yoga Mat”. PostgreSQL’s <code dir=\"ltr\">LIKE</code> is case-sensitive, a difference you’ll meet again in the dialects lesson.</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 style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> product_id, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, unit_price</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> products</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> LIKE</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '%mat%'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  AND</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> unit_price </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">BETWEEN</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 20</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> AND</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 80</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre><div class=\"run-output\" data-run-output=\"2\" hidden></div></div>\n<h2 id=\"sec-3\" tabindex=\"-1\">Saying “not” correctly</h2>\n<p>Negation is where careful people slip. Suppose Huda now wants orders that are <em>neither</em> cancelled <em>nor</em> returned. You can write it two ways, and they’re equivalent:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"sql\" data-run=\"sql\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">SQL</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"sql\" data-index=\"3\" aria-label=\"Run this example\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> COUNT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> orders</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">   WHERE</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> NOT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'cancelled'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> OR</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'returned'</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\"> not_either,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> COUNT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> orders</span></span>\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 style=\"color:#CF222E;--shiki-dark:#FF7B72\"> AND</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\"> 'returned'</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\"> both_not;</span></span></code></pre><div class=\"run-output\" data-run-output=\"3\" hidden></div></div>\n<p>Both return 1,864. The rule behind it is De Morgan’s law: pushing a NOT inside the parentheses flips every OR into an AND (and every AND into an OR). The common bug is to flip the comparisons but forget to flip the connector: <code dir=\"ltr\">status &lt;&gt; 'cancelled' OR status &lt;&gt; 'returned'</code> is TRUE for every row, because any status differs from at least one of the two. When you write a negative filter, <code dir=\"ltr\">NOT IN ('cancelled', 'returned')</code> is the clearest form of all.</p>\n<p>One more habit pays off here. Before trusting a filter with several conditions, count rows for each condition on its own. Huda’s filter has three parts: web or app keeps 1,848 rows, the two coupons keep 127, and “not cancelled or returned” keeps 1,864. Their AND can’t keep more than the smallest part, 127; if it does, the logic is wrong. A ten-second check like that catches most logic errors before anyone else sees them.</p>\n<p>Dates in Cartwheel are ISO text (<code dir=\"ltr\">2025-11-28 19:55:30</code>), which sorts in the same order as time. So <code dir=\"ltr\">order_date &gt;= '2025-11-01'</code> works as a date filter. The next lessons cover the traps, starting with what happens at the end of a day.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Sorting you can rely on</h2>\n<p><code dir=\"ltr\">ORDER BY</code> takes several keys, each with its own direction. The second key only matters among rows that tie on the first:</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 style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> order_id, customer_id, order_date, coupon_code</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\"> 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\">  AND</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> coupon_code </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">IS NOT NULL</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 style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, order_id</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">LIMIT</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 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>Why add <code dir=\"ltr\">order_id</code> when dates rarely tie? Because “rarely” isn’t “never”. When two rows tie on every sort key, SQL makes no promise about their order, and it can differ between runs. Ending the <code dir=\"ltr\">ORDER BY</code> with a unique column, usually the primary key, makes the order fully deterministic.</p>\n<p>That matters most for paging. <code dir=\"ltr\">LIMIT 20 OFFSET 40</code> means “skip 40 rows, return the next 20”, the third page of 20. If the order isn’t deterministic, a row can show up on page two and again on page three.</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>Write the sentence, then the WHERE</span></p>\n<p>Before typing a filter, write the condition as one sentence with brackets: “coupon is (SPRING15 or WELCOME10) and channel is (web or app) and status is not cancelled”. The brackets in the sentence become the parentheses in SQL.</p>\n</aside>\n<p>Huda’s coupon question is answered: 110 orders. Her follow-up is in the exercise below. Next comes the value that breaks all of these operators in surprising ways: <code dir=\"ltr\">NULL</code>.</p>\n","toc":[{"id":"sec-1","text":"AND before OR","level":2},{"id":"sec-2","text":"The rest of the filtering toolkit","level":2},{"id":"sec-3","text":"Saying “not” correctly","level":2},{"id":"sec-4","text":"Sorting you can rely on","level":2}],"takeaways":["AND binds tighter than OR, so wrap every OR group in parentheses when it sits next to an AND.","IN replaces a chain of OR equality tests on one column and is easier to read and harder to get wrong.","In SQLite, = on text is case-sensitive, while LIKE ignores case for ASCII letters.","Add a unique column as the last ORDER BY key so ties sort the same way every time.","LIMIT with OFFSET pages through a result, but only a deterministic ORDER BY makes the pages stable."],"further":[{"title":"SQLite expressions and operator precedence","url":"https://www.sqlite.org/lang_expr.html"},{"title":"SQLite SELECT, ORDER BY and LIMIT","url":"https://www.sqlite.org/lang_select.html#orderby"}],"quiz":[{"q":"<p>Which rows does this filter keep?</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\">WHERE</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'delivered'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> OR</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'shipped'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> AND</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> channel </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'web'</span></span></code></pre></div>\n","options":[{"text":"Delivered or shipped orders, but only on the web channel.","why":"That’s what the author probably meant, but AND is evaluated first, so the channel test only applies to the shipped side."},{"text":"Only orders that are both delivered and shipped, on the web.","why":"A single status can’t be two values at once; OR is still an OR here, it’s the grouping that surprises people."},{"text":"It’s a syntax error because AND and OR need parentheses.","why":"SQL accepts mixed AND/OR without parentheses. That’s exactly why the bug is silent."},{"text":"All delivered orders from any channel, plus shipped orders from the web.","why":"Correct. It reads as <code dir=\"ltr\">status = 'delivered' OR (status = 'shipped' AND channel = 'web')</code>."}],"answer":3},{"q":"<p>You need web orders from customer 42 whose channel value might be stored as ‘Web’ or ‘web’. Which test matches both?</p>","options":[{"text":"<code dir=\"ltr\">channel = 'web'</code>","why":"In SQLite, = compares text case-sensitively, so ‘Web’ would not match."},{"text":"<code dir=\"ltr\">channel IN ('web')</code>","why":"IN uses the same equality comparison as =, so it’s case-sensitive too."},{"text":"<code dir=\"ltr\">channel LIKE 'web'</code>","why":"Correct. SQLite’s LIKE is case-insensitive for ASCII letters by default, so it matches both spellings. <code dir=\"ltr\">LOWER(channel) = 'web'</code> would work too."}],"answer":2},{"q":"<p>A dashboard pages through orders 20 at a time with <code dir=\"ltr\">ORDER BY order_date LIMIT 20 OFFSET 40</code>. Users report an order appearing on two pages. What’s the most likely fix?</p>","options":[{"text":"Add <code dir=\"ltr\">order_id</code> as a second sort key so rows with equal dates always come in the same order.","why":"Correct. With ties on <code dir=\"ltr\">order_date</code>, the database may return tied rows in a different order on each query, so one row can land on two pages."},{"text":"Replace OFFSET with a larger LIMIT so fewer pages are needed.","why":"That hides the symptom on small tables but doesn’t make the order deterministic."},{"text":"Sort with DESC instead of ASC.","why":"The direction doesn’t matter; ties are just as ambiguous in descending order."}],"answer":0},{"q":"<p><code dir=\"ltr\">BETWEEN 10 AND 20</code> on <code dir=\"ltr\">shipping_fee</code>: which value is NOT included?</p>","options":[{"text":"10","why":"BETWEEN includes its lower bound; <code dir=\"ltr\">x BETWEEN a AND b</code> means <code dir=\"ltr\">x &gt;= a AND x &lt;= b</code>."},{"text":"20","why":"BETWEEN includes its upper bound as well, which is what trips people up with dates later."},{"text":"15.5","why":"Any value between the bounds is included, decimals too."},{"text":"20.01","why":"Correct. It’s above the upper bound. Both bounds themselves are included."}],"answer":3}],"exercise":{"kind":"sql","starter":"-- BLACKFRIDAY25 used outside November 2025, web or app, not cancelled\nSELECT order_id, order_date, channel\nFROM orders\nWHERE coupon_code = 'BLACKFRIDAY25'\n  AND channel = 'web' OR channel = 'mobile_app'\n  AND status <> 'cancelled'\n  AND order_date >= '2025-01-01'\n  AND order_date < '2025-11-01' OR order_date >= '2025-12-01'\nORDER BY order_date;","solution":"SELECT order_id, order_date, channel\nFROM orders\nWHERE coupon_code = 'BLACKFRIDAY25'\n  AND channel IN ('web', 'mobile_app')\n  AND status <> 'cancelled'\n  AND order_date >= '2025-01-01'\n  AND (order_date < '2025-11-01' OR order_date >= '2025-12-01')\nORDER BY order_date, order_id;","orderMatters":true,"title":"Is BLACKFRIDAY25 leaking?","prompt":"<p>Huda’s follow-up: “BLACKFRIDAY25 was meant for November only. Show me every 2025 order that used it <strong>outside November</strong>, on <strong>web or the app</strong>, and that <strong>wasn’t cancelled</strong>.”</p>\n<p>Return <code dir=\"ltr\">order_id</code>, <code dir=\"ltr\">order_date</code> and <code dir=\"ltr\">channel</code>, sorted by <code dir=\"ltr\">order_date</code> (oldest first). Order matters.</p>\n<p>The starter runs, but it returns hundreds of rows. Fix its logic.</p>\n","hints":["Count the ORs in the starter. Each one splits the whole WHERE clause in two because AND binds tighter.","Replace the channel OR with <code dir=\"ltr\">channel IN ('web', 'mobile_app')</code>.","The date condition needs parentheses: <code dir=\"ltr\">AND (order_date &lt; '2025-11-01' OR order_date &gt;= '2025-12-01')</code>, after the line that keeps only 2025."],"explanation":"<p>Without parentheses, each <code dir=\"ltr\">OR</code> splits the filter into separate alternatives, so the starter kept every app order that matched the later conditions and every order from December onwards, coupon or not. Using <code dir=\"ltr\">IN</code> for the channel removes one OR; wrapping the date alternatives in parentheses keeps the other one local. The result is 22 orders, mostly on the web: worth raising with Huda, because the code clearly still works year-round.</p>\n","checkLabels":{}},"words":754,"readMinutes":4}