Junior 4 min · March 05, 2026

SQL WHERE: AND/OR Precedence Bug Sent 40k Wrong Customers

AND has higher precedence than OR - missing parentheses emailed 40k customers wrongly.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • WHERE filters individual rows after FROM but before GROUP BY, SELECT, and ORDER BY
  • AND requires both conditions true; OR requires either true — use parentheses to control precedence
  • BETWEEN is inclusive on both ends: BETWEEN 10 AND 20 includes 10 and 20
  • IN is shorthand for multiple OR conditions; LIKE uses % for any sequence and _ for a single character
  • NULL comparison requires IS NULL / IS NOT NULL — WHERE col = NULL always returns zero rows
  • Biggest mistake: WHERE country = 'USA' OR country = 'UK' AND revenue > 1000 — AND binds tighter than OR, producing wrong logic without parentheses
Plain-English First

Imagine you have a massive filing cabinet with thousands of customer folders. The WHERE clause is like telling your assistant: 'Only bring me folders where the customer lives in New York AND spent more than $500.' Instead of dumping every single folder on your desk, you get exactly the ones you need. That's all WHERE does — it filters rows from a table so you only see the data that matches your conditions.

Every real-world database holds thousands, sometimes millions of rows. A table of online orders might have five million records going back a decade. Without a way to pinpoint just the rows you care about, querying a database would be like searching for a specific email by reading your entire inbox from the beginning. The WHERE clause is the most fundamental filtering tool in SQL — and it's why databases are actually useful in practice, not just in theory.

Before WHERE existed as a concept, you'd have to pull every row into your application and filter it in code. That means your server drags across the network, your app chews through memory, and your users wait. WHERE pushes the filtering down to the database engine itself — the place best equipped to do it fast, using indexes. It solves the problem of unnecessary data transfer and processing at its source.

By the end of this article you'll know how to write WHERE clauses from scratch, combine multiple conditions using AND, OR, and NOT, use powerful operators like BETWEEN, LIKE, and IN, and avoid the three beginner mistakes that silently break your queries. You'll also walk away with the answers to the interview questions that trip up even developers who've been writing SQL for a year.

What the WHERE Clause Actually Does (And Why It Belongs After FROM)

Every SQL SELECT statement follows a logical order: you tell the database WHAT columns you want (SELECT), then WHERE to look for rows (FROM), and then WHICH rows to keep (WHERE). The WHERE clause acts as a gatekeeper — the database evaluates every row in the table against your condition, and only the rows that pass get returned.

Think of it like a bouncer at a club checking IDs. Every single person in the queue gets checked. If your condition says 'age >= 21', only people who meet that rule get in. Everyone else is turned away quietly — they don't throw an error, they just don't appear in your results.

The clause uses standard comparison operators you already know from maths: = (equals), != or <> (not equals), > (greater than), < (less than), >= (greater than or equal to), and <= (less than or equal to). You attach the condition to a column name, and the database tests each row against it.

One thing that trips beginners up: WHERE is evaluated BEFORE SELECT. The database finds matching rows first, then decides which columns to show. This matters when you start writing more advanced queries.

basic_where_filter.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- Imagine a small e-commerce database.
-- This is our 'orders' table with real-looking data.

-- First, let's see ALL orders (no filter) so we understand what we're working with
SELECT order_id, customer_name, country, total_amount
FROM orders;

-- Output above shows 6 rows. Now let's FILTER:
-- We only want orders from customers in the USA.
-- The WHERE clause checks 'country' for every row.
SELECT order_id, customer_name, country, total_amount
FROM orders
WHERE country = 'USA';  -- Only rows where country equals 'USA' pass through

-- Now filter by a number — orders over $300
SELECT order_id, customer_name, country, total_amount
FROM orders
WHERE total_amount > 300;  -- '>' means strictly greater than, so 300.00 itself is excluded

