Home Database SQL WHERE: AND/OR Precedence Bug Sent 40k Wrong Customers
Beginner 7 min · March 05, 2026
SQL WHERE Clause and Filtering

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 & Principal Engineer

20+ years shipping high-throughput database systems. Written from production experience, not tutorials.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 20 min
  • Basic programming fundamentals
  • A computer with internet access
  • Willingness to follow along with examples
 ● 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
✦ Definition~90s read
What is SQL WHERE Clause and Filtering?

The SQL WHERE clause is the gatekeeper of row-level filtering in a query, executing after the FROM clause identifies the source table(s) but before GROUP BY, HAVING, or ORDER BY. Its job is simple: evaluate a boolean expression against each row and discard any row where that expression is FALSE or UNKNOWN.

Imagine you have a massive filing cabinet with thousands of customer folders.

This is where you turn a table of millions into the handful of rows you actually care about — but the order of operations matters. WHERE cannot reference column aliases from SELECT because the filtering happens before the projection, a common trap that wastes hours in debugging. The clause is mandatory for any production query that touches more than a few hundred rows; omitting it on a large table is how you accidentally email 40,000 customers instead of 40.

When combining conditions, AND and OR follow standard boolean precedence: AND binds tighter than OR, meaning WHERE status = 'active' OR status = 'pending' AND age > 18 is parsed as status = 'active' OR (status = 'pending' AND age > 18), not (status = 'active' OR status = 'pending') AND age > 18. This is the exact bug that caused the 40k-customer fiasco in the article — a missing pair of parentheses shifted the logic, and the database happily returned every active customer regardless of age.

Always parenthesize OR combinations explicitly; your future self and your on-call pager will thank you. For complex filtering, consider breaking conditions into a CASE expression or a derived table rather than stacking five levels of nested AND/OR.

Power operators like BETWEEN, IN, and LIKE exist to replace verbose chains of AND/OR and improve both readability and performance. BETWEEN is inclusive on both ends (WHERE salary BETWEEN 50000 AND 100000 includes 50000 and 100000), which surprises developers who expect half-open intervals. IN is syntactic sugar for multiple OR conditions but can degrade performance on large lists — PostgreSQL, for example, may switch from an index scan to a sequential scan if the IN list exceeds a few hundred values. LIKE with a leading wildcard ('%pattern') kills index usage in most databases; use full-text search or trigram indexes instead. For NULL filtering, remember that NULL = NULL is not TRUE — it's UNKNOWN, so WHERE column = NULL returns zero rows.

You must use IS NULL or IS NOT NULL, and be aware that NOT IN with a subquery that returns any NULL will produce zero results because NOT IN is equivalent to != ALL(...), and any comparison with NULL yields UNKNOWN. This is not a bug; it's SQL's three-valued logic working as designed, and it will break your query silently every time you forget it.

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.
sql-where-clause-filtering WHERE Clause Filtering Layers Hierarchical processing of conditions in SQL query Row Source Table Scan | Index Seek | Join Results NULL Handling IS NULL Check | IS NOT NULL Check | Three-Valued Logic Operator Precedence NOT Evaluation | AND Grouping | OR Combination Power Operators BETWEEN Range | IN List | LIKE Pattern Subquery Layer Correlated Subquery | EXISTS Check | IN Subquery THECODEFORGE.IO
thecodeforge.io
Sql Where Clause Filtering

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.
sql-where-clause-filtering THECODEFORGE.IO SQL WHERE Clause Execution Layers Hierarchical filter processing in query engine Query Parser Tokenize WHERE | Build expression tree Operator Precedence NOT highest | AND middle | OR lowest Condition Evaluators Comparison operators | BETWEEN/IN/LIKE | NULL checks Row Filter Engine Apply conditions per row | Short-circuit evaluation Result Set Matching rows | Excluded rows THECODEFORGE.IO
thecodeforge.io
Sql Where Clause Filtering

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.

