← Back to Cheat Sheets

SQL Cheat Sheet

Comprehensive SQL reference — DDL, DML, joins, window functions, CTEs, string/date functions, and performance tips.

DDL — Data Definition

Create Table
CREATE TABLE users (
  id INT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  email VARCHAR(255) UNIQUE,
  age INT CHECK (age >= 0),
  dept_id INT REFERENCES departments(id),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Define a table with column types and constraints.
Alter Table
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
ALTER TABLE users DROP COLUMN phone;
ALTER TABLE users RENAME COLUMN name TO full_name;
ALTER TABLE users ALTER COLUMN age SET NOT NULL;
ALTER TABLE users ADD CONSTRAINT uq_email UNIQUE(email);
Modify table structure — add, drop, rename columns and constraints.
Create Index
CREATE INDEX idx_email ON users(email);
CREATE UNIQUE INDEX idx_uq ON users(email);
CREATE INDEX idx_composite ON orders(customer_id, order_date DESC);
DROP INDEX idx_email;
Speed up lookups with single or composite indexes.
DROP & TRUNCATE
DROP TABLE IF EXISTS temp_data;     -- Remove table + data
DROP TABLE orders CASCADE;          -- Drop with dependents
TRUNCATE TABLE staging_data;        -- Delete all rows fast (no rollback)
DELETE FROM logs WHERE dt < '2023-01-01'; -- Row-by-row delete
DROP removes structure, TRUNCATE clears data fast, DELETE removes selectively.
Views
CREATE VIEW active_users AS
SELECT * FROM users WHERE status = 'active';

CREATE OR REPLACE VIEW monthly_revenue AS
SELECT DATE_TRUNC('month', order_date) AS month,
       SUM(amount) AS revenue
FROM orders GROUP BY 1;

DROP VIEW IF EXISTS active_users;
Virtual tables based on saved queries.
Temporary Tables
CREATE TEMPORARY TABLE temp_results AS
SELECT * FROM orders WHERE status = 'pending';

-- Session-scoped: auto-dropped when session ends
-- Useful for intermediate results in multi-step queries
Session-scoped tables for intermediate processing.

DML — Insert, Update, Delete

INSERT
INSERT INTO users (name, email) VALUES ('Alice', 'a@co.com');

-- Insert multiple rows
INSERT INTO users (name, email) VALUES
  ('Bob', 'b@co.com'),
  ('Carol', 'c@co.com');

-- Insert from SELECT
INSERT INTO archive SELECT * FROM orders WHERE year = 2022;
Add new rows — single, batch, or from a query.
UPDATE
UPDATE employees SET salary = salary * 1.10
WHERE dept = 'Engineering';

-- Update with JOIN
UPDATE orders o
SET o.status = 'cancelled'
FROM customers c
WHERE o.customer_id = c.id AND c.is_blocked = TRUE;
Modify existing rows with optional join-based updates.
DELETE
DELETE FROM logs WHERE created_at < '2023-01-01';

-- Delete with subquery
DELETE FROM orders
WHERE customer_id IN (
  SELECT id FROM customers WHERE status = 'inactive'
);
Remove rows by condition or subquery.
MERGE / UPSERT
MERGE INTO target t
USING source s ON t.id = s.id
WHEN MATCHED THEN
  UPDATE SET t.name = s.name, t.updated_at = NOW()
WHEN NOT MATCHED THEN
  INSERT (id, name) VALUES (s.id, s.name);
Upsert — insert new rows, update existing ones in a single statement.

SELECT — Filtering & Sorting

WHERE Operators
WHERE salary > 80000                 -- Comparison
WHERE dept IN ('Eng', 'Sales')       -- List match
WHERE salary BETWEEN 50000 AND 90000 -- Range
WHERE name LIKE 'A%'                 -- Starts with A
WHERE name ILIKE '%smith%'           -- Case-insensitive (Postgres/SF)
WHERE email IS NOT NULL              -- NULL check
WHERE NOT (age < 18)                 -- Negation
All common WHERE clause operators.
ORDER BY & LIMIT
SELECT * FROM employees
ORDER BY salary DESC, name ASC
LIMIT 10 OFFSET 20;     -- Skip 20, take 10

-- FETCH FIRST (SQL standard)
ORDER BY salary DESC
FETCH FIRST 10 ROWS ONLY;
Sort results and paginate with LIMIT/OFFSET.
DISTINCT & CASE
SELECT DISTINCT dept FROM employees;

SELECT name,
  CASE
    WHEN salary > 100000 THEN 'Senior'
    WHEN salary > 60000  THEN 'Mid'
    ELSE 'Junior'
  END AS level
FROM employees;
Remove duplicates and conditional column logic.
GROUP BY + HAVING
SELECT dept, COUNT(*) AS cnt, AVG(salary) AS avg_sal
FROM employees
GROUP BY dept
HAVING COUNT(*) >= 5
ORDER BY avg_sal DESC;

-- GROUP BY with ROLLUP (subtotals)
SELECT dept, role, SUM(salary)
FROM employees
GROUP BY ROLLUP(dept, role);
Aggregate by groups, filter aggregated results, add subtotals.

JOINs

INNER & LEFT JOIN
-- INNER JOIN: only matching rows
SELECT o.id, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id;

-- LEFT JOIN: all left rows, NULLs for no match
SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.id;
Most common join types for combining tables.
RIGHT, FULL & CROSS
-- RIGHT JOIN: all right rows
SELECT * FROM orders o
RIGHT JOIN products p ON o.product_id = p.id;

-- FULL OUTER JOIN: all rows from both
SELECT * FROM table_a a
FULL OUTER JOIN table_b b ON a.id = b.id;

-- CROSS JOIN: Cartesian product
SELECT * FROM sizes CROSS JOIN colors;
RIGHT, FULL OUTER, and CROSS join variations.
Self Join
-- Find employee and their manager
SELECT e.name AS employee,
       m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
Join a table to itself using aliases.
SEMI & ANTI Join
-- SEMI JOIN: rows in A that have a match in B
SELECT * FROM orders o
WHERE EXISTS (SELECT 1 FROM returns r WHERE r.order_id = o.id);

-- ANTI JOIN: rows in A with NO match in B
SELECT * FROM customers c
WHERE NOT EXISTS (
  SELECT 1 FROM orders o WHERE o.customer_id = c.id
);
Filter rows based on existence in another table.

Subqueries

Scalar Subquery
SELECT name, salary,
  salary - (SELECT AVG(salary) FROM employees) AS diff_from_avg
FROM employees;
Returns a single value, usable in SELECT or WHERE.
Correlated Subquery
-- Employees earning more than their department avg
SELECT name, salary, dept
FROM employees e
WHERE salary > (
  SELECT AVG(salary) FROM employees
  WHERE dept = e.dept  -- references outer query
);
Subquery that references columns from the outer query.
IN / NOT IN
SELECT * FROM customers
WHERE id IN (SELECT customer_id FROM orders WHERE year = 2024);

-- NOT IN: be careful with NULLs!
SELECT * FROM products
WHERE id NOT IN (SELECT product_id FROM order_items
                 WHERE product_id IS NOT NULL);
Filter using result lists from subqueries.

Window Functions

ROW_NUMBER / RANK / DENSE_RANK
SELECT name, dept, salary,
  ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn,
  RANK()       OVER (PARTITION BY dept ORDER BY salary DESC) AS rnk,
  DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS drnk
FROM employees;
-- Ties: ROW_NUMBER=arbitrary, RANK=gaps, DENSE_RANK=no gaps
Assign sequential numbers or ranks within partitions.
LAG / LEAD
SELECT sale_date, amount,
  LAG(amount, 1)  OVER (ORDER BY sale_date) AS prev_day,
  LEAD(amount, 1) OVER (ORDER BY sale_date) AS next_day,
  amount - LAG(amount) OVER (ORDER BY sale_date) AS day_change
FROM daily_sales;
Access previous or next row values for comparisons.
Running Total & Moving Avg
SELECT sale_date, amount,
  SUM(amount) OVER (
    ORDER BY sale_date ROWS UNBOUNDED PRECEDING
  ) AS running_total,
  AVG(amount) OVER (
    ORDER BY sale_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS moving_avg_7d
FROM daily_sales;
Cumulative sums and sliding window averages.
FIRST_VALUE / LAST_VALUE / NTH_VALUE
SELECT dept, name, salary,
  FIRST_VALUE(name) OVER (
    PARTITION BY dept ORDER BY salary DESC
  ) AS top_earner,
  LAST_VALUE(name) OVER (
    PARTITION BY dept ORDER BY salary DESC
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  ) AS lowest_earner
FROM employees;
Access specific positional values within a window.
NTILE & PERCENT_RANK
SELECT name, salary,
  NTILE(4) OVER (ORDER BY salary DESC) AS quartile,
  PERCENT_RANK() OVER (ORDER BY salary) AS pct_rank,
  CUME_DIST() OVER (ORDER BY salary) AS cume_dist
FROM employees;
Divide into buckets or calculate percentile ranks.

CTEs & Set Operations

Common Table Expression
WITH active_users AS (
  SELECT * FROM users WHERE status = 'active'
),
dept_counts AS (
  SELECT dept, COUNT(*) AS cnt
  FROM active_users GROUP BY dept
)
SELECT * FROM dept_counts WHERE cnt > 10;
Chain multiple named CTEs for readable multi-step logic.
Recursive CTE
-- Org hierarchy: find all reports under a manager
WITH RECURSIVE org_tree AS (
  SELECT id, name, manager_id, 1 AS depth
  FROM employees WHERE id = 1   -- root manager
  UNION ALL
  SELECT e.id, e.name, e.manager_id, t.depth + 1
  FROM employees e
  JOIN org_tree t ON e.manager_id = t.id
)
SELECT * FROM org_tree;
Traverse hierarchical data like org charts or tree structures.
UNION / INTERSECT / EXCEPT
-- UNION ALL: combine, keep duplicates (faster)
SELECT email FROM customers
UNION ALL
SELECT email FROM leads;

-- UNION: combine, remove duplicates
-- INTERSECT: rows in both
-- EXCEPT: rows in first but not second
Combine or compare result sets from multiple queries.

String Functions

Common String Ops
SELECT
  UPPER('hello'),             -- 'HELLO'
  LOWER('HELLO'),             -- 'hello'
  TRIM('  hi  '),             -- 'hi'
  LENGTH('hello'),            -- 5
  SUBSTRING('hello' FROM 2 FOR 3), -- 'ell'
  LEFT('hello', 3),           -- 'hel'
  RIGHT('hello', 2),          -- 'lo'
  REPLACE('hello', 'l', 'r'), -- 'herro'
  CONCAT(first_name, ' ', last_name);
Transform, extract, and manipulate string values.
Pattern Matching
WHERE name LIKE 'J%'            -- Starts with J
WHERE name LIKE '%son'          -- Ends with son
WHERE name LIKE '_a%'           -- 2nd char is a
WHERE email LIKE '%@gmail.com'

-- SIMILAR TO / REGEXP (Postgres)
WHERE phone ~ '^\d{3}-\d{3}-\d{4}$'
LIKE wildcards (% and _) and regex matching.
SPLIT & ARRAY
-- Split string to array (Postgres / Snowflake)
SELECT SPLIT_PART('a,b,c', ',', 2); -- 'b'

-- String aggregation
SELECT dept, STRING_AGG(name, ', ') AS members
FROM employees GROUP BY dept;
Split strings and aggregate strings across rows.

Date & Time Functions

Current Date/Time
SELECT
  CURRENT_DATE,          -- 2024-07-11
  CURRENT_TIMESTAMP,     -- 2024-07-11 10:30:00
  NOW(),                 -- Same as CURRENT_TIMESTAMP
  CURRENT_DATE - INTERVAL '7 days';
Get current date, timestamp, and relative dates.
Date Extraction & Truncation
SELECT
  EXTRACT(YEAR FROM order_date)  AS yr,
  EXTRACT(MONTH FROM order_date) AS mo,
  EXTRACT(DOW FROM order_date)   AS day_of_week,
  DATE_TRUNC('month', order_date) AS month_start,
  DATE_TRUNC('week', order_date)  AS week_start;
Extract parts and truncate to period boundaries.
Date Arithmetic
SELECT
  order_date + INTERVAL '30 days'    AS due_date,
  DATEDIFF('day', start_dt, end_dt)  AS days_between,
  AGE(NOW(), hire_date)              AS tenure,
  DATE_PART('year', AGE(NOW(), dob)) AS age_years;
Add intervals, calculate differences, and compute ages.
Date Formatting
-- Postgres
SELECT TO_CHAR(NOW(), 'YYYY-MM-DD HH24:MI:SS');
SELECT TO_DATE('2024-07-11', 'YYYY-MM-DD');

-- MySQL
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d');
Convert between date types and formatted strings.

NULL Handling

COALESCE & NULLIF
SELECT
  COALESCE(phone, email, 'N/A') AS contact,  -- First non-null
  NULLIF(status, 'unknown')     AS clean_status, -- Returns NULL if equal
  IFNULL(discount, 0)           AS safe_discount; -- MySQL shorthand
Replace NULLs with defaults or convert values to NULL.
NULL-Safe Comparisons
-- NULL = NULL is NOT true!
WHERE col IS NULL          -- Correct NULL check
WHERE col IS NOT NULL      -- Correct not-null check

-- NULL-safe equality (varies by DB)
WHERE col IS NOT DISTINCT FROM other_col  -- Postgres
WHERE col <=> other_col                   -- MySQL
Handle NULL comparison pitfalls correctly.

Transactions & Constraints

Transactions
BEGIN TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- If all good:
COMMIT;
-- If something fails:
ROLLBACK;

SAVEPOINT sp1;     -- Create savepoint
ROLLBACK TO sp1;   -- Partial rollback
ACID transactions with commit, rollback, and savepoints.
Constraints
PRIMARY KEY (id)                -- Unique + NOT NULL
UNIQUE (email)                  -- No duplicates
NOT NULL                        -- Cannot be NULL
CHECK (salary > 0)              -- Value validation
FOREIGN KEY (dept_id) REFERENCES departments(id)
  ON DELETE CASCADE
  ON UPDATE SET NULL;
Enforce data integrity at the database level.

Performance & Execution

EXPLAIN / Query Plan
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE customer_id = 42
ORDER BY order_date DESC;

-- Look for: Seq Scan (bad), Index Scan (good),
-- Hash Join vs Nested Loop, Sort cost
Analyze query execution plans to find bottlenecks.
Index Strategy
-- B-Tree (default): equality, range, ORDER BY
CREATE INDEX idx_date ON orders(order_date);

-- Composite: order matters! Left-to-right
CREATE INDEX idx_comp ON orders(status, order_date);

-- Covering index: all columns in query
CREATE INDEX idx_cover ON orders(customer_id)
  INCLUDE (amount, status);
Choose the right index type and column order.