SQL Cheatsheet

Filtering (WHERE)

Use this SQL reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

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

OperatorMeaningExample
=EqualWHERE status = 'active'
<> / !=Not equalWHERE status <> 'deleted'
<Less thanWHERE age < 18
>Greater thanWHERE price > 99.99
<=Less than or equalWHERE score <= 100
>=Greater than or equalWHERE 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