The SQL WHERE Clause: Filtering Data with Every Operator
On this page 12 sections ▾
- Comparison operators
- Combining conditions: AND, OR, NOT
- Matching a list with IN
- A range with BETWEEN
- Pattern matching with LIKE
- Checking for missing values with IS NULL
- Operator precedence: why AND beats OR
- The NOT IN and NULL trap
- WHERE vs HAVING
- Practice this topic
- Frequently asked questions
- Related reading
SELECT tells the database which columns you want. WHERE tells it which rows qualify. It goes right
after FROM (and before GROUP BY, if you use one), and every example below runs against the
Employees table
in the SQL Playground.
Comparison operators
| Operator | Meaning |
|---|---|
| = | equal to |
| <> or != | not equal to |
| > / < | greater than / less than |
| >= / <= | greater than or equal / less than or equal |
SELECT first_name, salary
FROM Employees
WHERE salary >= 100000;
| first_name | salary |
|---|---|
| Ananya | 142000 |
| Marcus | 118000 |
| Tomas | 131000 |
| Priya | 105000 |
| Chen | 156000 |
● 5 rows · produced by running this query on the sample database
Combining conditions: AND, OR, NOT
AND requires
both sides to be true; OR
requires at least one. When you mix them, use parentheses — AND is evaluated before OR, which trips people up:
-- Active employees who are either in Engineering (10) OR earn over 120k
SELECT first_name, dept_id, salary
FROM Employees
WHERE active = 1
AND (dept_id = 10 OR salary > 120000);
| first_name | dept_id | salary |
|---|---|---|
| Ananya | 10 | 142000 |
| Marcus | 10 | 118000 |
| Leila | 10 | 96000 |
| Tomas | 20 | 131000 |
| Sofia | 10 | 79000 |
| Chen | 50 | 156000 |
● 6 rows · produced by running this query on the sample database
Without the parentheses, active = 1 AND dept_id = 10 OR salary > 120000 would also return inactive employees who happen to earn over 120k — probably not what you meant.
Matching a list with IN
Instead of chaining several ORs, use IN to check against a list of values:
SELECT first_name, dept_id
FROM Employees
WHERE dept_id IN (10, 20, 30);
Add NOT to invert it: WHERE dept_id NOT IN (10, 20, 30).
A range with BETWEEN
BETWEEN is inclusive on both ends — the boundary values themselves are included in the result:
SELECT first_name, salary
FROM Employees
WHERE salary BETWEEN 70000 AND 100000;
| first_name | salary |
|---|---|
| Leila | 96000 |
| Grace | 89500 |
| Hiroshi | 74500 |
| Sofia | 79000 |
| Isabela | 92000 |
● 5 rows · produced by running this query on the sample database
Pattern matching with LIKE
LIKE matches text patterns using two wildcards: %
(any number of characters, including zero) and _
(exactly one character).
WHERE email LIKE '%@example.com' -- ends with this domain
WHERE first_name LIKE 'A%' -- starts with A
WHERE first_name LIKE '%an%' -- contains "an" anywhere
Whether LIKE cares about upper and lower case depends on the database. The playground on this site runs SQLite, where LIKE ignores case for the letters A to Z, so the "starts with A" pattern still works written in lower case:
SELECT first_name, last_name
FROM Employees
WHERE first_name LIKE 'a%';
| first_name | last_name |
|---|---|
| Ananya | Rao |
● 1 row · produced by running this query on the sample database
The = operator
is stricter: WHERE first_name = 'ananya'
returns nothing here, because it compares the text exactly. PostgreSQL's LIKE is case-sensitive, while MySQL and
SQL Server usually ignore case because of their default collations. If a search has to behave the same
everywhere, lower-case both sides: LOWER(first_name) LIKE 'a%'.
Checking for missing values with IS NULL
This is the mistake almost every beginner makes at least once: you cannot test for NULL with
= NULL. NULL means
"unknown," and in SQL's three-valued logic, unknown compared to anything — even another NULL — is never true.
-- Wrong: this returns zero rows, even if manager_id has NULLs
SELECT * FROM Employees WHERE manager_id = NULL;
-- Correct
SELECT * FROM Employees WHERE manager_id IS NULL;
SELECT * FROM Employees WHERE manager_id IS NOT NULL;
Operator precedence: why AND beats OR
AND binds more tightly than OR, exactly the way multiplication binds more tightly than addition. That single rule quietly changes the meaning of a lot of queries. Compare these two — they differ only by a pair of brackets:
-- Reads as: dept 10, OR (dept 20 AND well paid)
SELECT first_name, last_name, dept_id, salary
FROM Employees
WHERE dept_id = 10 OR dept_id = 20 AND salary > 120000;
| first_name | last_name | dept_id | salary |
|---|---|---|---|
| Ananya | Rao | 10 | 142000 |
| Marcus | Bennett | 10 | 118000 |
| Leila | Haddad | 10 | 96000 |
| Tomas | Nowak | 20 | 131000 |
| Sofia | Lindqvist | 10 | 79000 |
| Omar | Farouk | 10 | 61000 |
● 6 rows · produced by running this query on the sample database
-- Reads as: (dept 10 or dept 20), AND well paid
SELECT first_name, last_name, dept_id, salary
FROM Employees
WHERE (dept_id = 10 OR dept_id = 20) AND salary > 120000;
| first_name | last_name | dept_id | salary |
|---|---|---|---|
| Ananya | Rao | 10 | 142000 |
| Tomas | Nowak | 20 | 131000 |
● 2 rows · produced by running this query on the sample database
The first returns six rows — all five employees in department 10 whatever they earn, plus the one well-paid employee in department 20. The second returns two. Neither is wrong as SQL; only one of them is the question you meant to ask. When a condition mixes AND with OR, add the brackets even where they are technically redundant.
The NOT IN and NULL trap
This one is genuinely surprising the first time. If the list you give to NOT IN contains a single
NULL, the whole condition returns no rows at all — not an error, just an empty result. That is how PostgreSQL,
MySQL, SQL Server and SQLite all behave, and because this site's playground runs SQLite, you can watch it happen.
The question is "which employees manage nobody?", answered by excluding everyone who appears as someone's manager:
-- Looks right, returns nothing: the subquery's list contains NULL
SELECT first_name, last_name
FROM Employees
WHERE emp_id NOT IN (SELECT manager_id FROM Employees);
● 0 rows · the query ran, and nothing in the sample database matched it
Five employees have no manager, so the subquery hands NOT IN a list with NULL in it, and
not a single row survives. The reason follows from how NULL works. x NOT IN (1, 2, NULL) is evaluated as
x <> 1 AND x <> 2 AND x <> NULL, and that last comparison can never be true —
NULL means "unknown", and nothing is known to be unequal to an unknown value. One unknown in the chain makes
the whole AND unknown, so the row is not returned. IN does not have this problem, only
NOT IN. If the subquery feeding the list might produce NULLs, filter them out with
WHERE column IS NOT NULL, or use NOT EXISTS instead. It only asks whether a matching
row exists, so a comparison with NULL simply fails to match instead of making the whole condition unknown:
SELECT e.first_name, e.last_name
FROM Employees e
WHERE NOT EXISTS (
SELECT 1 FROM Employees r WHERE r.manager_id = e.emp_id
)
ORDER BY e.emp_id;
| first_name | last_name |
|---|---|
| Marcus | Bennett |
| Leila | Haddad |
| Grace | Okafor |
| Hiroshi | Tanaka |
| Daniel | Cruz |
| Omar | Farouk |
| Isabela | Moreira |
● 7 rows · produced by running this query on the sample database
Seven employees have nobody reporting to them. Adding WHERE manager_id IS NOT NULL inside the
first query's subquery returns the same seven. The EXISTS vs IN guide
compares the two approaches in more depth.
WHERE vs HAVING
WHERE filters individual rows before any grouping happens. If you need to filter based on an aggregate result (like "departments with more than 5 employees"), you need HAVING instead — see our dedicated guide on GROUP BY vs HAVING.
Try it yourself
Every WHERE clause above runs directly against the pre-loaded Employees table in the Playground.
Open the PlaygroundPractice this topic
Reading explains it; solving it is what makes it stick. These exercises use exactly what this page covered, and your answer is checked by running it against the same database:
- Find the expensive products Easy
- Active engineers only Easy
- Companies beginning with A Medium
- Who reports to nobody? Medium
Frequently asked questions
What is the difference between = and LIKE in SQL?
The = operator tests for an exact match, while LIKE matches a pattern using wildcards: % stands for any run of characters and _ stands for exactly one. Use = when you know the whole value and LIKE when you only know part of it.
How do I check for NULL in a WHERE clause?
Use IS NULL or IS NOT NULL. Comparing with = NULL never works, because NULL means “unknown” and any comparison against an unknown value is itself unknown rather than true.
Is BETWEEN inclusive of both endpoints?
Yes. BETWEEN 10 AND 20 includes both 10 and 20. It is shorthand for column >= 10 AND column <= 20. Be careful when applying it to dates stored with a time component, because the end of the range then means midnight at the start of that day.
Does WHERE run before or after GROUP BY?
Before. The order is FROM, then WHERE, then GROUP BY, then HAVING, then SELECT, then ORDER BY. That is why WHERE cannot filter on an aggregate such as COUNT(*) — at that point the groups do not exist yet.
Is LIKE case-sensitive?
It depends on the database and its collation settings. PostgreSQL is case-sensitive and offers ILIKE for case-insensitive matching. MySQL and SQL Server are usually case-insensitive because of their default collations, and SQLite, which runs this site's playground, ignores case in LIKE for the letters A to Z while its = still compares text exactly. Applying LOWER() to both sides works consistently everywhere.
Can I use a column alias in the WHERE clause?
In most databases, no. WHERE is evaluated before SELECT, so the alias does not exist yet, and PostgreSQL, MySQL and SQL Server reject it. SQLite is an exception: it accepts the alias, so a query that works in this site's playground can still fail in those three. Repeat the underlying expression, or wrap the query in a subquery and filter on the alias in the outer query.
Related reading
About the author
SQL Practice
SQL Practice publishes free SQL tutorials, an in-browser playground and 42 practice exercises, built so you can run what you read. Every runnable example in our tutorials is executed against the site’s sample database before it is published. Read how we keep tutorials accurate, or report a mistake.