-- Not equal to — find everyone NOT from Canada
SELECT order_id, customer_name, country, total_amount
FROM orders
WHERE country != 'Canada';  -- <> works identically: WHERE country <> 'Canada'
Output
-- Result of WHERE country = 'USA':
order_id | customer_name | country | total_amount
----------+----------------+---------+--------------
1001 | Alice Johnson | USA | 450.00
1004 | Marcus Webb | USA | 120.00
1006 | Sandra Lee | USA | 875.50
(3 rows)
-- Result of WHERE total_amount > 300:
order_id | customer_name | country | total_amount
----------+----------------+---------+--------------
1001 | Alice Johnson | USA | 450.00
1003 | Priya Sharma | India | 310.00
1006 | Sandra Lee | USA | 875.50
(3 rows)
Watch Out: String Values Need Quotes, Numbers Don't
Write WHERE country = 'USA' with single quotes around the text. Write WHERE total_amount > 300 with NO quotes around the number. If you write WHERE total_amount > '300', most databases silently cast it and it works — but you're relying on implicit type conversion, which can produce wrong results with certain data types. Be explicit from day one.
Production Insight
WHERE runs before GROUP BY and before SELECT aliases exist — it cannot reference computed column names.
The database evaluates WHERE first to eliminate rows before doing any expensive aggregation or sorting.
A WHERE clause that eliminates 99% of rows makes every subsequent operation faster — always filter early.
Key Takeaway
WHERE is the primary performance lever — it runs before aggregation, projection, and sorting.
SQL execution order: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY.
Index your most selective WHERE columns — that's where the B-tree lookup happens.

Combining Conditions with AND, OR, and NOT — The Logic Operators

One condition is useful. Multiple conditions are powerful. SQL gives you three logic operators to combine conditions: AND, OR, and NOT.

AND means BOTH conditions must be true. Think of it as a strict filter — like finding customers who live in the USA AND spent more than $300. Both boxes must be ticked.

OR means AT LEAST ONE condition must be true. Think of a wider net — customers from the USA OR customers from Canada. Either one qualifies.

NOT flips a condition on its head. WHERE NOT country = 'USA' is the same as WHERE country != 'USA'. It's most useful with operators like IN and LIKE, which we'll cover shortly.

Here's the critical thing beginners miss: AND has higher precedence than OR, just like multiplication beats addition in maths. So WHERE country = 'USA' OR country = 'Canada' AND total_amount > 300 does NOT mean what you think. The AND runs first, binding only the Canada condition to the amount check. Always use parentheses to make your logic explicit and readable.

and_or_not_conditions.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-- AND: both conditions must be true
-- Find USA customers who spent more than $300
SELECT order_id, customer_name, country, total_amount
FROM orders
WHERE country = 'USA'
  AND total_amount > 300;  -- Both must match — the row clears TWO checkpoints

-- OR: at least one condition must be true
-- Find anyone from the USA OR from India
SELECT order_id, customer_name, country, total_amount
FROM orders
WHERE country = 'USA'
   OR country = 'India';  -- Passes if EITHER condition is true

-- NOT: inverts the condition
-- Find every order that is NOT from the USA
SELECT order_id, customer_name, country, total_amount
FROM orders
WHERE NOT country = 'USA';  -- Equivalent to: WHERE country != 'USA'

-- MIXING AND + OR — always use parentheses to be safe!
-- Find: (USA customers over $300) OR (any Indian customer)
SELECT order_id, customer_name, country, total_amount
FROM orders
WHERE (country = 'USA' AND total_amount > 300)  -- Group 1 with parentheses
   OR (country = 'India');                       -- Group 2 with parentheses
Output
-- AND result (USA + over $300):
order_id | customer_name | country | total_amount
----------+---------------+---------+--------------
1001 | Alice Johnson | USA | 450.00
1006 | Sandra Lee | USA | 875.50
(2 rows)
-- OR result (USA or India):
order_id | customer_name | country | total_amount
----------+----------------+---------+--------------
1001 | Alice Johnson | USA | 450.00
1003 | Priya Sharma | India | 310.00
1004 | Marcus Webb | USA | 120.00
1006 | Sandra Lee | USA | 875.50
(4 rows)
Pro Tip: Parentheses Are Free — Use Them
Any time you mix AND with OR, wrap each logical group in parentheses. It costs you nothing in performance, and it makes your intent crystal clear to every developer (including future you) who reads the query six months from now. WHERE (a AND b) OR (c) is always safer than WHERE a AND b OR c.
Production Insight
AND binds before OR in SQL just like multiplication before addition in arithmetic — mixing them without parentheses is the most common logic bug in WHERE clauses.
Always use parentheses when combining AND and OR, even when you think the precedence is correct.
Test mixed conditions with a COUNT(*) first to confirm the expected row count before running the full query.
Key Takeaway
AND binds before OR — always use parentheses when combining them.
Missing parentheses in OR/AND expressions produce wrong rows, not errors — the bug is silent.
Test every complex WHERE with a COUNT(*) against your expected number before shipping.

BETWEEN, IN, and LIKE — The Power Operators That Replace Messy Conditions

Once you've got AND, OR, and NOT down, SQL gives you three shortcut operators that make common filtering patterns much cleaner to write and read.

