SQL Cheatsheet
Filtering (WHERE)
Use this SQL reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
WHERE Clause Syntax
WHERE filters rows before any grouping. It follows FROM/JOIN and precedes GROUP BY.
SELECT * FROM employees WHERE department = 'Engineering'; -- Multiple conditions SELECT * FROM orders WHERE status = 'pending' AND total > 100 AND created_at >= '2024-01-01';
Comparison Operators
| Operator | Meaning | Example |
|---|---|---|
= | Equal | WHERE status = 'active' |
<> / != | Not equal | WHERE status <> 'deleted' |
< | Less than | WHERE age < 18 |
> | Greater than | WHERE price > 99.99 |
<= | Less than or equal | WHERE score <= 100 |
>= | Greater than or equal | WHERE created_at >= '2024-01-01' |
Logical Operators
-- AND: both must be true WHERE age > 18 AND country = 'US' -- OR: either must be true WHERE status = 'active' OR status = 'trial' -- NOT: inverts condition WHERE NOT deleted WHERE NOT (status = 'inactive' OR status = 'banned') -- Precedence: NOT > AND > OR -- Always use parentheses to make intent explicit WHERE (country = 'US' OR country = 'CA') AND verified = TRUE
IN and NOT IN
-- Match any value in a list SELECT * FROM products WHERE category IN ('Electronics', 'Books', 'Toys'); -- NOT IN SELECT * FROM users WHERE role NOT IN ('admin', 'moderator'); -- IN with subquery SELECT * FROM orders WHERE user_id IN ( SELECT id FROM users WHERE plan = 'premium' ); -- Gotcha: NOT IN with NULLs returns no rows -- If subquery can return NULL, use NOT EXISTS instead SELECT * FROM a WHERE id NOT IN (SELECT id FROM b); -- broken if b has NULLs SELECT * FROM a WHERE NOT EXISTS ( SELECT 1 FROM b WHERE b.id = a.id ); -- safe
BETWEEN
-- Inclusive on both ends SELECT * FROM products WHERE price BETWEEN 10 AND 100; -- Equivalent to: WHERE price >= 10 AND price <= 100 -- NOT BETWEEN SELECT * FROM events WHERE event_date NOT BETWEEN '2024-01-01' AND '2024-12-31'; -- Works on strings (lexicographic order) SELECT * FROM employees WHERE last_name BETWEEN 'A' AND 'M'; -- Works on dates SELECT * FROM orders WHERE order_date BETWEEN '2024-01-01' AND '2024-03-31';
LIKE and Pattern Matching
-- % matches zero or more characters WHERE email LIKE '%@gmail.com' -- ends with @gmail.com WHERE name LIKE 'A%' -- starts with A WHERE description LIKE '%keyword%' -- contains keyword -- _ matches exactly one character WHERE code LIKE 'US-_' -- US-A, US-B, etc. WHERE phone LIKE '___-___-____' -- NNN-NNN-NNNN -- NOT LIKE WHERE name NOT LIKE 'test%' -- ILIKE — case-insensitive (PostgreSQL) WHERE email ILIKE '%@GMAIL.COM' -- ESCAPE: treat % or _ literally WHERE path LIKE '100\%' ESCAPE '\' -- literal percent sign -- MySQL: LIKE is case-insensitive for non-binary columns by default -- SQLite: LIKE is case-insensitive for ASCII letters by default
Regular Expressions
-- PostgreSQL: ~ (case-sensitive), ~* (case-insensitive) WHERE email ~ '^[a-z]+@' WHERE name ~* 'alice' -- PostgreSQL: !~ (not match), !~* (not match, case-insensitive) WHERE name !~ '^[0-9]' -- PostgreSQL: SIMILAR TO (SQL standard regex, limited) WHERE name SIMILAR TO '[A-Z][a-z]+' -- MySQL: REGEXP / RLIKE WHERE email REGEXP '^[a-z]' WHERE name RLIKE 'alice|bob' -- SQL Server: no native regex without CLR; use LIKE -- REGEXP_LIKE (Oracle, MySQL 8+) WHERE REGEXP_LIKE(email, '^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$')
NULL Checks
-- Correct WHERE phone IS NULL WHERE phone IS NOT NULL -- Wrong — always evaluates to NULL, never TRUE WHERE phone = NULL -- never use this WHERE phone != NULL -- never use this -- IS DISTINCT FROM: NULL-safe not-equal (PostgreSQL, SQL:2003) WHERE a IS DISTINCT FROM b -- TRUE even when one side is NULL WHERE a IS NOT DISTINCT FROM b -- TRUE when both are NULL -- COALESCE to treat NULL as a value WHERE COALESCE(discount, 0) > 0
Filtering on Dates and Times
-- Exact date WHERE order_date = '2024-06-15' -- Date range (half-open interval avoids time-of-day issues) WHERE created_at >= '2024-01-01' AND created_at < '2024-02-01' -- Relative dates (PostgreSQL) WHERE created_at >= NOW() - INTERVAL '7 days' WHERE created_at >= CURRENT_DATE - INTERVAL '30 days' -- Extract date part WHERE EXTRACT(YEAR FROM created_at) = 2024 WHERE EXTRACT(DOW FROM created_at) = 0 -- 0 = Sunday (PostgreSQL) -- Date trunc equality WHERE DATE_TRUNC('month', created_at) = '2024-06-01' -- MySQL equivalents WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) WHERE YEAR(created_at) = 2024 WHERE MONTH(created_at) = 6
EXISTS and NOT EXISTS
-- EXISTS: true if subquery returns at least one row (short-circuits) SELECT * FROM users u WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.id ); -- NOT EXISTS: true if subquery returns no rows SELECT * FROM products p WHERE NOT EXISTS ( SELECT 1 FROM order_items oi WHERE oi.product_id = p.id ); -- EXISTS is usually faster than IN for large subqueries -- because it short-circuits on the first match
Filtering on Aggregates — HAVING vs WHERE
WHERE filters individual rows (before aggregation). HAVING filters groups (after aggregation).
-- WRONG: aggregate in WHERE SELECT department, AVG(salary) FROM employees WHERE AVG(salary) > 70000 -- ERROR GROUP BY department; -- CORRECT: use HAVING SELECT department, AVG(salary) AS avg_sal FROM employees GROUP BY department HAVING AVG(salary) > 70000; -- Both WHERE and HAVING in same query SELECT department, COUNT(*) AS cnt FROM employees WHERE status = 'active' -- filter rows first GROUP BY department HAVING COUNT(*) >= 5; -- then filter groups
Advanced Filtering Patterns
-- Filter on window function result (must wrap in subquery/CTE) SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn FROM employees ) ranked WHERE rn = 1; -- ANY / ALL with subquery WHERE salary > ANY (SELECT salary FROM employees WHERE dept = 'Intern') WHERE salary > ALL (SELECT salary FROM employees WHERE dept = 'Manager') -- ANY = at least one; ALL = every row in subquery -- FILTER clause on aggregates (PostgreSQL) SELECT COUNT(*) FILTER (WHERE status = 'active') AS active_count, COUNT(*) FILTER (WHERE status = 'inactive') AS inactive_count FROM users; -- Bitmap/array containment (PostgreSQL) WHERE tags @> ARRAY['sql','database'] -- array contains all elements WHERE 'premium' = ANY(roles) -- element in array -- JSONB filtering (PostgreSQL) WHERE profile->>'plan' = 'premium' WHERE profile @> '{"verified": true}'
String Filtering Tips
-- Full-text search (PostgreSQL) WHERE to_tsvector('english', body) @@ to_tsquery('english', 'sql & tutorial') -- Full-text search (MySQL) WHERE MATCH(title, body) AGAINST ('sql tutorial' IN NATURAL LANGUAGE MODE) -- Case-insensitive equality without ILIKE WHERE LOWER(email) = LOWER('User@Example.com') WHERE UPPER(code) = 'ABC123' -- Starts with (can use index if LIKE 'prefix%') WHERE username LIKE 'john%' -- Contains (cannot use standard index) WHERE description LIKE '%keyword%' -- Use full-text index or trigram index (PostgreSQL pg_trgm) for contains queries