The WHERE Clause Execution Order — Why Filtering Before JOIN Saves Hours

Your junior thinks WHERE runs on the final result set. In production, that misconception kills performance. The WHERE clause filters rows before JOINs and GROUP BY execute, not after. This is critical for reducing the amount of data entering memory-bound operations. If you filter after a JOIN, you force the engine to join millions of rows only to discard most. Filter early with WHERE. Push predicates into JOIN conditions when you can—that's called 'predicate pushdown' and it's how senior engineers keep queries sub-second. Why does this matter? Because a query that runs in 200ms with early filtering can blow up to 20 seconds when filters are misplaced. Always put your most selective filter first in WHERE. The optimizer might reorder it, but it's a good habit. Test with EXPLAIN to see the actual execution plan. If you see a full table scan before a filter, you've broken the order.

ExecutionOrderExample.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- io.thecodeforge
-- Wrong: filter after JOIN causes unnecessary work
SELECT o.order_id, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at >= '2025-01-01';

-- Right: filter before JOIN reduces row count early
SELECT o.order_id, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
   AND o.created_at >= '2025-01-01';

-- Alternative: ctid scan killers
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE customer_id IN (SELECT id FROM customers WHERE status = 'active');
Output
Execution plan shows filter pushed to table scan (cost down 80%)
⚠ Production Trap:
Using WHERE after a JOIN to filter the outer table? You just killed your index. Move that filter into the ON clause to keep index scans alive.
🎯 Key Takeaway
WHERE filters before JOINs. Push predicates down. Always check EXPLAIN ANALYZE for accidental full scans.

Correlated Subqueries in WHERE — The Silent Serial Execution Trap

You've seen it: a WHERE clause with a subquery that runs once per row. That's a correlated subquery, and it's the fastest way to turn a 50ms query into a 5-minute one. The engine executes the outer query first, then for each row, runs the inner subquery. This is serial—no parallelism. Why do juniors write them? Because they look clean. 'SELECT * FROM orders WHERE customer_id IN (SELECT id FROM customers WHERE status = ''active'')' is actually fine if the subquery is uncorrelated—the engine caches it. But the moment you reference the outer table inside that subquery, you've created a nested loop where each iteration scans the same rows. Replace it with a JOIN or a window function. In production, one correlated subquery in a WHERE clause can spike CPU to 100% and lock your table for writes. Always test with a small dataset first. If you see 'SubPlan' in the EXPLAIN output, you're doing it wrong. The fix is almost always a JOIN with a DISTINCT or LATERAL join.

CorrelatedSubqueryFix.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- io.thecodeforge
-- Bad: correlated subquery runs N times
SELECT *
FROM orders o
WHERE o.total > (
    SELECT AVG(total)
    FROM orders o2
    WHERE o2.customer_id = o.customer_id
);

-- Good: single pass with window function
WITH customer_avg AS (
    SELECT customer_id, AVG(total) as avg_total
    FROM orders
    GROUP BY customer_id
)
SELECT o.*
FROM orders o
JOIN customer_avg ca ON o.customer_id = ca.customer_id
WHERE o.total > ca.avg_total;
Output
First query: 12 seconds. With window function: 0.2 seconds.
🔥Refactoring Trick:
If your WHERE subquery references the outer table alias, it's correlated. Rewrite it as a JOIN with a derived table or CTE. Your database will thank you.
🎯 Key Takeaway
Correlated subqueries in WHERE = serial execution. Replace them with JOINs or window functions to keep queries parallelizable.

JSON/JSONB Querying in WHERE Clauses

