IN & BETWEEN
Two shortcuts make common filters shorter and clearer. IN checks a value against a list, replacing chains of OR. BETWEEN checks whether a value falls within a range, replacing a pair of comparisons.
IN: matching a list
SQL
SELECT name, country
FROM customers
WHERE country IN ('Spain', 'France', 'Germany');
Result 4 rows
| name | country |
| Chloé Martin | France |
| David Müller | Germany |
| Elena García | Spain |
| Farah Haddad | France |
This is equivalent to country = 'Spain' OR country = 'France' OR country = 'Germany', but easier to read and extend.
NOT IN
SQL
SELECT id, status
FROM orders
WHERE status NOT IN ('delivered', 'cancelled');
Result 5 rows
| id | status |
| 9 | shipped |
| 11 | shipped |
| 12 | pending |
| 13 | pending |
| 15 | pending |
IN with a subquery
SQL
SELECT name
FROM products
WHERE id IN (SELECT product_id FROM order_items WHERE quantity >= 2);
Result 7 rows
| name |
| The Patient Gardener |
| Learning SQL Step by Step |
| Wireless Earbuds |
| USB-C Charger |
| Ceramic Mug |
| Linen Throw |
| Jigsaw Puzzle 1000 |
The list can come from another query: here, products that were ever bought two or more at a time. Subqueries get a full treatment in lesson 29.
The NOT IN and NULL trap
SQL
SELECT name FROM customers
WHERE city NOT IN ('Lisbon', NULL);
No rows at all! city NOT IN ('Lisbon', NULL) means city <> 'Lisbon' AND city <> NULL, and any comparison with NULL is unknown — so the whole condition is never true. This bites most often when the list comes from a subquery that contains a NULL. Use NOT EXISTS (lesson 30) or filter out the NULLs.
BETWEEN: ranges
SQL
SELECT name, price
FROM products
WHERE price BETWEEN 30 AND 50
ORDER BY price;
Result 5 rows
| name | price |
| Learning SQL Step by Step | 32.5 |
| Desk Lamp | 34 |
| Wooden Train Set | 39.9 |
| Night Sky Atlas | 45 |
| Linen Throw | 49 |
BETWEEN a AND b is inclusive: it means >= a AND <= b. The smaller value must come first; BETWEEN 50 AND 30 matches nothing.
BETWEEN with dates
SQL
SELECT id, order_date
FROM orders
WHERE order_date BETWEEN '2025-12-01' AND '2025-12-31';
Result 2 rows
| id | order_date |
| 7 | 2025-12-01 |
| 8 | 2025-12-12 |
This works here because the dates have no time part. With timestamps, '2025-12-31' means midnight at the start of the 31st, so anything later that day is missed. A half-open range is safer and works for both:
SQL
SELECT id, order_date
FROM orders
WHERE order_date >= '2025-12-01'
AND order_date < '2026-01-01';
Result 2 rows
| id | order_date |
| 7 | 2025-12-01 |
| 8 | 2025-12-12 |
NOT BETWEEN and text ranges
SQL
SELECT first_name, salary
FROM employees
WHERE salary NOT BETWEEN 50000 AND 90000
ORDER BY salary;
Result 4 rows
| first_name | salary |
| Priya | 42000 |
| Hannah | 48000 |
| Mei | 98000 |
| Olivia | 150000 |
SQL
SELECT name
FROM customers
WHERE name BETWEEN 'A' AND 'D'
ORDER BY name;
Result 3 rows
| name |
| Ana Pereira |
| Ben Carter |
| Chloé Martin |
Text ranges compare alphabetically. Note that 'David Müller' is excluded, because it sorts after plain 'D'. To include all names starting with A–D, use name < 'E'.
Try it yourself
Find employees in Sales or Marketing (departments 1 and 3) hired between 2018 and 2021 inclusive.