SQL WHERE Clause
WHERE filters rows before they’re returned by a query. Only rows where the condition evaluates to TRUE are included — FALSE and NULL conditions exclude the row.
WHERE works with SELECT, UPDATE, and DELETE.
Basic Syntax
SELECT columnsFROM table_nameWHERE condition;Comparison Operators
WHERE price = 149.99 -- equal toWHERE price != 149.99 -- not equal (also: <>)WHERE price > 100 -- greater thanWHERE price >= 100 -- greater than or equalWHERE price < 500 -- less thanWHERE price <= 499.99 -- less than or equal
WHERE status = 'active' -- string comparison (case-sensitive in PostgreSQL)WHERE name != 'archived'AND, OR, NOT
-- All conditions must be trueWHERE category = 'electronics' AND price < 300 AND is_active = TRUE
-- Either condition can be trueWHERE city = 'London' OR city = 'Manchester'
-- Negate a conditionWHERE NOT is_deleted
-- Parentheses for clarity (critical when mixing AND and OR)WHERE (category = 'books' OR category = 'music') AND price < 20Operator precedence: NOT evaluates first, then AND, then OR. Always use parentheses when combining AND and OR — ambiguous expressions cause bugs.
BETWEEN
Inclusive range test (equivalent to >= AND <=):
WHERE price BETWEEN 50 AND 200-- Same as: WHERE price >= 50 AND price <= 200
WHERE created_at BETWEEN '2025-01-01' AND '2025-12-31'
WHERE NOT price BETWEEN 50 AND 200 -- outside the rangeIN: Match a List
WHERE status IN ('pending', 'processing', 'shipped')-- Same as: WHERE status = 'pending' OR status = 'processing' OR status = 'shipped'
WHERE country NOT IN ('US', 'CA', 'MX')
-- IN with subqueryWHERE customer_id IN ( SELECT customer_id FROM customers WHERE country = 'UK')NOT IN with NULLs: If the list contains any NULLs, NOT IN returns no rows. Use NOT EXISTS instead when the subquery might return NULLs.
LIKE: Pattern Matching
WHERE name LIKE 'A%' -- starts with 'A'WHERE email LIKE '%.com' -- ends with '.com'WHERE description LIKE '%SQL%' -- contains 'SQL'WHERE code LIKE '___-__' -- exact pattern: 3 chars, dash, 2 chars
WHERE name NOT LIKE 'test%' -- does not start with 'test'
-- PostgreSQL: case-insensitive LIKEWHERE name ILIKE '%smith%'% matches any sequence of characters (including empty). _ matches exactly one character.
Performance: LIKE '%text%' (leading wildcard) can’t use a B-tree index — full table scan. LIKE 'text%' (trailing wildcard only) can use an index.
NULL Handling
NULL means unknown — not zero, not empty. Standard comparisons never match NULL:
WHERE phone IS NULL -- correct: find rows with no phoneWHERE phone IS NOT NULL -- correct: find rows with a phone
WHERE phone = NULL -- WRONG: always returns no rowsWHERE phone != NULL -- WRONG: always returns no rows-- Find customers without a phone number on fileSELECT name, email FROM customers WHERE phone IS NULL;
-- Find orders where the shipping date hasn't been setSELECT order_id FROM orders WHERE shipped_at IS NULL AND status = 'pending';WHERE with Date Conditions
WHERE order_date = '2025-06-15'WHERE order_date >= '2025-01-01'WHERE order_date BETWEEN '2025-01-01' AND '2025-06-30'
-- Recent records (PostgreSQL)WHERE created_at >= NOW() - INTERVAL '30 days'WHERE created_at >= CURRENT_DATE - 7
-- Year filter (sargable — can use index)WHERE created_at >= '2025-01-01' AND created_at < '2026-01-01'
-- Non-sargable (avoids index — don't do this)WHERE YEAR(created_at) = 2025WHERE DATE(created_at) = '2025-06-15'Subquery in WHERE
-- Orders from customers in the UKWHERE customer_id IN ( SELECT customer_id FROM customers WHERE country = 'UK')
-- Products that have been ordered (EXISTS is often faster)WHERE EXISTS ( SELECT 1 FROM order_items oi WHERE oi.product_id = products.product_id)
-- Products that have NEVER been orderedWHERE NOT EXISTS ( SELECT 1 FROM order_items oi WHERE oi.product_id = products.product_id)
-- Products priced above the overall average (scalar subquery)WHERE price > (SELECT AVG(price) FROM products)WHERE vs HAVING
-- WHERE filters individual rows BEFORE groupingSELECT category, SUM(amount)FROM ordersWHERE status = 'completed' -- filter rows firstGROUP BY categoryHAVING SUM(amount) > 10000; -- filter groups afterYou can’t use aggregate functions in WHERE. That’s what HAVING is for.
Practical Examples
Active customers in specific cities:
SELECT name, email, cityFROM customersWHERE is_active = TRUE AND city IN ('London', 'Manchester', 'Birmingham')ORDER BY city, name;Overdue invoices:
SELECT invoice_id, customer_id, amount, due_dateFROM invoicesWHERE status = 'unpaid' AND due_date < CURRENT_DATEORDER BY due_date;Products with recent sales:
SELECT p.product_id, p.nameFROM products pWHERE EXISTS ( SELECT 1 FROM order_items oi JOIN orders o ON oi.order_id = o.order_id WHERE oi.product_id = p.product_id AND o.created_at >= NOW() - INTERVAL '90 days');