Filtering: Intermediate
OR widens the filter
| left | right | left OR right |
|---|---|---|
| TRUE | TRUE | TRUE |
| TRUE | FALSE | TRUE |
| TRUE | NULL | TRUE |
| FALSE | FALSE | FALSE |
| FALSE | NULL | NULL |
| NULL | NULL | NULL |
When every branch of an OR compares the same column with =, IN says the same thing in fewer characters: status IN ('refunded', 'pending') is status = 'refunded' OR status = 'pending' with the repetition removed. OR earns its place when the branches test different columns, like status and amount above, or use different operators.
CASE picks a value per row
SELECTorder_id,amount,CASEWHEN amount >= 500 THEN 'high'WHEN amount >= 100 THEN 'medium'ELSE 'low'END AS priorityFROM orders
Two shapes of CASE
The form above is the searched CASE: each WHEN holds a full condition. The simple CASE names one expression once, CASE status WHEN 'refunded' THEN 1, and tests it against each WHEN value with =. The simple form reads cleanly for a fixed list of labels but cannot express a range, and since NULL = NULL is never TRUE, it can never match a NULL. The query below uses it to sort orders by how urgently they need a look.
This CASE labels each order by amount. Which label does order 1004, with an amount of 500, receive?
SELECTuser_id,country,status,total_spentFROM usersWHERE country = 'US'AND status = 'active'OR total_spent > 1000
The binding order
| evaluated | operators | example |
|---|---|---|
| 1st | =, !=, <, >, <=, >=, IN, LIKE, BETWEEN | total_spent > 1000 |
| 2nd | NOT | NOT status = 'active' |
| 3rd | AND | a AND b OR c reads as (a AND b) OR c |
| 4th | OR | a OR b AND c reads as a OR (b AND c) |
NOT binds tight
NOT status = 'active' AND country = 'US' negates only status = 'active', so it keeps US users whose status is anything else. To negate the whole conjunction, wrap it: NOT (status = 'active' AND country = 'US') keeps every row that fails at least one of the 2 tests, including users 156, 203 and 310 from the table above. A NULL status always drops out of the first form, because NOT NULL is NULL. The wrapped form decides by country: for a non-US row, NULL AND FALSE is FALSE and NOT FALSE is TRUE, so the row survives; for a US row, NULL AND TRUE is NULL, NOT NULL is NULL, and the row drops.
Against the 5-row users table above, how many rows does this query return?
- Add parentheses whenever one WHERE mixes AND with OR
- Put each condition on its own line, indented under its connector
- Read a clause back as AND groups joined by OR before trusting it
- Test a mixed clause on a small table where you can count the expected rows
- Rely on top to bottom reading to decide how conditions group
- Negate a group with a bare NOT in front of its first condition only
- Trust a query that returns the right count once; check which rows it returned
'Lake%'Starts with Lake, followed by anything or by nothing
'%Lake%'Contains Lake anywhere in the string
'_ake%'Any single character, then ake, then anything
'Cabin'No wildcard, so it behaves like =
Against the 6-row listings table above, how many rows does WHERE title LIKE 'Lake_%' return?
Matching a literal wildcard
A discount label such as '50% off' contains a percent sign that should match itself. Name an escape character with ESCAPE and put it before the wildcard: label LIKE '50\%%' ESCAPE '\' reads the first % as a literal percent sign and the second as a wildcard, so it matches labels starting with 50%. The same trick covers underscores in codes like 'SKU_'.
What LIKE costs
> Arrange the tiles into a query that returns the titles of Tahoe listings whose title contains the word Cabin, in alphabetical order. Two tiles do not belong.
| page | OFFSET | LIMIT | product_id returned |
|---|---|---|---|
| 1 | 0 | 3 | 2, 7, 5 |
| 2 | 3 | 3 | 3, 6, 1 |
| 3 | 6 | 3 | 4, 8 |
| 4 | 9 | 3 | none |
The products table is sorted by price DESC, then product_id. Which rows does OFFSET 2 LIMIT 2 return?
ORDER BY is the contract
What OFFSET costs
Keyset pagination
- Jump to any page number directly
- Cost grows with the page number: sorts and skips every earlier row
- Rows inserted between 2 requests shift the later pages
- Fits a numbered page bar in a UI
- Reach pages in sequence from the last seen row
- Cost stays flat: the filter lands on the next row
- Rows inserted earlier in the sort do not shift the next page
- Fits a Next button or an infinite scroll
SELECTorder_id,order_date,totalFROM ordersWHERE order_date BETWEEN '2024-01-01' AND '2024-03-31'AND total BETWEEN 500 AND 1500
The products table from the last section has prices 12, 45, 45, 90, 90, 120, 310 and 310. How many rows does WHERE price BETWEEN 45 AND 90 return?
The last day problem
- ▸Whole numbers and dates without a time: BETWEEN reads well and both ends are meant
- ▸Timestamps: lower bound with >=, upper bound with < at the start of the next period
- ▸Text: a longer string sorts after its own prefix, so 'Mango' sits beyond 'M'
- ▸Reversed bounds return 0 rows and no error, so check that low <= high
BETWEEN on text
| name | BETWEEN 'A' AND 'M' | >= 'A' AND < 'N' |
|---|---|---|
| Apple | TRUE | TRUE |
| Lemon | TRUE | TRUE |
| M | TRUE | TRUE |
| Mango | FALSE | TRUE |
| Nectarine | FALSE | FALSE |
The AND inside BETWEEN belongs to BETWEEN. In total BETWEEN 500 AND 1500 AND status = 'shipped' the engine reads the first AND as the range separator and the second as a connector, and BETWEEN resolves before that connector, the same as any comparison. NOT BETWEEN keeps the rows outside the range, and a NULL total matches neither form, since NULL compared to either bound is NULL.
> A Shopify merchant opens the orders list in the admin, filters to orders that are unfulfilled or flagged as high risk, searches customer names beginning with 'Mar', narrows to the current month, and pages through the matches 50 at a time while the store keeps taking new orders.
Branch, group, pattern-match, page and range-filter your rows, and predict exactly which ones come back.
Boolean logic: it's complicated
- Category
- SQL
- Difficulty
- intermediate
- Duration
- 24 minutes
- Challenges
- 18 hands-on challenges
Topics covered: OR and CASE expressions, Operator precedence, LIKE for pattern matching, LIMIT and OFFSET for pagination, BETWEEN for range filtering
Lesson Sections
- OR and CASE expressions (concepts: Conditional Logic)
OR widens the filter AND keeps a row only when every condition is true. OR keeps a row when at least one condition is true, so adding an OR condition can only grow the result and never shrink it. For each row the engine evaluates every condition to TRUE, FALSE or NULL, combines those values with the table below, and keeps the row when the final value is TRUE. A TRUE on either side settles the question, which is why TRUE OR NULL is TRUE. FALSE OR NULL stays NULL, and WHERE drops a row whose combi
- Operator precedence (concepts: Operator Precedence)
A WHERE clause with 3 conditions and both connectors has 2 possible readings, and the engine picks one by a fixed rule rather than by the order you typed them. A product team asks for active US users plus anyone who has spent more than 1000 regardless of location. The first attempt writes the conditions in that order. Four rows pass. Users 42 and 89 are active US users, and users 156 and 203 pass on spending alone although they live in Canada and the UK and are inactive. The engine read the clau
- LIKE for pattern matching (concepts: Pattern Matching)
Equality asks whether a string is exactly one value. LIKE asks whether a string fits a pattern, where 2 wildcard characters stand in for text you do not know. The percent sign matches any run of characters, including a run of none, and the underscore matches exactly 1 character. Every other character in the pattern must match itself, the pattern must account for the whole string, and letter case counts. The listings table below holds 6 titles. The 3 tabs search it with a prefix pattern, a contai
- LIMIT and OFFSET for pagination (concepts: Row Limiting)
LIMIT cuts the result to its first n rows. OFFSET skips the first m rows before the cut begins. Together they carve a sorted result into pages: page 1 is OFFSET 0 LIMIT 20, page 2 is OFFSET 20 LIMIT 20, and page k is OFFSET (k - 1) * 20 LIMIT 20. In the query text OFFSET follows ORDER BY and comes before LIMIT. The products table below has 8 rows, paged 3 at a time. Page 3 returns 2 rows because only 8 exist: OFFSET 6 skips 6, LIMIT 3 asks for 3, and the table has 2 left. A page shorter than the
- BETWEEN for range filtering (concepts: Range Filtering)
BETWEEN low AND high keeps a row when the value is at least low and at most high. It is the same test as value >= low AND value <= high written as a single operator, and both ends are included. The order of the bounds matters: total BETWEEN 1500 AND 500 asks for a number at least 1500 and at most 500, no number satisfies that, and the result is empty rather than an error. The animation applies 2 ranges to a set of orders. Two of the 5 orders survive. Order 1 sits well inside both ranges. Order 4