BETWEEN filters rows within a range — it's shorthand for >= and <= combined. WHERE total_amount BETWEEN 100 AND 500 is identical to WHERE total_amount >= 100 AND total_amount <= 500. Both ends are inclusive, meaning 100 and 500 themselves are included.

IN filters against a list of specific values — it's shorthand for chaining multiple OR equals conditions. WHERE country IN ('USA', 'Canada', 'India') beats writing three separate OR conditions, especially when that list grows to ten items.

LIKE is for pattern matching on text. The % symbol means 'any sequence of characters', and the _ symbol means 'exactly one character'. WHERE customer_name LIKE 'A%' finds every name starting with A. WHERE customer_name LIKE '_a%' finds names where the second character is 'a'.

NOT BETWEEN, NOT IN, and NOT LIKE all work as inverses. They're your go-to tools for exclusion filtering.

between_in_like_operators.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
-- BETWEEN: find orders where total_amount is between $100 and $500 (inclusive)
SELECT order_id, customer_name, total_amount
FROM orders
WHERE total_amount BETWEEN 100 AND 500;  -- Includes rows where amount = 100 or 500 exactly

-- NOT BETWEEN: orders outside that range (under $100 or over $500)
SELECT order_id, customer_name, total_amount
FROM orders
WHERE total_amount NOT BETWEEN 100 AND 500;

-- IN: cleaner alternative to multiple OR conditions
-- Find customers from any of three specific countries
SELECT order_id, customer_name, country
FROM orders
WHERE country IN ('USA', 'Canada', 'India');  -- Much cleaner than 3 separate OR clauses

-- NOT IN: exclude those same countries
SELECT order_id, customer_name, country
FROM orders
WHERE country NOT IN ('USA', 'Canada', 'India');

-- LIKE: pattern matching on text
-- Find all customers whose name STARTS WITH the letter 'A'
SELECT order_id, customer_name
FROM orders
WHERE customer_name LIKE 'A%';  -- % matches zero or more of ANY character

-- Find customers whose name CONTAINS 'son' anywhere
SELECT order_id, customer_name
FROM orders
WHERE customer_name LIKE '%son%';  -- % on both sides = 'son' can appear anywhere

-- _ matches exactly ONE character
-- Find 5-letter names (4 wildcards + 1 specific? No — 5 underscores = exactly 5 chars)
SELECT order_id, customer_name
FROM orders
WHERE customer_name LIKE '_____';  -- Exactly 5 characters in the name
Output
-- BETWEEN $100 AND $500:
order_id | customer_name | total_amount
----------+----------------+--------------
1001 | Alice Johnson | 450.00
1003 | Priya Sharma | 310.00
1004 | Marcus Webb | 120.00
(3 rows)
-- IN ('USA', 'Canada', 'India'):
order_id | customer_name | country
----------+----------------+---------
1001 | Alice Johnson | USA
1003 | Priya Sharma | India
1004 | Marcus Webb | USA
1006 | Sandra Lee | USA
(4 rows)
-- LIKE 'A%' (names starting with A):
order_id | customer_name
----------+---------------
1001 | Alice Johnson
(1 row)
-- LIKE '%son%' (names containing 'son'):
order_id | customer_name
----------+---------------
1001 | Alice Johnson
(1 row)
Interview Gold: BETWEEN Is Always Inclusive
Interviewers love asking whether BETWEEN includes the boundary values. It always does — BETWEEN 100 AND 500 is mathematically >= 100 AND <= 500. If you want to exclude the boundaries, drop back to plain comparison operators: WHERE amount > 100 AND amount < 500.
Production Insight
LIKE '%pattern%' with a leading wildcard forces a full table scan — the index on the column cannot be used.
For full-text search requirements, use a proper full-text index (see Full-Text Search in SQL) rather than LIKE '%term%'.
LIKE 'prefix%' can use an index; LIKE '%suffix' and LIKE '%middle%' cannot.
Key Takeaway
LIKE 'prefix%' uses the index; LIKE '%anything' does not — leading wildcards kill index usage.
For contains-search at scale, full-text indexes (PostgreSQL tsvector, MySQL FULLTEXT) replace LIKE '%term%'.
IN is shorthand for OR on the same column — more readable and equally performant.

Filtering NULL Values — The Special Case That Breaks Beginners

NULL in SQL doesn't mean zero. It doesn't mean an empty string. It means unknown or absent — there is no value there at all. This distinction matters enormously when filtering.

Here's the trap: you cannot use = to check for NULL. Writing WHERE phone_number = NULL will never return any rows — not because there are none, but because NULL = NULL evaluates to NULL (unknown), not TRUE. The database sees 'I don't know if this equals nothing' and skips the row.

