Combining conditions
AND keeps rows when both conditions are true; OR keeps rows when either is true. This query looks for paid or shipped orders worth more than 500.
1
SELECT order_number, status, total
FROM orders
WHERE (status = 'paid' OR status = 'shipped')
AND total > 500
ORDER BY total DESC
LIMIT 20;
Result — first 5 of 20 rows
| order_number | status | total |
|---|---|---|
| ORD-2025-002767 | shipped | 30906.66 |
| ORD-2022-001901 | shipped | 28151.01 |
| ORD-2023-001772 | paid | 26797.87 |
| ORD-2024-001907 | shipped | 26648.91 |
| ORD-2025-002847 | paid | 25837.57 |
Opens Planche in a separate browser tab, with the sample database loaded and this query ready. Every example on the page reuses that tab, and nothing runs until you press F9.
The parentheses change the meaning. Run the same conditions without them:
2
-- The same words without the brackets. AND binds tighter than OR,
-- so this reads as: paid (any total) OR shipped-and-over-500.
SELECT order_number, status, total
FROM orders
WHERE status = 'paid' OR status = 'shipped' AND total > 500
ORDER BY total DESC
LIMIT 20;
Result — first 5 of 20 rows
| order_number | status | total |
|---|---|---|
| ORD-2025-002767 | shipped | 30906.66 |
| ORD-2022-001901 | shipped | 28151.01 |
| ORD-2023-001772 | paid | 26797.87 |
| ORD-2024-001907 | shipped | 26648.91 |
| ORD-2025-002847 | paid | 25837.57 |
The result changes because AND has higher precedence than OR, just as × has higher precedence than +. Without parentheses, the database reads the condition as “paid at any total, or shipped and over 500,” so small paid orders are included too.
Matching one of several values
Use IN when the same column is compared with several values using OR.
3
SELECT order_number, status, total, ordered_at
FROM orders
WHERE status IN ('paid', 'shipped', 'delivered')
ORDER BY ordered_at DESC
LIMIT 20;
Result — first 5 of 20 rows
| order_number | status | total | ordered_at |
|---|---|---|---|
| ORD-2025-002480 | shipped | 6333.77 | 2025-12-28 23:52:14 |
| ORD-2025-000505 | delivered | 2276.51 | 2025-12-28 11:16:39 |
| ORD-2025-001581 | delivered | 2645.06 | 2025-12-28 06:12:38 |
| ORD-2025-000995 | delivered | 2584.02 | 2025-12-28 05:47:53 |
| ORD-2025-003219 | delivered | 561.48 | 2025-12-28 03:20:37 |
status IN ('paid', 'shipped', 'delivered') is equivalent to three OR conditions on status. NOT IN is the inverse, but it needs care when NULL is involved; the subqueries lesson covers that case.
Ranges
BETWEEN a AND b is shorthand for >= a AND <= b. Both ends are included.
4
SELECT order_number, ordered_at, total
FROM orders
WHERE ordered_at BETWEEN '2025-01-01' AND '2025-01-31 23:59:59'
ORDER BY ordered_at
LIMIT 20;
Result — first 5 of 20 rows
| order_number | ordered_at | total |
|---|---|---|
| ORD-2025-003412 | 2025-01-01 00:18:52 | 2806.57 |
| ORD-2025-003616 | 2025-01-01 11:18:14 | 6955.62 |
| ORD-2025-001503 | 2025-01-01 23:43:34 | 5837.23 |
| ORD-2025-000317 | 2025-01-02 05:08:32 | 15130.7 |
| ORD-2025-001298 | 2025-01-02 18:36:32 | 5045.04 |
Patterns
LIKE compares against a pattern where % stands for any run of characters and _ for exactly one.
5
SELECT first_name, last_name, email
FROM customers
WHERE email LIKE '%@inbox.example'
ORDER BY last_name
LIMIT 20;
Result — first 5 of 20 rows
| first_name | last_name | |
|---|---|---|
| Freya | Ahmed | freya.ahmed@inbox.example |
| Hugo | Ahmed | hugo.ahmed@inbox.example |
| Jonas | Ahmed | jonas.ahmed@inbox.example |
| Kenji | Ahmed | kenji.ahmed@inbox.example |
| Liam | Ahmed | liam.ahmed@inbox.example |
Missing values
279 of these customers have no phone number recorded. Here they are:
6
SELECT id, first_name, last_name, phone
FROM customers
WHERE phone IS NULL
LIMIT 20;
Result — first 5 of 20 rows
| id | first_name | last_name | phone |
|---|---|---|---|
| 2 | Mateo | Costa | NULL |
| 9 | Sven | Smith | NULL |
| 21 | Layla | Fischer | NULL |
| 25 | Freya | Nguyen | NULL |
| 28 | Priya | Jang | NULL |
And here is the query most people write first:
7
-- 279 customers have no phone number, and this finds none of them.
SELECT id, first_name, last_name, phone
FROM customers
WHERE phone = NULL;
No rows
The result is an empty grid, with no error or warning.
NULL represents an unknown or missing value, so ordinary comparisons with it cannot be true. phone = NULL produces an unknown result, not true, and therefore matches no rows. NULL = NULL is not true either. Use IS NULL and IS NOT NULL instead.
What you can do now
You can express almost any condition over one table. The remaining limit is still that everything lives in one table — and the questions worth asking usually do not.