Modern PostgreSQL databases often store semi-structured data in JSON or JSONB columns. Filtering such data in the WHERE clause requires special operators. The -> operator extracts a JSON object field as JSON, while ->> extracts it as text. For JSONB, the @> operator checks if a JSONB document contains a key/value pair. For example, to find customers whose metadata includes a 'vip' status: SELECT * FROM customers WHERE metadata @> '{"status": "vip"}'. You can also use ? for key existence: WHERE metadata ? 'email_verified'. For nested paths, use #>> to extract by path: WHERE metadata #>> '{address, city}' = 'New York'. These operators are indexable with GIN indexes on JSONB columns, drastically improving performance. Always use JSONB over JSON for queryable columns, as JSONB supports indexing and more operators. Avoid extracting fields in WHERE clauses using functions like json_extract_path_text() as they prevent index usage. Instead, use the native operators. Also beware of NULL handling: a missing key returns NULL, not false, so combine with IS NOT NULL if needed. Proper JSON/JSONB filtering can reduce application-side parsing and enable complex conditions directly in SQL.

json_filtering.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- Find customers with VIP status in JSONB metadata
SELECT * FROM customers
WHERE metadata @> '{"status": "vip"}';

-- Find customers who have email_verified key
SELECT * FROM customers
WHERE metadata ? 'email_verified';

-- Find customers in city 'New York' using path extraction
SELECT * FROM customers
WHERE metadata #>> '{address, city}' = 'New York';

-- Create GIN index for performance
CREATE INDEX idx_customers_metadata ON customers USING GIN (metadata);
🔥JSONB vs JSON
📊 Production Insight
In production, always create GIN indexes on JSONB columns used in WHERE filters. Monitor query plans to ensure index usage; avoid functions like json_extract_path_text() that disable index scans.
🎯 Key Takeaway
JSON/JSONB columns can be filtered in WHERE using operators like @>, ?, and #>>, and benefit from GIN indexes for fast queries.

LATERAL JOIN for Row-by-Row Filtering

The LATERAL JOIN allows a subquery to reference columns from preceding tables in the FROM clause, enabling row-by-row filtering that is impossible with regular joins. For example, to find the most recent order for each customer: SELECT c., o.order_date FROM customers c LEFT JOIN LATERAL (SELECT order_date FROM orders WHERE customer_id = c.id ORDER BY order_date DESC LIMIT 1) o ON true. The LATERAL subquery executes once per customer row, making it powerful for top-N-per-group queries. It can also be used in WHERE clauses indirectly by filtering the outer query based on LATERAL results. For instance, to get customers whose last order was in 2023: SELECT c. FROM customers c WHERE EXISTS (SELECT 1 FROM LATERAL (SELECT order_date FROM orders WHERE customer_id = c.id ORDER BY order_date DESC LIMIT 1) o WHERE o.order_date >= '2023-01-01'). LATERAL joins are more efficient than correlated subqueries in SELECT because they can use indexes on the inner table. However, they are still row-by-row, so ensure proper indexing on the join key (e.g., orders.customer_id). Use LATERAL when you need to compute a value per row that depends on the outer row, especially for complex aggregations or filtering on derived data.

lateral_filtering.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
-- Find customers with their most recent order date
SELECT c.*, o.last_order_date
FROM customers c
LEFT JOIN LATERAL (
    SELECT order_date AS last_order_date
    FROM orders
    WHERE customer_id = c.id
    ORDER BY order_date DESC
    LIMIT 1
) o ON true;

-- Filter customers whose last order was in 2023
SELECT c.*
FROM customers c
WHERE EXISTS (
    SELECT 1 FROM LATERAL (
        SELECT order_date
        FROM orders
        WHERE customer_id = c.id
        ORDER BY order_date DESC
        LIMIT 1
    ) o
    WHERE o.order_date >= '2023-01-01'
);