The correct operators are IS NULL and IS NOT NULL. These are the only two ways to reliably filter on the presence or absence of a value.

This also affects AND and OR. If any part of a compound condition involves NULL, the result can be NULL rather than TRUE or FALSE, causing rows to silently vanish from your results. When you're debugging a query that returns fewer rows than expected, checking for unexpected NULLs in your filter columns is always a smart first step.

Real-world scenario: a customers table might have a phone_number column where some customers haven't provided a number. IS NULL helps you find them so you can prompt them to update their profile.

filtering_null_values.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- Let's say our 'customers' table has an optional 'phone_number' column.
-- Some customers filled it in, some didn't.

-- WRONG WAY: this returns 0 rows, always, even if NULLs exist
SELECT customer_id, customer_name, phone_number
FROM customers
WHERE phone_number = NULL;  -- NULL = NULL is NULL (unknown), not TRUE — NEVER do this

-- RIGHT WAY: use IS NULL to find customers with no phone number
SELECT customer_id, customer_name, phone_number
FROM customers
WHERE phone_number IS NULL;  -- 'IS NULL' is the only reliable NULL check

-- IS NOT NULL: find customers who DID provide a phone number
SELECT customer_id, customer_name, phone_number
FROM customers
WHERE phone_number IS NOT NULL;  -- Only returns rows where a value actually exists

-- Combining NULL checks with other conditions
-- Find customers from the USA who haven't provided a phone number
SELECT customer_id, customer_name, country, phone_number
FROM customers
WHERE country = 'USA'
  AND phone_number IS NULL;  -- Both conditions must be true
Output
-- IS NULL result (customers with no phone number):
customer_id | customer_name | phone_number
-------------+----------------+--------------
2002 | Marcus Webb | NULL
2005 | Sandra Lee | NULL
(2 rows)
-- IS NOT NULL result (customers with a phone number on file):
customer_id | customer_name | phone_number
-------------+----------------+--------------
2001 | Alice Johnson | 555-0142
2003 | Priya Sharma | 555-0198
2004 | Tom Nguyen | 555-0237
(3 rows)
Watch Out: NULL Is Not Zero and Not Empty String
WHERE phone_number = '' finds rows with an empty string — a value that exists but contains nothing. WHERE phone_number IS NULL finds rows where no value was ever stored. These are different things. A column can have an empty string AND not be NULL. Always check which you actually have in your data before filtering.
Production Insight
WHERE col = NULL is the most common silent SQL bug — it returns zero rows without an error.
NULL comparisons propagate: NULL = NULL evaluates to UNKNOWN, not TRUE.
COALESCE in WHERE (WHERE COALESCE(col, 'default') = 'default') works but prevents index usage — use IS NULL instead.
Key Takeaway
NULL = NULL is UNKNOWN in SQL, not TRUE — use IS NULL and IS NOT NULL exclusively for NULL checks.
Any arithmetic or comparison involving NULL produces NULL — the result disappears from WHERE results.
IS NOT NULL should be added explicitly to any column that might be NULL when it shouldn't affect results.
● Production incidentPOST-MORTEMseverity: high

Operator Precedence Bug Emailed the Wrong 40,000 Customers

Symptom
Campaign response rates and cost-per-acquisition were drastically off. Investigation revealed the email went to all UK customers regardless of tier, and only premium US customers.
Assumption
The developer wrote WHERE country = 'UK' OR country = 'US' AND tier = 'premium' expecting it to filter premium customers from both countries.
Root cause
AND has higher precedence than OR. The query was evaluated as WHERE country = 'UK' OR (country = 'US' AND tier = 'premium') — all UK customers passed regardless of tier.
Fix
Added parentheses: WHERE (country = 'UK' OR country = 'US') AND tier = 'premium'. Added a mandatory WHERE clause review step to the marketing query approval process.
Key lesson
  • AND has higher precedence than OR in SQL — always use parentheses when mixing the two
  • Test WHERE clause logic with COUNT(*) before running any data modification or bulk send
  • Peer-review all marketing queries against a user count expectation before execution
