Quick reference tables
SELECT syntax
| Syntax | What it does |
|---|
SELECT * FROM users | Select all columns |
SELECT name, email FROM users | Select specific columns |
SELECT name AS full_name FROM users | Column alias |
SELECT DISTINCT country FROM users | Unique values only |
SELECT * FROM users LIMIT 10 | Limit rows returned |
SELECT * FROM users LIMIT 10 OFFSET 20 | Pagination (skip 20, take 10) |
SELECT * FROM users ORDER BY name ASC | Sort ascending |
SELECT * FROM users ORDER BY created_at DESC | Sort descending |
SELECT * FROM users ORDER BY country ASC, name DESC | Multi-column sort |
Filtering — WHERE
| Syntax | What it does |
|---|
WHERE status = 'active' | Equality |
WHERE age != 18 | Not equal (<> also works) |
WHERE age > 18 AND age < 65 | Range with AND |
WHERE status = 'active' OR status = 'trial' | OR condition |
WHERE NOT status = 'banned' | NOT |
WHERE name LIKE 'Al%' | Starts with (case-sensitive) |
WHERE name LIKE '%son' | Ends with |
WHERE name LIKE '%lee%' | Contains |
WHERE id IN (1, 2, 3) | Match any value in list |
WHERE id NOT IN (4, 5) | Exclude values |
WHERE age BETWEEN 18 AND 65 | Inclusive range |
WHERE email IS NULL | NULL check |
WHERE email IS NOT NULL | Not null |
WHERE created_at >= '2025-01-01' | Date comparison |
Conditional expressions
| Syntax | What it does |
|---|
CASE WHEN age < 18 THEN 'minor' ELSE 'adult' END | If/else in a column |
COALESCE(nickname, name, 'Anonymous') | First non-NULL value |
NULLIF(value, 0) | Returns NULL if value equals 0 (avoid divide-by-zero) |
IIF(active, 'Yes', 'No') | Shorthand CASE (SQL Server / SQLite) |
Joins
| Join | What it does |
|---|
INNER JOIN orders ON orders.user_id = users.id | Only rows with matches on both sides |
LEFT JOIN orders ON orders.user_id = users.id | All users; NULL for unmatched orders |
RIGHT JOIN users ON orders.user_id = users.id | All orders; NULL for unmatched users |
FULL OUTER JOIN | All rows from both; NULL for non-matches |
CROSS JOIN | Every combination (cartesian product) |
JOIN employees m ON e.manager_id = m.id | Self-join (same table twice) |
Aggregations
| Function | What it does |
|---|
COUNT(*) | Count all rows (including NULLs) |
COUNT(email) | Count non-NULL values |
COUNT(DISTINCT country) | Count unique values |
SUM(amount) | Total |
AVG(score) | Average |
MIN(price) | Minimum value |
MAX(price) | Maximum value |
GROUP BY country | Group rows before aggregating |
HAVING COUNT(*) > 5 | Filter groups (not rows — use after GROUP BY) |
Set operations
| Syntax | What it does |
|---|
SELECT ... UNION SELECT ... | Combine results, remove duplicates |
SELECT ... UNION ALL SELECT ... | Combine results, keep duplicates |
SELECT ... INTERSECT SELECT ... | Rows in both results |
SELECT ... EXCEPT SELECT ... | Rows in first but not second (MINUS in Oracle) |
Data types
| Type | Examples | Notes |
|---|
INT / INTEGER | 42, -7 | Whole numbers |
BIGINT | Large IDs | 64-bit integer |
DECIMAL(10,2) | 99.99 | Fixed precision (use for money) |
FLOAT / DOUBLE | 3.14 | Approximate — avoid for money |
VARCHAR(255) | 'hello' | Variable-length string with limit |
TEXT | 'long text...' | Unlimited string (PostgreSQL/MySQL) |
CHAR(10) | 'ABCDE ' | Fixed-length, padded with spaces |
BOOLEAN | TRUE, FALSE | Some DBs use TINYINT(1) |
DATE | '2025-12-31' | Date only |
TIME | '14:30:00' | Time only |
TIMESTAMP | '2025-12-31 14:30:00' | Date + time |
JSON / JSONB | '{"key": "val"}' | Semi-structured data |
NULL | NULL | Unknown/missing — not equal to anything |
Constraints
| Constraint | What it does |
|---|
PRIMARY KEY | Unique, not null, one per table |
FOREIGN KEY (user_id) REFERENCES users(id) | Referential integrity |
UNIQUE | No duplicate values in the column |
NOT NULL | Column must have a value |
CHECK (age >= 0) | Custom validation rule |
DEFAULT 'active' | Value used when none is provided |
ON DELETE CASCADE | Delete child rows when parent is deleted |
ON DELETE SET NULL | Null out FK when parent is deleted |
DDL — schema changes
| Command | What it does |
|---|
CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100) NOT NULL) | Create table |
DROP TABLE users | Delete table permanently |
DROP TABLE IF EXISTS users | Delete only if it exists |
TRUNCATE TABLE users | Delete all rows fast (no rollback in some DBs) |
ALTER TABLE users ADD COLUMN age INT | Add a column |
ALTER TABLE users DROP COLUMN age | Remove a column |
ALTER TABLE users ALTER COLUMN name TYPE TEXT | Change column type (PostgreSQL) |
ALTER TABLE users RENAME COLUMN name TO full_name | Rename column |
ALTER TABLE users RENAME TO customers | Rename table |
CREATE SCHEMA analytics | Create a namespace/schema |
DML — data changes
| Command | What it does |
|---|
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com') | Insert one row |
INSERT INTO users (name, email) VALUES (...), (...), (...) | Insert multiple rows |
INSERT INTO archive SELECT * FROM users WHERE active = false | Insert from query |
UPDATE users SET name = 'Bob' WHERE id = 1 | Update one row |
UPDATE users SET active = false WHERE last_login < '2024-01-01' | Conditional update |
DELETE FROM users WHERE id = 1 | Delete one row |
DELETE FROM users WHERE active = false | Delete matching rows |
Upsert (insert or update)
| Syntax | Database |
|---|
INSERT ... ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name | PostgreSQL |
INSERT ... ON DUPLICATE KEY UPDATE name = VALUES(name) | MySQL |
INSERT OR REPLACE INTO ... | SQLite |
MERGE INTO target USING source ON (...) WHEN MATCHED THEN UPDATE ... WHEN NOT MATCHED THEN INSERT ... | SQL Server / Oracle |
Indexes
| Command | What it does |
|---|
CREATE INDEX idx_users_email ON users(email) | Basic index |
CREATE UNIQUE INDEX ON users(email) | Unique index |
CREATE INDEX ON orders(user_id, created_at) | Composite index |
CREATE INDEX CONCURRENTLY ON users(email) | Non-blocking (PostgreSQL) |
DROP INDEX idx_users_email | Remove index |
Detailed sections
SELECT — expressions and computed columns
-- Arithmetic in SELECT
SELECT price, quantity, price * quantity AS total FROM order_items;
-- CASE WHEN — if/else per row
SELECT
name,
score,
CASE
WHEN score >= 90 THEN 'A'
WHEN score >= 80 THEN 'B'
WHEN score >= 70 THEN 'C'
ELSE 'F'
END AS grade
FROM students;
-- COALESCE — first non-NULL wins
SELECT name, COALESCE(display_name, username, 'Anonymous') AS label FROM users;
-- NULLIF — avoid divide-by-zero
SELECT total_sales / NULLIF(num_orders, 0) AS avg_order_value FROM sales_summary;
-- String functions (standard SQL)
SELECT UPPER(name), LOWER(email), LENGTH(bio) FROM users;
SELECT TRIM(' hello '); -- 'hello'
SELECT SUBSTRING(name, 1, 3); -- first 3 chars
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users;
-- Date functions
SELECT CURRENT_DATE, CURRENT_TIMESTAMP;
SELECT EXTRACT(YEAR FROM created_at) AS year FROM orders;
JOIN patterns
-- INNER JOIN: only rows with a match on both sides
SELECT users.name, orders.total
FROM users
INNER JOIN orders ON orders.user_id = users.id;
-- LEFT JOIN: all users, even those with no orders
SELECT users.name, COUNT(orders.id) AS order_count
FROM users
LEFT JOIN orders ON orders.user_id = users.id
GROUP BY users.id, users.name;
-- Multi-table join
SELECT u.name, o.id AS order_id, p.name AS product
FROM users u
JOIN orders o ON o.user_id = u.id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id;
-- Self-join: employees and their managers
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
-- FULL OUTER JOIN: show unmatched rows from both sides
SELECT a.id AS a_id, b.id AS b_id
FROM table_a a
FULL OUTER JOIN table_b b ON a.key = b.key
WHERE a.id IS NULL OR b.id IS NULL; -- show only non-matching rows
Subqueries
-- Scalar subquery: returns one value
SELECT name, salary,
(SELECT AVG(salary) FROM employees) AS company_avg
FROM employees;
-- Subquery in WHERE
SELECT name FROM products
WHERE price > (SELECT AVG(price) FROM products);
-- IN subquery
SELECT name FROM users
WHERE id IN (SELECT DISTINCT user_id FROM orders WHERE total > 1000);
-- EXISTS — often faster than IN for large sets
SELECT name FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.total > 1000
);
-- Correlated subquery: references outer query
SELECT name,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_count
FROM users u;
-- Derived table (subquery as a FROM source)
SELECT dept, avg_sal FROM (
SELECT department AS dept, AVG(salary) AS avg_sal
FROM employees
GROUP BY department
) dept_stats
WHERE avg_sal > 60000;
CTEs — Common Table Expressions
-- Basic CTE: readable named subquery
WITH active_users AS (
SELECT id, name FROM users WHERE active = true
),
user_order_counts AS (
SELECT user_id, COUNT(*) AS orders
FROM orders
GROUP BY user_id
)
SELECT u.name, COALESCE(o.orders, 0) AS total_orders
FROM active_users u
LEFT JOIN user_order_counts o ON o.user_id = u.id
ORDER BY total_orders DESC;
-- Recursive CTE: walk a hierarchy (org chart, category tree)
WITH RECURSIVE category_tree AS (
-- Base: top-level categories
SELECT id, name, parent_id, 0 AS depth
FROM categories
WHERE parent_id IS NULL
UNION ALL
-- Recursive: children of each row
SELECT c.id, c.name, c.parent_id, t.depth + 1
FROM categories c
JOIN category_tree t ON c.parent_id = t.id
)
SELECT REPEAT(' ', depth) || name AS indented_name, depth
FROM category_tree
ORDER BY depth, name;
Window functions
Window functions compute values across a set of rows related to the current row — without collapsing them like GROUP BY does.
-- ROW_NUMBER: unique sequential rank per partition
SELECT
name,
department,
salary,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rank_in_dept
FROM employees;
-- RANK vs DENSE_RANK
-- RANK skips numbers after ties (1,2,2,4); DENSE_RANK does not (1,2,2,3)
SELECT name, score,
RANK() OVER (ORDER BY score DESC) AS rank,
DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM students;
-- Running total
SELECT
order_date,
amount,
SUM(amount) OVER (ORDER BY order_date) AS running_total
FROM orders;
-- Moving average (last 7 rows)
SELECT
day,
revenue,
AVG(revenue) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_avg_7
FROM daily_revenue;
-- LAG / LEAD: access previous/next row
SELECT
month,
revenue,
LAG(revenue) OVER (ORDER BY month) AS prev_month,
revenue - LAG(revenue) OVER (ORDER BY month) AS change
FROM monthly_revenue;
-- Get the most recent order per user
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) ranked
WHERE rn = 1;
Transactions
-- Basic transaction
BEGIN;
UPDATE accounts SET balance = balance - 500 WHERE id = 1;
UPDATE accounts SET balance = balance + 500 WHERE id = 2;
COMMIT;
-- Rollback on error
BEGIN;
DELETE FROM orders WHERE user_id = 99;
-- something went wrong...
ROLLBACK;
-- Savepoint: partial rollback
BEGIN;
INSERT INTO log (event) VALUES ('step 1');
SAVEPOINT step1;
INSERT INTO log (event) VALUES ('step 2');
-- oops, undo only step 2
ROLLBACK TO SAVEPOINT step1;
INSERT INTO log (event) VALUES ('step 2 corrected');
COMMIT;
Isolation levels (from lowest to highest isolation):
| Level | Dirty Read | Non-repeatable Read | Phantom Read |
|---|
READ UNCOMMITTED | Yes | Yes | Yes |
READ COMMITTED (default) | No | Yes | Yes |
REPEATABLE READ | No | No | Yes |
SERIALIZABLE | No | No | No |
-- Set isolation level (PostgreSQL / SQL Server)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
-- ...
COMMIT;
Schema design patterns
-- One-to-many: users → orders
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
total DECIMAL(10, 2) NOT NULL,
status VARCHAR(50) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Many-to-many: posts ↔ tags (junction table)
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL
);
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL
);
CREATE TABLE post_tags (
post_id INT REFERENCES posts(id) ON DELETE CASCADE,
tag_id INT REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);
-- Query: all tags for a post
SELECT t.name
FROM tags t
JOIN post_tags pt ON pt.tag_id = t.id
WHERE pt.post_id = 42;
Cross-dialect notes
Key syntax differences between the major SQL databases:
| Feature | PostgreSQL | MySQL | SQLite | SQL Server |
|---|
| Auto-increment PK | SERIAL or GENERATED ALWAYS AS IDENTITY | AUTO_INCREMENT | INTEGER PRIMARY KEY | IDENTITY(1,1) |
| String concat | || or CONCAT() | CONCAT() | || | + or CONCAT() |
| Limit rows | LIMIT n OFFSET m | LIMIT n OFFSET m | LIMIT n OFFSET m | OFFSET m ROWS FETCH NEXT n ROWS ONLY |
| Boolean type | BOOLEAN | TINYINT(1) | INTEGER (0/1) | BIT |
| Current timestamp | NOW() | NOW() | datetime('now') | GETDATE() |
| Upsert | ON CONFLICT DO UPDATE | ON DUPLICATE KEY UPDATE | ON CONFLICT DO UPDATE | MERGE |
| Regex match | ~ (case-sensitive), ~* (i) | REGEXP | REGEXP (requires extension) | LIKE only (no native regex) |
| JSON support | JSON / JSONB with rich operators | JSON type + functions | JSON functions (3.38+) | NVARCHAR + OPENJSON() |
| Case-sensitive LIKE | LIKE (case-sensitive) | LIKE (case-insensitive by default) | LIKE (case-insensitive for ASCII) | LIKE (collation-dependent) |
| Full-text search | tsvector / tsquery | FULLTEXT INDEX | FTS5 extension | CONTAINS / FREETEXT |
Related: PostgreSQL Cheat Sheet | Debug a Slow SQL Query
Next_Recommended_Node
PostgreSQL Cheat Sheet: SQL, Indexes, EXPLAIN & psql
Complete PostgreSQL reference — psql commands, CRUD queries, joins, indexes, EXPLAIN ANALYZE, CTEs, window functions, JSONB operators, and admin queries.