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 SELECTSELECT price, quantity, price * quantity AS total FROM order_items;-- CASE WHEN — if/else per rowSELECT name, score, CASE WHEN score >= 90 THEN 'A' WHEN score >= 80 THEN 'B' WHEN score >= 70 THEN 'C' ELSE 'F' END AS gradeFROM students;-- COALESCE — first non-NULL winsSELECT name, COALESCE(display_name, username, 'Anonymous') AS label FROM users;-- NULLIF — avoid divide-by-zeroSELECT 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 charsSELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users;-- Date functionsSELECT 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 sidesSELECT users.name, orders.totalFROM usersINNER JOIN orders ON orders.user_id = users.id;-- LEFT JOIN: all users, even those with no ordersSELECT users.name, COUNT(orders.id) AS order_countFROM usersLEFT JOIN orders ON orders.user_id = users.idGROUP BY users.id, users.name;-- Multi-table joinSELECT u.name, o.id AS order_id, p.name AS productFROM users uJOIN orders o ON o.user_id = u.idJOIN order_items oi ON oi.order_id = o.idJOIN products p ON p.id = oi.product_id;-- Self-join: employees and their managersSELECT e.name AS employee, m.name AS managerFROM employees eLEFT JOIN employees m ON e.manager_id = m.id;-- FULL OUTER JOIN: show unmatched rows from both sidesSELECT a.id AS a_id, b.id AS b_idFROM table_a aFULL OUTER JOIN table_b b ON a.key = b.keyWHERE a.id IS NULL OR b.id IS NULL; -- show only non-matching rows
Subqueries
-- Scalar subquery: returns one valueSELECT name, salary, (SELECT AVG(salary) FROM employees) AS company_avgFROM employees;-- Subquery in WHERESELECT name FROM productsWHERE price > (SELECT AVG(price) FROM products);-- IN subquerySELECT name FROM usersWHERE id IN (SELECT DISTINCT user_id FROM orders WHERE total > 1000);-- EXISTS — often faster than IN for large setsSELECT name FROM users uWHERE EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.total > 1000);-- Correlated subquery: references outer querySELECT name, (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_countFROM 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_statsWHERE avg_sal > 60000;
CTEs — Common Table Expressions
-- Basic CTE: readable named subqueryWITH 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_ordersFROM active_users uLEFT JOIN user_order_counts o ON o.user_id = u.idORDER 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, depthFROM category_treeORDER 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 partitionSELECT name, department, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rank_in_deptFROM 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_rankFROM students;-- Running totalSELECT order_date, amount, SUM(amount) OVER (ORDER BY order_date) AS running_totalFROM 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_7FROM daily_revenue;-- LAG / LEAD: access previous/next rowSELECT month, revenue, LAG(revenue) OVER (ORDER BY month) AS prev_month, revenue - LAG(revenue) OVER (ORDER BY month) AS changeFROM monthly_revenue;-- Get the most recent order per userSELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn FROM orders) rankedWHERE rn = 1;
Transactions
-- Basic transactionBEGIN; UPDATE accounts SET balance = balance - 500 WHERE id = 1; UPDATE accounts SET balance = balance + 500 WHERE id = 2;COMMIT;-- Rollback on errorBEGIN; DELETE FROM orders WHERE user_id = 99; -- something went wrong...ROLLBACK;-- Savepoint: partial rollbackBEGIN; 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):
-- One-to-many: users → ordersCREATE 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 postSELECT t.nameFROM tags tJOIN post_tags pt ON pt.tag_id = t.idWHERE pt.post_id = 42;
Cross-dialect notes
Key syntax differences between the major SQL databases: