EduFlow
SQL for Analysts
0%

Section 1 · Querying with Intent

Filtering and Sorting with Purpose

16 min4 min readSQL exercise

Lesson at a glance

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.

  1. 1AND before OR
  2. 2The rest of the filtering toolkit
  3. 3Saying “not” correctly
  4. 4Sorting you can rely on

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

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 WHERE exactly right.

AND before OR

Here’s the first draft most people type, translating her words left to right:

SQL
SELECT COUNT(*) AS orders
FROM orders
WHERE channel = 'web' OR channel = 'mobile_app'
  AND coupon_code IN ('SPRING15', 'WELCOME10');

1,191 orders. That’s more than half of all orders in two years, for two niche coupons. The number is the alarm: AND binds tighter than OR, so SQL read this as “every web order, plus app orders that used a coupon”. Every web order is in there, coupon or not.

Parentheses say what Huda meant:

SQL
SELECT COUNT(*) AS orders
FROM orders
WHERE (channel = 'web' OR channel = 'mobile_app')
  AND coupon_code IN ('SPRING15', 'WELCOME10');

110 orders. Better still, the channel test can use IN too, which removes the OR entirely: channel IN ('web', 'mobile_app'). IN checks one column against a list, and a list can’t be mis-grouped.

The rest of the filtering toolkit

A few operators cover nearly every filter you’ll write:

Operator Example Note
IN (...) status IN ('cancelled', 'returned') NOT IN excludes; beware NULLs (next lesson)
BETWEEN a AND b shipping_fee BETWEEN 5 AND 10 Includes both ends
LIKE name LIKE '%mat%' % is any run of characters, _ is exactly one
<> / != status <> 'cancelled' Both mean “not equal”

Text comparison has a SQLite-specific twist. = is case-sensitive, so 'a' = 'A' is false, while LIKE ignores case for ASCII letters, so 'a' LIKE 'A' is true. That’s why name LIKE '%mat%' finds both “Desk Mat XL” and “Cork Yoga Mat”. PostgreSQL’s LIKE is case-sensitive, a difference you’ll meet again in the dialects lesson.

SQL
SELECT product_id, name, unit_price
FROM products
WHERE name LIKE '%mat%'
  AND unit_price BETWEEN 20 AND 80;

Saying “not” correctly

Negation is where careful people slip. Suppose Huda now wants orders that are neither cancelled nor returned. You can write it two ways, and they’re equivalent:

SQL
SELECT
  (SELECT COUNT(*) FROM orders
   WHERE NOT (status = 'cancelled' OR status = 'returned')) AS not_either,
  (SELECT COUNT(*) FROM orders
   WHERE status <> 'cancelled' AND status <> 'returned') AS both_not;

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: status <> 'cancelled' OR status <> 'returned' is TRUE for every row, because any status differs from at least one of the two. When you write a negative filter, NOT IN ('cancelled', 'returned') is the clearest form of all.

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.

Dates in Cartwheel are ISO text (2025-11-28 19:55:30), which sorts in the same order as time. So order_date >= '2025-11-01' works as a date filter. The next lessons cover the traps, starting with what happens at the end of a day.

Sorting you can rely on

ORDER BY takes several keys, each with its own direction. The second key only matters among rows that tie on the first:

SQL
SELECT order_id, customer_id, order_date, coupon_code
FROM orders
WHERE channel = 'marketplace'
  AND coupon_code IS NOT NULL
ORDER BY order_date DESC, order_id
LIMIT 5;

Why add order_id 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 ORDER BY with a unique column, usually the primary key, makes the order fully deterministic.

That matters most for paging. LIMIT 20 OFFSET 40 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.

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: NULL.

Key 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 reading

Check your understanding

4 questions

Question 1 of 4

Which rows does this filter keep?

SQL
WHERE status = 'delivered' OR status = 'shipped' AND channel = 'web'
Question 2 of 4

You need web orders from customer 42 whose channel value might be stored as ‘Web’ or ‘web’. Which test matches both?

Question 3 of 4

A dashboard pages through orders 20 at a time with ORDER BY order_date LIMIT 20 OFFSET 40. Users report an order appearing on two pages. What’s the most likely fix?

Question 4 of 4

BETWEEN 10 AND 20 on shipping_fee: which value is NOT included?

Practice

SQL exercise

Is BLACKFRIDAY25 leaking?

Huda’s follow-up: “BLACKFRIDAY25 was meant for November only. Show me every 2025 order that used it outside November, on web or the app, and that wasn’t cancelled.”

Return order_id, order_date and channel, sorted by order_date (oldest first). Order matters.

The starter runs, but it returns hundreds of rows. Fix its logic.

Tables
customers
(customer_id, first_name, last_name, email, country, city, signup_date, segment, referred_by)
orders
(order_id, customer_id, order_date, status, channel, coupon_code, shipping_fee)
order_items
(order_item_id, order_id, product_id, quantity, unit_price, discount)
products
(product_id, name, category_id, unit_price, unit_cost, launched_on, is_active)
categories
(category_id, name, parent_id)
returns
(return_id, order_item_id, return_date, reason, refund_amount)
employees
(employee_id, first_name, last_name, title, department, manager_id, hire_date, salary)
support_tickets
(ticket_id, customer_id, order_id, opened_at, closed_at, priority, channel, satisfaction, agent_id)
web_sessions
(session_id, customer_id, started_at, source, device, pages_viewed, added_to_cart, purchased)
-- BLACKFRIDAY25 used outside November 2025, web or app, not cancelled
SELECT order_id, order_date, channel
FROM orders
WHERE coupon_code = 'BLACKFRIDAY25'
  AND channel = 'web' OR channel = 'mobile_app'
  AND status <> 'cancelled'
  AND order_date >= '2025-01-01'
  AND order_date < '2025-11-01' OR order_date >= '2025-12-01'
ORDER BY order_date;Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.