-- Ensure index on orders.customer_id for performance
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
💡LATERAL vs Correlated Subquery
📊 Production Insight
Use LATERAL joins sparingly in high-volume queries; they are not set-based. Ensure indexes on the inner table's join columns. For simple lookups, a regular JOIN with aggregation may be faster.
🎯 Key Takeaway
LATERAL JOIN enables row-by-row filtering by allowing subqueries to reference outer columns, ideal for top-N-per-group and complex per-row conditions.
AND/OR Precedence: Correct vs Buggy How parentheses prevent wrong customer selection Correct: With Parentheses Buggy: Without Parentheses Condition grouping Explicit (A OR B) AND C Implicit A OR B AND C Evaluation order AND evaluated after OR group AND evaluated before OR Result set size 40k correct customers 40k wrong customers Developer intent Matches logical expectation Violates intended logic Fix required None Add parentheses around OR THECODEFORGE.IO
thecodeforge.io
Sql Where Clause Filtering

Full-Text Search WHERE Conditions: tsvector, MATCH, CONTAINS

Full-text search in PostgreSQL uses tsvector and tsquery types for efficient text matching. Instead of LIKE '%word%', which cannot use indexes, use to_tsvector('english', column) @@ to_tsquery('english', 'word'). For example, to find articles containing 'database' and 'tutorial': SELECT FROM articles WHERE to_tsvector('english', title) @@ to_tsquery('english', 'database & tutorial'). You can also use plainto_tsquery for simpler input: @@ plainto_tsquery('english', 'database tutorial'). For prefix matching, use : syntax: to_tsquery('english', 'datab:*'). To improve performance, create a GIN index on the tsvector column: CREATE INDEX idx_articles_tsv ON articles USING GIN (to_tsvector('english', title)). Some databases like SQL Server use CONTAINS and FREETEXT, but PostgreSQL uses the @@ operator. For phrase searches, use phraseto_tsquery. Full-text search is language-aware, handling stemming and stop words. Avoid using LIKE for text search in production; always use full-text search with indexes for scalability. Combine with WHERE conditions on other columns for precise filtering.

fulltext_search.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- Find articles containing 'database' and 'tutorial'
SELECT * FROM articles
WHERE to_tsvector('english', title) @@ to_tsquery('english', 'database & tutorial');

-- Simple phrase search
SELECT * FROM articles
WHERE to_tsvector('english', title) @@ phraseto_tsquery('english', 'database tutorial');

-- Prefix search
SELECT * FROM articles
WHERE to_tsvector('english', title) @@ to_tsquery('english', 'datab:*');

-- Create GIN index
CREATE INDEX idx_articles_tsv ON articles USING GIN (to_tsvector('english', title));
⚠ Avoid LIKE for Text Search
📊 Production Insight
Create GIN indexes on tsvector columns for all text search columns. Use language-specific configurations (e.g., 'english') for accurate stemming. For user input, use plainto_tsquery to avoid syntax errors.
🎯 Key Takeaway
Full-text search in WHERE uses tsvector and tsquery with the @@ operator, supporting stemming, ranking, and efficient GIN indexes.
● 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
⚙ Quick Reference
9 commands from this guide
FileCommand / CodePurpose
basic_where_filter.sqlSELECT order_id, customer_name, country, total_amountWhat the WHERE Clause Actually Does (And Why It Belongs Afte
and_or_not_conditions.sqlSELECT order_id, customer_name, country, total_amountCombining Conditions with AND, OR, and NOT
between_in_like_operators.sqlSELECT order_id, customer_name, total_amountBETWEEN, IN, and LIKE
filtering_null_values.sqlSELECT customer_id, customer_name, phone_numberFiltering NULL Values
ExecutionOrderExample.sqlSELECT o.order_id, c.nameThe WHERE Clause Execution Order
CorrelatedSubqueryFix.sqlSELECT *Correlated Subqueries in WHERE
json_filtering.sqlSELECT * FROM customersJSON/JSONB Querying in WHERE Clauses
lateral_filtering.sqlSELECT c.*, o.last_order_dateLATERAL JOIN for Row-by-Row Filtering
fulltext_search.sqlSELECT * FROM articlesFull-Text Search WHERE Conditions

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.
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?
N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Written from production experience, not tutorials.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's SQL Basics. Mark it forged?

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

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