SQL WHERE Clause
The WHERE clause filters rows before they’re returned by a query. Only rows where the condition evaluates to TRUE are included. This is true for SELECT, UPDATE, DELETE, and any other statement that reads from a table.
Basic Syntax
SELECT column1, column2FROM table_nameWHERE condition;Comparison Operators
-- EqualWHERE status = 'active'
-- Not equalWHERE status != 'cancelled'-- OR (same meaning)WHERE status <> 'cancelled'
-- Greater than / less thanWHERE price > 50WHERE price >= 50WHERE price < 100WHERE price <= 99.99
-- Range (inclusive on both ends)WHERE price BETWEEN 10 AND 50-- Equivalent to: WHERE price >= 10 AND price <= 50
-- NOT BETWEENWHERE price NOT BETWEEN 10 AND 50AND, OR, NOT
Combine multiple conditions using logical operators:
-- Both conditions must be trueSELECT * FROM productsWHERE category = 'electronics' AND price < 200;
-- Either condition can be trueSELECT * FROM customersWHERE city = 'London' OR city = 'Manchester';
-- Negate a conditionSELECT * FROM ordersWHERE NOT status = 'cancelled';-- More readable as: WHERE status != 'cancelled'Operator precedence: NOT is evaluated first, then AND, then OR. Use parentheses when combining AND and OR to be explicit:
-- Ambiguous without parenthesesWHERE category = 'books' OR category = 'music' AND price > 20
-- This is what the database actually evaluates (AND first):WHERE category = 'books' OR (category = 'music' AND price > 20)
-- If you meant something else, add parentheses:WHERE (category = 'books' OR category = 'music') AND price > 20Always add parentheses when mixing AND and OR — it makes the intent clear and prevents bugs.
IN: Matching a List of Values
IN is a clean way to test membership in a list, without chaining multiple OR conditions:
-- Instead of:WHERE status = 'pending' OR status = 'active' OR status = 'processing'
-- Use:WHERE status IN ('pending', 'active', 'processing')
-- NOT INWHERE country NOT IN ('US', 'CA', 'MX')IN also accepts a subquery:
-- Orders from customers in LondonWHERE customer_id IN ( SELECT customer_id FROM customers WHERE city = 'London')Watch out: NOT IN with a subquery fails silently if the subquery returns any NULL values. Use NOT EXISTS instead in those cases:
-- Safer than NOT IN when subquery might return NULLsWHERE NOT EXISTS ( SELECT 1 FROM blacklisted_emails b WHERE b.email = users.email)LIKE: Pattern Matching
LIKE matches string patterns using two wildcards:
%— matches any sequence of characters (including empty)_— matches exactly one character
-- Starts with 'J'WHERE name LIKE 'J%'
-- Ends with '.com'WHERE email LIKE '%.com'
-- Contains 'smith' anywhereWHERE name LIKE '%smith%'
-- Exactly 5 charactersWHERE code LIKE '_____'
-- Second character is 'a'WHERE product_name LIKE '_a%'
-- Negate the patternWHERE email NOT LIKE '%@gmail.com'Performance note: % at the start of a pattern (LIKE '%smith%') prevents index usage — the database must scan every row. % at the end only (LIKE 'smith%') can use a B-tree index efficiently.
Case sensitivity: LIKE is case-sensitive in PostgreSQL and case-insensitive in MySQL. Use ILIKE in PostgreSQL for case-insensitive matching:
WHERE name ILIKE '%smith%' -- PostgreSQL onlyWHERE LOWER(name) LIKE '%smith%' -- works everywhereNULL Handling
NULL is not a value — it represents the absence of a value. Standard comparison operators don’t work with NULL:
-- This returns NO rows, even if phone IS NULLWHERE phone = NULL -- wrong
-- This is the correct syntaxWHERE phone IS NULL
-- Find rows where phone exists (is not null)WHERE phone IS NOT NULLNULL compared to anything — including another NULL — returns NULL (neither TRUE nor FALSE). This is why WHERE column = NULL always returns no rows.
-- Find customers without a phone numberSELECT name, email FROM customers WHERE phone IS NULL;
-- Find customers who have provided all contact infoSELECT name FROM customers WHERE phone IS NOT NULL AND email IS NOT NULL;WHERE with Dates
Date comparisons work like numeric comparisons — the string literal is implicitly cast to a date:
-- Exact dateWHERE order_date = '2025-06-01'
-- RangeWHERE order_date BETWEEN '2025-01-01' AND '2025-06-30'WHERE order_date >= '2025-01-01' AND order_date < '2026-01-01'
-- Recent records (PostgreSQL)WHERE created_at >= NOW() - INTERVAL '30 days'
-- Specific year (non-sargable: wrapping in a function blocks index use)WHERE YEAR(order_date) = 2025 -- bad: can't use index
-- Better: range comparison (sargable: can use index)WHERE order_date >= '2025-01-01' AND order_date < '2026-01-01'WHERE vs HAVING
WHERE filters individual rows before grouping. HAVING filters groups after aggregation:
-- WHERE: filter before aggregationSELECT category, SUM(amount)FROM ordersWHERE status = 'completed' -- filter rows firstGROUP BY categoryHAVING SUM(amount) > 10000; -- then filter groupsYou cannot use aggregate functions (SUM, COUNT, etc.) in a WHERE clause — that’s what HAVING is for.
Subqueries in WHERE
-- Products that have at least one orderSELECT product_id, nameFROM productsWHERE product_id IN ( SELECT DISTINCT product_id FROM order_items);
-- Or equivalently with EXISTS (often faster for large datasets):SELECT p.product_id, p.nameFROM products pWHERE EXISTS ( SELECT 1 FROM order_items oi WHERE oi.product_id = p.product_id);Practical Examples
Find overdue invoices:
SELECT invoice_id, customer_id, amount, due_dateFROM invoicesWHERE status = 'unpaid' AND due_date < CURRENT_DATEORDER BY due_date;Find products low on stock:
SELECT name, sku, stock_quantityFROM productsWHERE is_active = TRUE AND stock_quantity BETWEEN 1 AND 10ORDER BY stock_quantity;Find users who haven’t logged in recently:
SELECT user_id, email, last_login_atFROM usersWHERE last_login_at < NOW() - INTERVAL '90 days' OR last_login_at IS NULL;