What a WHERE clause does
A table can hold millions of rows, but a query usually needs only the ones that meet a requirement. The WHERE clause describes that requirement: SQL checks it against every candidate row and keeps the row only when its condition is TRUE.
- If the condition is
TRUE, the row is included. - If it is
FALSE, the row is left out. - If it is
UNKNOWN, which can happen whenNULLis involved, the row is also left out.
Suppose we need orders with an amount greater than 100.
SQL applies the condition amount > 100 to each row:
Ben and Dev are left out because their amounts are not greater than 100.
WHERE clause includes only the rows for which its complete condition evaluates to TRUE. Comparison operators
Most WHERE conditions begin with a comparison.
A comparison handles one condition at a time. Real requirements often involve several conditions, patterns, lists, or ranges, so SQL adds AND and OR, IN and NOT IN, LIKE, and BETWEEN. Let’s look into how each one works, with examples, common mistakes, and a quick quiz.
AND and OR
Use AND when every connected condition must be true; use OR when at least one may be true.
We’ll use this table throughout.
Using AND: every condition must be true
Problem: Find paid orders from India.
Solution: Here the country must be India and the status must be paid.
Only Order 1 is from India with a status equal to PAID, so only this one is included in the result.
Using OR: at least one condition may be true
Problem: To find all paid or pending orders:
Combining AND and OR: AND runs first
Now a more specific requirement: return paid or pending orders from India. A natural first attempt lists the three conditions in order. It is valid SQL and raises no error.
Evaluate the grouped condition row by row and the stray UK order is obvious:
The database followed the query exactly. The query simply did not express the intended requirement, which is what makes a precedence mistake dangerous: it runs successfully while returning incorrect data.
Parenthesise the OR
The two allowed statuses belong together, so group them. The country condition now applies to both, and the equivalent IN form reads even more directly.
AND and OR appear in the same WHERE clause, add parentheses to make the grouping explicit, even when the default precedence would be correct. Parentheses document the intent for the next reader and make the query safer to modify later.Quick check: AND / OR
Requirement: high-priority tickets from the data team whose status is open or pending.
Three candidate conditions: which one expresses it correctly?
IN and NOT IN
Use IN when a column may match any value in a list. Instead of a chain of ORs:
write:
Using the earlier orders table:
IN performs exact-value matching; it does not do pattern matching. Use NOT IN when a value must not belong to a list.
The NOT IN + NULL trap
For a fixed list of known, non-null values, NOT IN is straightforward. The trap appears when the list comes from another table or subquery that can contain NULL.
We want customers who are not blocked. You might expect Asha and Carla.
For Asha, NOT IN (2, NULL) behaves like a chain of not-equals joined by AND:
1 <> 2 → TRUE
1 <> NULL → UNKNOWN
TRUE AND UNKNOWN → UNKNOWN
WHERE UNKNOWN → row removedThe same thing happens to Carla. Because one value on the right is unknown, the whole condition is UNKNOWN, and the WHERE clause keeps only TRUE.
Safer approach: NOT EXISTS
When excluding rows based on a subquery, NOT EXISTS usually expresses the requirement more safely:
NOT EXISTS asks a direct question for each customer: is there no blocked-customer row with the same id? The unrelated NULL in the blocked table does not make the condition unknown. You can also strip the nulls inside the subquery, but NOT EXISTS usually communicates the anti-matching intent more clearly:
Two more IN gotchas
IN matches exact values, not patterns
WHERE country IN ('Ind%') does not find countries starting with Ind; IN treats 'Ind%' as literal text. Use LIKE for patterns: WHERE country LIKE 'Ind%'.
NULL on the left is never matched
A row with status = NULL satisfies neither status IN (…) nor status NOT IN (…). Test for missing values with IS NULL / IS NOT NULL.
Quick check: NOT IN
LIKE
Use LIKE when you need to match a text pattern rather than one exact value. It uses two wildcards.
Begins with, ends with, contains
Because % matches zero or more characters, position matters:
One character with the underscore
The underscore matches exactly one character. LIKE '_m%' means: any one character, then m, then anything, so Amit matches because its second character is m.
Use NOT LIKE to exclude a pattern. As with other comparisons, a NULL value produces UNKNOWN and is left out.
LIKE gotchas
= does not understand wildcards. WHERE customer = 'A%' looks for the literal text A%. Choose the operator by intent: = for one exact value, IN for several, LIKE for a pattern.LIKE normally matches the complete value. LIKE 'mit' matches only the exact string mit; to find it inside Amit, write LIKE '%mit%'.With codes like SAVE_10, SAVEA10, SAVEB10, the condition LIKE 'SAVE_10' treats _ as any one character and can match all three. Escape it to mean a literal underscore:
The same technique works for a literal %.
Do not assume LIKE 'a%' always matches Asha; behaviour varies by database, collation, and configuration. When you need case-insensitive matching, make it explicit, e.g.
Be aware that wrapping a column in a function can prevent a regular index from being used; on large datasets, check your database’s indexing options.
Quick check: LIKE
Which condition finds names whose second character is m?
BETWEEN
Use BETWEEN when a value must fall within a lower and upper boundary.
amount BETWEEN 100 AND 300 is equivalent to amount >= 100 AND amount <= 300.
BETWEEN gotchas
BETWEEN is inclusive: both 100 and 300 are kept. If the rule is “above 100 but below 300,” use > 100 AND < 300; for “at least 100 but below 300,” use >= 100 AND < 300. Let the operators come from the business rule, not habit.BETWEEN 300 AND 100 means amount >= 300 AND amount <= 100, which no ordinary value can satisfy.Timestamps: prefer a half-open range
BETWEEN is misleading on timestamp columns. Suppose created_at stores a date and a time:
To get every July order, BETWEEN '2026-07-01' AND '2026-07-31' looks reasonable, but the end may be read as 2026-07-31 00:00:00, which drops 2026-07-31 18:30:00. Use an inclusive start and an exclusive next boundary instead:
start <= value < next_start includes the complete final day, works regardless of timestamp precision, and prevents a boundary value from being counted in two adjacent periods. Use BETWEEN comfortably for clear numeric ranges; for timestamp periods, prefer the half-open form.Quick check: BETWEEN
Translating a business requirement
Most filtering mistakes happen before the SQL is written, when the requirement has not been divided into clear logical groups. Take:
Break it into separate decisions, then write one condition for each:
- Country must be India.
- Order time must fall within July.
- Status may be paid or pending.
- Amount must be from 100 through 500.
No parentheses are needed here because IN already groups its own list and every top-level condition is joined with AND. The query reads in the same order as the requirement (India, during July, paid or pending, amount 100 through 500), which makes it easy to verify.
Keep this mental model
A WHERE clause determines which rows are included, and a row is included only when the complete condition evaluates to TRUE. Pick the condition that fits the requirement:
And the gotchas worth remembering:
ANDis evaluated beforeOR; parentheses make logical groups explicit.INmatches exact values, not patterns.NOT INcan behave unexpectedly when its list containsNULL; preferNOT EXISTS.%and_are wildcards only withLIKE.BETWEENincludes both boundaries; write the lower one first.- Timestamp ranges are safer with an inclusive start and an exclusive next boundary.