All rows from left + matching from right (NULL if no match)
RIGHT JOIN
All rows from right + matching from left
FULL OUTER JOIN
All rows from both tables (NULL where no match)
CROSS JOIN
Cartesian product (every combination)
SELF JOIN
Table joined with itself
-- INNER JOINSELECT u.name, o.total
FROM users u
INNER JOIN orders o ON u.id = o.user_id;
-- LEFT JOIN (keep all users, even without orders)SELECT u.name, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
-- Multiple JOINsSELECT u.name, o.id, p.name AS product
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN products p ON o.product_id = p.id;
-- Self JOIN (find employees and their managers)SELECT e.name, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
Aggregation
Function
Description
COUNT(*)
Count all rows
COUNT(column)
Count non-NULL values
COUNT(DISTINCT column)
Count unique non-NULL values
SUM(column)
Sum of values
AVG(column)
Average value
MIN(column)
Minimum value
MAX(column)
Maximum value
-- GROUP BY with aggregateSELECT city, COUNT(*) AS user_count
FROM users
GROUP BY city;
-- HAVING (filter groups, not rows)SELECT city, COUNT(*) AS cnt
FROM users
GROUP BY city
HAVINGCOUNT(*) > 10;
-- GROUP BY with multiple aggregatesSELECT department,
COUNT(*) AS employees,
AVG(salary) AS avg_salary,
MAX(salary) AS max_salary
FROM employees
GROUP BY department
ORDER BY avg_salary DESC;
Tip:WHERE filters individual rows before grouping. HAVING filters groups after aggregation. You can't use aggregate functions in WHERE.
Subqueries & CTEs
-- Subquery in WHERESELECT name FROM users
WHERE id IN (
SELECT user_id FROM orders WHERE total > 100
);
-- Subquery in FROM (derived table)SELECT avg_total
FROM (
SELECT user_id, AVG(total) AS avg_total
FROM orders
GROUP BY user_id
) AS user_avgs
WHERE avg_total > 50;
-- EXISTSSELECT name FROM users u
WHERE EXISTS (
SELECT1FROM orders o WHERE o.user_id = u.id
);
-- CTE (Common Table Expression)WITH top_customers AS (
SELECT user_id, SUM(total) AS lifetime
FROM orders
GROUP BY user_id
HAVINGSUM(total) > 1000
)
SELECT u.name, tc.lifetime
FROM users u
JOIN top_customers tc ON u.id = tc.user_id;
-- Window functionsSELECT name, department, salary,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rank,
SUM(salary) OVER (PARTITION BY department) AS dept_total
FROM employees;
INSERT / UPDATE / DELETE
-- INSERT single rowINSERT INTO users (name, email, age)
VALUES ('Alice', 'alice@example.com', 30);
-- INSERT multiple rowsINSERT INTO users (name, email) VALUES
('Bob', 'bob@example.com'),
('Carol', 'carol@example.com');
-- INSERT from SELECTINSERT INTO archive (name, email)
SELECT name, email FROM users WHERE active = false;
-- UPDATEUPDATE users
SET email = 'new@example.com', updated_at = NOW()
WHERE id = 1;
-- UPDATE with JOIN (PostgreSQL)UPDATE orders o
SET status = 'vip'FROM users u
WHERE o.user_id = u.id AND u.tier = 'gold';
-- DELETEDELETE FROM users WHERE id = 1;
-- DELETE all rows (keep table structure)DELETE FROM logs;
TRUNCATE TABLE logs; -- faster, resets auto-increment-- UPSERT (INSERT or UPDATE on conflict)-- PostgreSQL:INSERT INTO users (email, name) VALUES ('a@b.com', 'Alice')
ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name;
-- MySQL:INSERT INTO users (email, name) VALUES ('a@b.com', 'Alice')
ON DUPLICATE KEY UPDATE name = VALUES(name);
Warning: Always use WHERE with UPDATE and DELETE. Without it, you'll modify or delete every row in the table.
Table Management
-- CREATE TABLECREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
age INTEGER,
active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW()
);
-- ALTER TABLEALTER 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 old_name RENAME TO new_name;
-- DROP TABLEDROP TABLE users;
DROP TABLE IF EXISTS users;
Common Data Types
Type
Description
INTEGER / INT
Whole numbers
BIGINT
Large whole numbers
SERIAL
Auto-incrementing integer (PostgreSQL)
DECIMAL(p,s)
Exact decimal (precision, scale)
FLOAT / REAL
Floating point number
VARCHAR(n)
Variable-length string (max n chars)
TEXT
Unlimited-length string
BOOLEAN
True / false
DATE
Date only (YYYY-MM-DD)
TIMESTAMP
Date and time
JSON / JSONB
JSON data (JSONB is binary, faster)
UUID
Universally unique identifier
Constraints & Indexes
Constraint
Description
PRIMARY KEY
Unique identifier for each row
FOREIGN KEY
References a primary key in another table
UNIQUE
All values must be different
NOT NULL
Column cannot contain NULL
DEFAULT value
Default value if none provided
CHECK (condition)
Values must satisfy condition
-- Foreign keyCREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
total DECIMAL(10,2)
);
-- Create indexCREATE INDEX idx_users_email ON users(email);
CREATE UNIQUE INDEX idx_users_email ON users(email);
-- Composite indexCREATE INDEX idx_orders_user_date ON orders(user_id, created_at);
-- Drop indexDROP INDEX idx_users_email;
Common Functions
Function
Description
COALESCE(a, b, c)
First non-NULL value
NULLIF(a, b)
NULL if a = b, otherwise a
CASE WHEN ... THEN ... ELSE ... END
Conditional expression
CAST(x AS type)
Convert data type
UPPER(str) / LOWER(str)
Change case
TRIM(str)
Remove whitespace
LENGTH(str)
String length
SUBSTRING(str, pos, len)
Extract substring
CONCAT(a, b)
Concatenate strings
REPLACE(str, old, new)
Replace in string
NOW()
Current date and time
CURRENT_DATE
Current date
EXTRACT(YEAR FROM date)
Extract part of date
DATE_TRUNC('month', date)
Truncate to month (PostgreSQL)
ABS(x)
Absolute value
ROUND(x, n)
Round to n decimals
CEIL(x) / FLOOR(x)
Round up / down
-- CASE expressionSELECT name,
CASEWHEN age < 18THEN'minor'WHEN age < 65THEN'adult'ELSE'senior'END AS category
FROM users;
Useful Patterns
-- PaginationSELECT * FROM products
ORDER BY id
LIMIT20OFFSET40; -- page 3 (20 per page)-- Find duplicatesSELECT email, COUNT(*) AS cnt
FROM users
GROUP BY email
HAVINGCOUNT(*) > 1;
-- Top N per group (window function)SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (
PARTITION BY department ORDER BY salary DESC
) AS rn
FROM employees
) t WHERE rn <= 3;
-- UNION (combine results, remove duplicates)SELECT name FROM customers
UNIONSELECT name FROM suppliers;
-- UNION ALL (keep duplicates, faster)SELECT name FROM customers
UNION ALLSELECT name FROM suppliers;