SQL WHERE Clause Explained with Examples (2026)
A query that checks WHERE email = NULL silently returns zero rows — every time, on every database — and most beginners spend twenty minutes debugging the wrong thing before finding out why. This guide covers WHERE from a single condition to combined AND/OR/NOT logic, BETWEEN, IN, LIKE wildcards, NULL handling, and the precedence trap that quietly returns the wrong rows in production.
Quick Answer: What Does WHERE Actually Filter?
WHERE filters which rows a query returns by testing each row against a condition — only rows where the condition evaluates true are kept. It supports comparison operators (= > < >= <= !=), logical operators (AND, OR, NOT), range and set matching (BETWEEN, IN), pattern matching (LIKE), and NULL checks (IS NULL). It runs early in a database's logical processing order — after FROM, before SELECT, GROUP BY, and ORDER BY — which is exactly why it can't reference a SELECT alias or an aggregate function.
Comparison Operators: The Building Blocks
Every WHERE clause is built from a small set of comparison operators, combined and chained as needed.
SELECT order_id, order_total FROM orders WHERE order_total > 100; -- not-equal has two spellings, both standard SELECT * FROM orders WHERE status != 'cancelled'; SELECT * FROM orders WHERE status <> 'cancelled';
The six you need: = equal, != or <> not equal, > greater than, < less than, >= greater than or equal, <= less than or equal. Both not-equal spellings are standard SQL and behave identically — != is more common in practice, but <> shows up often enough in older codebases and other languages' SQL dialects that you should recognize it on sight.
AND, OR, NOT: Combining Conditions
Real filtering rarely stops at one condition. AND requires every condition to be true, OR requires at least one, and NOT inverts whatever follows it.
-- both conditions must be true SELECT * FROM orders WHERE order_total > 100 AND status = 'shipped'; -- either condition can be true SELECT * FROM orders WHERE status = 'pending' OR status = 'processing'; -- invert a condition SELECT * FROM orders WHERE NOT status = 'cancelled';
That last example is functionally identical to WHERE status != 'cancelled' — both are valid, and which one reads more naturally depends on the condition. NOT becomes more useful once conditions get more complex, for example WHERE NOT (status = 'cancelled' OR status = 'refunded').
The Precedence Trap: Why Parentheses Aren't Optional
This is the single highest-stakes mistake in this article, because it doesn't error — it silently returns the wrong rows. SQL evaluates AND before OR by default, the same way multiplication is evaluated before addition in arithmetic.
-- INTENT: orders that are (pending OR processing) AND over $100 -- WHAT IT ACTUALLY DOES: (pending) OR (processing AND over $100) SELECT * FROM orders WHERE status = 'pending' OR status = 'processing' AND order_total > 100; -- CORRECT: parentheses force the intended grouping SELECT * FROM orders WHERE (status = 'pending' OR status = 'processing') AND order_total > 100;
The broken version silently includes every single pending order regardless of total, because AND binds tighter than OR and grabs status = 'processing' AND order_total > 100 as one unit before OR ever applies. The row count often looks plausible, which is exactly what makes this bug dangerous — it rarely throws an error, it just quietly returns a report that's wrong.
Any time a WHERE clause mixes AND and OR, add parentheses explicitly — even when you're confident about the default precedence. The cost of an extra pair of parentheses is nothing; the cost of a silently wrong report is a bad decision made on bad data.
BETWEEN and IN: Cleaner Range and Set Matching
BETWEEN and IN are shorthand for patterns you'd otherwise write with several chained comparisons.
-- BETWEEN is inclusive on both ends SELECT * FROM orders WHERE order_date BETWEEN '2026-01-01' AND '2026-01-31'; -- IN replaces a chain of OR = comparisons SELECT * FROM orders WHERE status IN ('pending', 'processing', 'shipped'); -- NOT IN excludes a set SELECT * FROM orders WHERE status NOT IN ('cancelled', 'refunded');
BETWEEN is inclusive on both boundary values — BETWEEN '2026-01-01' AND '2026-01-31' includes both January 1st and January 31st. This becomes a real trap on datetime columns with a time component: a timestamp of 2026-01-31 14:00:00 is excluded by that same BETWEEN, because it's later than the implicit midnight boundary on the 31st. For datetime ranges, comparing with >= and < against the start of the next day is usually safer than BETWEEN.
LIKE and Wildcards: Pattern Matching Text
LIKE matches text against a pattern using two wildcards: % matches any sequence of characters (including zero), and _ matches exactly one character.
-- starts with 'A' SELECT * FROM customers WHERE first_name LIKE 'A%'; -- ends with '.com' SELECT * FROM customers WHERE email LIKE '%.com'; -- contains 'gmail' anywhere SELECT * FROM customers WHERE email LIKE '%gmail%'; -- exactly 5 characters, starting with 'A' SELECT * FROM products WHERE sku LIKE 'A____';
| Database | LIKE case sensitivity | Case-insensitive option |
|---|---|---|
| PostgreSQL | Case-sensitive | ILIKE |
| MySQL | Usually insensitive | Depends on collation |
| BigQuery | Case-sensitive | LOWER() both sides |
A leading % wildcard (LIKE '%gmail%') generally cannot use a standard index efficiently, since the database can't know where in the text to start scanning — worth knowing before reaching for it on a large table in a performance-sensitive query.
NULL: The Operator That Isn't
NULL represents an unknown or missing value — not zero, not an empty string, not "false." Because it's unknown, SQL can't say NULL = NULL is true, since it doesn't know either value. This is why WHERE column = NULL always returns zero rows, silently, on every SQL database.
-- WRONG: this returns zero rows, always SELECT * FROM customers WHERE email = NULL; -- CORRECT SELECT * FROM customers WHERE email IS NULL; SELECT * FROM customers WHERE email IS NOT NULL;
SQL uses three-valued logic: TRUE, FALSE, and UNKNOWN. Any comparison involving NULL — with =, !=, >, anything — evaluates to UNKNOWN, and WHERE only keeps rows where the condition is TRUE. UNKNOWN is treated the same as FALSE for filtering purposes, so the row gets silently dropped either way.
WHERE vs HAVING: Different Points in the Pipeline
WHERE and HAVING both filter, but at different stages, which is why they can't be swapped for each other.
- WHERE filters individual rows before any grouping or aggregation happens — it operates on raw table data.
- HAVING filters groups after GROUP BY has aggregated rows — it can reference aggregate functions like
SUM()orCOUNT()that don't exist yet when WHERE runs. - Trying to use an aggregate in WHERE — for example
WHERE SUM(order_total) > 1000— throws an error in every major database, because at that point in the logical order, no aggregation has happened yet.
GROUP BY and HAVING get full treatment in their own article later in this cluster — the distinction here is worth knowing early, since "why can't I use SUM in WHERE" is one of the most common early SQL errors.
Mistakes That Give Away a Beginner
- Using = NULL instead of IS NULL. Silent, zero rows, no error — the hardest kind of bug to notice.
- Mixing AND/OR without parentheses. Covered in depth above — the single highest-stakes mistake in this article because it returns a plausible-looking wrong answer instead of an error.
- Referencing a SELECT alias inside WHERE. WHERE runs before the SELECT list is evaluated, so an alias defined there doesn't exist yet — repeat the full expression instead.
- Reaching for an aggregate function in WHERE. SUM, COUNT, AVG and friends belong in HAVING, not WHERE, because they don't exist until after grouping.
- Assuming BETWEEN excludes the boundary. It's inclusive on both ends — a frequent off-by-one source on date ranges with a time component.
Filter Early: A Performance Note
WHERE conditions that run against an indexed column are typically fast even on large tables, since the database can jump directly to matching rows instead of scanning every one. Filtering on a calculated expression — for example WHERE UPPER(email) = 'TEST@EXAMPLE.COM' — usually prevents the database from using a standard index on that column, since it has to compute the expression for every row before it can compare.
When a query involving a join is slow, check whether your WHERE conditions are filtering before or after the join happens in the query plan. Filtering the smaller table down first, before joining, is usually faster than joining everything and filtering the combined result afterward — the query planner often does this automatically, but not always.
Practice: Write These Three Filters Yourself
Using the same orders table from the SELECT article (order_id, customer_id, product_name, price, quantity, order_date, status), try writing these:
Combine a range and a set
Return orders placed in March 2026 with a status of either "shipped" or "delivered."
Handle a missing value
Return every order where customer_id is missing (never assume you can use = for this).
Force the right precedence
Return orders that are either "pending" or "processing," and separately, always over $50 — using parentheses to guarantee correct grouping.
Frequently Asked Questions
What does the WHERE clause do in SQL?
WHERE filters which rows a query returns by testing each row against a condition — only rows where the condition evaluates true are included. It runs after FROM but before SELECT, GROUP BY, and ORDER BY in the database's logical processing order.
Why doesn't WHERE column = NULL work in SQL?
NULL represents an unknown value, and in SQL's three-valued logic, any comparison to NULL using = returns unknown rather than true, so no rows match. Use IS NULL or IS NOT NULL instead, which are specifically designed to test for the presence or absence of a value.
What is the difference between WHERE and HAVING in SQL?
WHERE filters individual rows before any grouping or aggregation happens. HAVING filters groups after GROUP BY has aggregated them, and can reference aggregate functions like SUM() or COUNT() that WHERE cannot use, because those aggregates don't exist yet at the point WHERE runs.
How do you combine multiple conditions in a WHERE clause?
Use AND to require both conditions, OR to require either, and NOT to invert a condition. When mixing AND and OR in the same clause, use parentheses explicitly, since AND has higher precedence than OR by default and relying on that default silently changes which rows match.
What does the SQL LIKE operator do?
LIKE matches text against a pattern using two wildcards: percent (%) matches any sequence of characters, including none, and underscore (_) matches exactly one character. For example WHERE name LIKE 'A%' matches any name starting with A.
Is SQL LIKE case-sensitive?
It depends on the database. PostgreSQL's LIKE is case-sensitive by default, with a separate ILIKE operator for case-insensitive matching. MySQL's LIKE is case-insensitive by default under typical collations. Always verify behavior on the specific database you're using rather than assuming.
Conclusion: Filter Deliberately, Not by Default
WHERE gives you comparison operators, AND/OR/NOT, BETWEEN, IN, LIKE, and NULL checks — a small toolkit that covers nearly every filtering need once you know exactly how each piece behaves. The two habits worth carrying forward from this article: always use IS NULL instead of = NULL, and always add parentheses the moment AND and OR appear in the same clause. Neither costs you anything, and both prevent the specific kind of bug that doesn't error — it just quietly returns the wrong answer.
Practice the three filters above against a real table, then move to the next article in this cluster, which covers ORDER BY and LIMIT — sorting and capping the rows WHERE hands back.
Next Up: ORDER BY and LIMIT
WHERE decides which rows survive — ORDER BY and LIMIT decide what order they come back in and how many. Continue the fundamentals tier of this cluster.
▶ Read the ORDER BY & LIMIT GuideKhalid Hussain
Founder of Review Publically. MSc holder and Google Advanced Data Analytics certified. Teaches SQL and Python data analysis with a focus on current, correct, production-ready syntax rather than outdated conventions.
Related Articles