Production debug guideDiagnosing zero rows, unexpected inclusions, and NULL filter failures3 entries
Symptom · 01
WHERE filter returns zero rows despite data existing
Fix
Check for NULL: WHERE phone = NULL always returns zero rows. Use WHERE phone IS NULL. Also check data type mismatch: WHERE id = '123' (string) on an integer column may work in MySQL but fails in PostgreSQL.
Symptom · 02
OR condition includes rows that should be excluded
Fix
Operator precedence: AND binds before OR. Add parentheses: WHERE (country = 'UK' OR country = 'US') AND tier = 'premium'. Without parentheses, WHERE country = 'UK' OR country = 'US' AND tier = 'premium' only applies the tier filter to US rows.
Symptom · 03
LIKE pattern not matching expected rows
Fix
Check for leading/trailing spaces in the data: TRIM(column) LIKE '%pattern%'. Also verify case sensitivity — LIKE is case-insensitive in MySQL but case-sensitive in PostgreSQL. Use ILIKE for case-insensitive in PostgreSQL.
OperatorWhat It DoesExampleReturns Rows When...
=Exact matchWHERE country = 'USA'Column value exactly equals the given value
!= or <>Not equalWHERE country != 'USA'Column value is anything other than the given value
> / <Greater / Less thanWHERE amount > 300Column value is strictly above or below the threshold
>= / <=Greater or equal / Less or equalWHERE amount >= 300Column value meets or exceeds / meets or falls below threshold
BETWEEN x AND yRange (inclusive both ends)WHERE amount BETWEEN 100 AND 500Value is >= x AND <= y
IN (list)Matches any value in a listWHERE country IN ('USA','India')Column value appears anywhere in the provided list
LIKE 'pattern'Partial text matchWHERE name LIKE 'A%'Text matches the pattern (% = any chars, _ = one char)
IS NULLNo value storedWHERE phone IS NULLColumn contains NULL (no value)
IS NOT NULLA value existsWHERE phone IS NOT NULLColumn contains any non-NULL value
NOTInverts any conditionWHERE NOT country = 'USA'The inner condition evaluates to FALSE

Key takeaways

1
WHERE is evaluated before SELECT
the database finds matching rows first, then picks which columns to display. This is why you can't filter on a column alias defined in your SELECT list.
2
NULL means unknown, not zero or empty. Only IS NULL and IS NOT NULL reliably detect it. Using = NULL always returns nothing, silently, with no error.
3
BETWEEN is always inclusive on both ends
BETWEEN 100 AND 500 includes 100 and 500 themselves. Use plain > and < operators if you need exclusive boundaries.
4
Always use parentheses when mixing AND with OR. AND has higher precedence than OR just like * beats + in maths, so unparenthesised mixed logic produces results that look right but filter incorrectly.

Common mistakes to avoid

3 patterns
×

Using = NULL to compare NULL values

Symptom
WHERE phone_number = NULL returns zero rows silently — even if the column has many NULL rows. No error is thrown.
Fix
Use IS NULL for equality: WHERE phone_number IS NULL. Use IS NOT NULL to exclude nulls. The = operator produces UNKNOWN (not TRUE) when either operand is NULL, and UNKNOWN rows are excluded from WHERE results.
×

Mixing AND and OR without parentheses

Symptom
Wrong rows included or excluded — the query runs without error but the logic is different from intent. WHERE country = 'USA' OR country = 'UK' AND revenue > 1000 filters revenue only for UK rows.
Fix
Always use parentheses when mixing AND and OR: WHERE (country = 'USA' OR country = 'UK') AND revenue > 1000. AND has higher precedence than OR — parentheses make intent explicit and prevent precedence bugs.
×

Wrapping numbers in quotes in WHERE conditions

Symptom
Wrong results or implicit type coercion — WHERE total_amount > '300' performs string comparison ('9' > '300' is true), not numeric comparison
Fix
Match the data type in WHERE to the column's data type. Numbers: WHERE total_amount > 300. Strings: WHERE status = 'active'. Dates: WHERE created_at >= '2026-01-01' (the date string is cast automatically in most databases).
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between WHERE and HAVING in SQL?
Q02JUNIOR
Why does WHERE column = NULL return no rows?
Q03JUNIOR
If you write WHERE status = 'active' OR status = 'pending' AND total > 5...
Q01 of 03JUNIOR

What is the difference between WHERE and HAVING in SQL?

ANSWER
WHERE filters individual rows before GROUP BY runs — it operates on raw row data and cannot reference aggregate functions. HAVING filters groups after GROUP BY and aggregation — it can use aggregate conditions like HAVING COUNT(*) > 5. SQL logical execution order is FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. Both can appear in the same query: WHERE filters rows before grouping (reducing the rows that get aggregated), and HAVING filters groups after aggregation (removing groups that don't meet aggregate conditions).
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
Can you use the WHERE clause without a SELECT statement?
02
What is the difference between WHERE and HAVING in SQL?
03
Does the order of conditions in a WHERE clause affect performance?
🔥

That's SQL Basics. Mark it forged?

4 min read · try the examples if you haven't

Previous
SQL SELECT Statement
5 / 16 · SQL Basics
Next
SQL ORDER BY and LIMIT