SQL Cheat Sheet
General SQL syntax (portable across databases). See postgres.md for
PostgreSQL-specific features.
Basic Queries
Section titled “Basic Queries”SELECT
Section titled “SELECT”-- All columnsSELECT * FROM users;
-- Specific columnsSELECT name, email FROM users;
-- With aliasSELECT name AS username, email AS contact FROM users;
-- Distinct valuesSELECT DISTINCT city FROM users;
-- Calculated columnsSELECT name, price * quantity AS total FROM orders;
-- Limit resultsSELECT * FROM users LIMIT 10;SELECT * FROM users LIMIT 10 OFFSET 20; -- Skip first 20WHERE (Filtering)
Section titled “WHERE (Filtering)”-- Comparison operatorsSELECT * FROM users WHERE age = 25;SELECT * FROM users WHERE age <> 25; -- Not equalSELECT * FROM users WHERE age != 25; -- Not equal (alternate)SELECT * FROM users WHERE age > 18;SELECT * FROM users WHERE age >= 18;SELECT * FROM users WHERE age < 65;SELECT * FROM users WHERE age <= 65;
-- Multiple conditionsSELECT * FROM users WHERE age > 18 AND active = true;SELECT * FROM users WHERE role = 'admin' OR role = 'moderator';SELECT * FROM users WHERE NOT deleted;
-- RangeSELECT * FROM users WHERE age BETWEEN 18 AND 65;
-- List membershipSELECT * FROM users WHERE country IN ('US', 'UK', 'CA');SELECT * FROM users WHERE country NOT IN ('US', 'UK');
-- NULL handlingSELECT * FROM users WHERE phone IS NULL;SELECT * FROM users WHERE phone IS NOT NULL;
-- Pattern matchingSELECT * FROM users WHERE name LIKE 'A%'; -- Starts with ASELECT * FROM users WHERE name LIKE '%son'; -- Ends with sonSELECT * FROM users WHERE name LIKE '%ali%'; -- Contains aliSELECT * FROM users WHERE name LIKE '_ohn'; -- John, Bohn, etc.SELECT * FROM users WHERE email LIKE '%@gmail.com';ORDER BY
Section titled “ORDER BY”SELECT * FROM users ORDER BY name; -- Ascending (default)SELECT * FROM users ORDER BY created_at DESC; -- DescendingSELECT * FROM users ORDER BY country, name; -- Multiple columnsSELECT * FROM users ORDER BY country DESC, name ASC;SELECT * FROM users ORDER BY 2; -- By column positionAggregation
Section titled “Aggregation”Aggregate Functions
Section titled “Aggregate Functions”SELECT COUNT(*) FROM users; -- Count all rowsSELECT COUNT(phone) FROM users; -- Count non-nullSELECT COUNT(DISTINCT country) FROM users; -- Count uniqueSELECT SUM(amount) FROM orders;SELECT AVG(amount) FROM orders;SELECT MIN(price) FROM products;SELECT MAX(price) FROM products;GROUP BY
Section titled “GROUP BY”-- Count users per countrySELECT country, COUNT(*) AS user_countFROM usersGROUP BY country;
-- Multiple aggregationsSELECT category, COUNT(*) AS product_count, AVG(price) AS avg_price, SUM(stock) AS total_stockFROM productsGROUP BY category;
-- Group by multiple columnsSELECT country, city, COUNT(*)FROM usersGROUP BY country, city;HAVING (Filter Groups)
Section titled “HAVING (Filter Groups)”-- Countries with more than 100 usersSELECT country, COUNT(*) AS user_countFROM usersGROUP BY countryHAVING COUNT(*) > 100;
-- Categories with average price over $50SELECT category, AVG(price) AS avg_priceFROM productsGROUP BY categoryHAVING AVG(price) > 50;INNER JOIN
Section titled “INNER JOIN”-- Only matching rows from both tablesSELECT users.name, orders.totalFROM usersINNER JOIN orders ON users.id = orders.user_id;
-- With aliasesSELECT u.name, o.totalFROM users uJOIN orders o ON u.id = o.user_id;LEFT JOIN (LEFT OUTER JOIN)
Section titled “LEFT JOIN (LEFT OUTER JOIN)”-- All users, with orders if they existSELECT u.name, o.totalFROM users uLEFT JOIN orders o ON u.id = o.user_id;
-- Find users without ordersSELECT u.nameFROM users uLEFT JOIN orders o ON u.id = o.user_idWHERE o.id IS NULL;RIGHT JOIN (RIGHT OUTER JOIN)
Section titled “RIGHT JOIN (RIGHT OUTER JOIN)”-- All orders, with user info if existsSELECT u.name, o.totalFROM users uRIGHT JOIN orders o ON u.id = o.user_id;FULL OUTER JOIN
Section titled “FULL OUTER JOIN”-- All rows from both tablesSELECT u.name, o.totalFROM users uFULL OUTER JOIN orders o ON u.id = o.user_id;CROSS JOIN
Section titled “CROSS JOIN”-- Cartesian product (all combinations)SELECT colors.name, sizes.nameFROM colorsCROSS JOIN sizes;Self Join
Section titled “Self Join”-- Employees with their managersSELECT e.name AS employee, m.name AS managerFROM employees eLEFT JOIN employees m ON e.manager_id = m.id;Multiple Joins
Section titled “Multiple Joins”SELECT o.id AS order_id, u.name AS customer, p.name AS productFROM orders oJOIN users u ON o.user_id = u.idJOIN order_items oi ON o.id = oi.order_idJOIN products p ON oi.product_id = p.id;Subqueries
Section titled “Subqueries”In WHERE Clause
Section titled “In WHERE Clause”-- Users who have placed ordersSELECT * FROM usersWHERE id IN (SELECT DISTINCT user_id FROM orders);
-- Products more expensive than averageSELECT * FROM productsWHERE price > (SELECT AVG(price) FROM products);
-- Orders from users in USSELECT * FROM ordersWHERE user_id IN ( SELECT id FROM users WHERE country = 'US');In FROM Clause (Derived Table)
Section titled “In FROM Clause (Derived Table)”SELECT category, avg_priceFROM ( SELECT category, AVG(price) AS avg_price FROM products GROUP BY category) AS category_pricesWHERE avg_price > 50;Correlated Subqueries
Section titled “Correlated Subqueries”-- Users with above-average spending in their countrySELECT * FROM users uWHERE ( SELECT SUM(amount) FROM orders WHERE user_id = u.id) > ( SELECT AVG(total_spent) FROM ( SELECT SUM(amount) AS total_spent FROM orders o JOIN users u2 ON o.user_id = u2.id WHERE u2.country = u.country GROUP BY user_id ) AS country_avg);
-- Simpler: employees earning more than dept averageSELECT * FROM employees eWHERE salary > ( SELECT AVG(salary) FROM employees WHERE department_id = e.department_id);EXISTS
Section titled “EXISTS”-- Users who have at least one orderSELECT * FROM users uWHERE EXISTS ( SELECT 1 FROM orders WHERE user_id = u.id);
-- Users with no ordersSELECT * FROM users uWHERE NOT EXISTS ( SELECT 1 FROM orders WHERE user_id = u.id);Set Operations
Section titled “Set Operations”-- Combine results (remove duplicates)SELECT name FROM customersUNIONSELECT name FROM suppliers;
-- Combine results (keep duplicates)SELECT name FROM customersUNION ALLSELECT name FROM suppliers;
-- Common to bothSELECT name FROM customersINTERSECTSELECT name FROM suppliers;
-- In first but not secondSELECT name FROM customersEXCEPTSELECT name FROM suppliers;CASE Expressions
Section titled “CASE Expressions”-- Simple CASESELECT name, CASE status WHEN 'A' THEN 'Active' WHEN 'I' THEN 'Inactive' WHEN 'P' THEN 'Pending' ELSE 'Unknown' END AS status_labelFROM users;
-- Searched CASESELECT name, price, CASE WHEN price < 10 THEN 'Budget' WHEN price < 50 THEN 'Standard' WHEN price < 100 THEN 'Premium' ELSE 'Luxury' END AS tierFROM products;
-- In aggregationSELECT COUNT(CASE WHEN status = 'active' THEN 1 END) AS active_count, COUNT(CASE WHEN status = 'inactive' THEN 1 END) AS inactive_countFROM users;Common Table Expressions (CTEs)
Section titled “Common Table Expressions (CTEs)”-- Basic CTEWITH active_users AS ( SELECT * FROM users WHERE active = true)SELECT * FROM active_users WHERE created_at > '2024-01-01';
-- Multiple CTEsWITH recent_orders AS ( SELECT * FROM orders WHERE created_at > CURRENT_DATE - INTERVAL '30' DAY ), high_value AS ( SELECT * FROM recent_orders WHERE total > 100 )SELECT user_id, COUNT(*), SUM(total)FROM high_valueGROUP BY user_id;
-- Recursive CTE (hierarchical data)WITH RECURSIVE subordinates AS ( -- Base case SELECT id, name, manager_id, 0 AS level FROM employees WHERE manager_id IS NULL
UNION ALL
-- Recursive case SELECT e.id, e.name, e.manager_id, s.level + 1 FROM employees e JOIN subordinates s ON e.manager_id = s.id)SELECT * FROM subordinates ORDER BY level, name;Window Functions
Section titled “Window Functions”-- Row numberSELECT name, department, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rankFROM employees;
-- Row number within partitionSELECT name, department, ROW_NUMBER() OVER ( PARTITION BY department ORDER BY salary DESC ) AS dept_rankFROM employees;
-- Rank (with gaps for ties)SELECT name, score, RANK() OVER (ORDER BY score DESC) AS rankFROM players;
-- Dense rank (no gaps)SELECT name, score, DENSE_RANK() OVER (ORDER BY score DESC) AS rankFROM players;
-- Running totalSELECT date, amount, SUM(amount) OVER (ORDER BY date) AS running_totalFROM transactions;
-- Running total within partitionSELECT date, category, amount, SUM(amount) OVER ( PARTITION BY category ORDER BY date ) AS category_running_totalFROM transactions;
-- Moving averageSELECT date, amount, AVG(amount) OVER ( ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) AS moving_avg_7dayFROM transactions;
-- Previous/next row valuesSELECT date, amount, LAG(amount, 1) OVER (ORDER BY date) AS prev_amount, LEAD(amount, 1) OVER (ORDER BY date) AS next_amountFROM transactions;
-- First/last in partitionSELECT name, department, salary, FIRST_VALUE(name) OVER ( PARTITION BY department ORDER BY salary DESC ) AS highest_paidFROM employees;
-- PercentileSELECT name, salary, NTILE(4) OVER (ORDER BY salary) AS quartileFROM employees;Data Modification
Section titled “Data Modification”INSERT
Section titled “INSERT”-- Single rowINSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
-- Multiple rowsINSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com'), ('Charlie', 'charlie@example.com');
-- Insert from selectINSERT INTO archive_users (name, email)SELECT name, email FROM users WHERE deleted = true;
-- Insert with default valuesINSERT INTO users (name) VALUES ('Dave'); -- Other columns get defaultsUPDATE
Section titled “UPDATE”-- Update single rowUPDATE users SET name = 'Alice Smith' WHERE id = 1;
-- Update multiple columnsUPDATE usersSET name = 'Alice Smith', email = 'alice.smith@example.com'WHERE id = 1;
-- Update based on calculationUPDATE products SET price = price * 1.1; -- 10% increase
-- Update with subqueryUPDATE ordersSET status = 'cancelled'WHERE user_id IN (SELECT id FROM users WHERE deleted = true);
-- Update with join (syntax varies by database)UPDATE orders oSET o.status = 'vip'FROM users uWHERE o.user_id = u.id AND u.tier = 'premium';DELETE
Section titled “DELETE”-- Delete specific rowsDELETE FROM users WHERE id = 1;
-- Delete with conditionDELETE FROM sessions WHERE expires_at < CURRENT_TIMESTAMP;
-- Delete with subqueryDELETE FROM ordersWHERE user_id IN (SELECT id FROM users WHERE deleted = true);
-- Delete all rows (use TRUNCATE for better performance)DELETE FROM temp_data;TRUNCATE TABLE temp_data; -- Faster, resets auto-incrementTable Operations
Section titled “Table Operations”CREATE TABLE
Section titled “CREATE TABLE”CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, -- MySQL -- id SERIAL PRIMARY KEY, -- PostgreSQL name VARCHAR(100) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, age INT CHECK (age >= 0), country VARCHAR(50) DEFAULT 'US', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
-- With foreign keyCREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, total DECIMAL(10, 2), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE);ALTER TABLE
Section titled “ALTER TABLE”-- Add columnALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- Drop columnALTER TABLE users DROP COLUMN phone;
-- Modify columnALTER TABLE users MODIFY COLUMN name VARCHAR(200); -- MySQLALTER TABLE users ALTER COLUMN name TYPE VARCHAR(200); -- PostgreSQL
-- Rename columnALTER TABLE users RENAME COLUMN name TO full_name;
-- Add constraintALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email);
-- Drop constraintALTER TABLE users DROP CONSTRAINT unique_email;
-- Rename tableALTER TABLE users RENAME TO customers;DROP TABLE
Section titled “DROP TABLE”DROP TABLE users;DROP TABLE IF EXISTS users;DROP TABLE users CASCADE; -- Also drop dependent objectsIndexes
Section titled “Indexes”-- 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;DROP INDEX idx_users_email ON users; -- MySQL syntax-- Create viewCREATE VIEW active_users ASSELECT id, name, emailFROM usersWHERE active = true;
-- Use view like a tableSELECT * FROM active_users WHERE name LIKE 'A%';
-- Replace viewCREATE OR REPLACE VIEW active_users ASSELECT id, name, email, created_atFROM usersWHERE active = true;
-- Drop viewDROP VIEW active_users;DROP VIEW IF EXISTS active_users;Transactions
Section titled “Transactions”-- Basic transactionBEGIN TRANSACTION; -- or just BEGIN, or START TRANSACTION UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2;COMMIT;
-- Rollback on errorBEGIN; DELETE FROM users WHERE active = false; -- Something went wrongROLLBACK;
-- SavepointsBEGIN; INSERT INTO users (name) VALUES ('Alice'); SAVEPOINT sp1; INSERT INTO users (name) VALUES ('Bob'); ROLLBACK TO sp1; -- Only Bob's insert is rolled backCOMMIT;Useful Patterns
Section titled “Useful Patterns”Pagination
Section titled “Pagination”-- Offset-based (simple but slow for large offsets)SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 40;
-- Keyset pagination (faster for large datasets)SELECT * FROM usersWHERE id > 1000ORDER BY idLIMIT 20;Upsert (Insert or Update)
Section titled “Upsert (Insert or Update)”-- MySQLINSERT INTO users (email, name) VALUES ('a@b.com', 'Alice')ON DUPLICATE KEY UPDATE name = VALUES(name);
-- PostgreSQLINSERT INTO users (email, name) VALUES ('a@b.com', 'Alice')ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name;
-- SQLiteINSERT OR REPLACE INTO users (email, name) VALUES ('a@b.com', 'Alice');Find Duplicates
Section titled “Find Duplicates”SELECT email, COUNT(*) AS countFROM usersGROUP BY emailHAVING COUNT(*) > 1;Get Nth Highest Value
Section titled “Get Nth Highest Value”-- 3rd highest salarySELECT DISTINCT salaryFROM employeesORDER BY salary DESCLIMIT 1 OFFSET 2;
-- Using window functionSELECT * FROM ( SELECT *, DENSE_RANK() OVER (ORDER BY salary DESC) AS rank FROM employees) rankedWHERE rank = 3;Running Difference
Section titled “Running Difference”SELECT date, amount, amount - LAG(amount) OVER (ORDER BY date) AS daily_changeFROM sales;Pivot (Cross-Tab)
Section titled “Pivot (Cross-Tab)”-- Manual pivotSELECT product_id, SUM(CASE WHEN month = 'Jan' THEN sales END) AS jan, SUM(CASE WHEN month = 'Feb' THEN sales END) AS feb, SUM(CASE WHEN month = 'Mar' THEN sales END) AS marFROM monthly_salesGROUP BY product_id;See Also
Section titled “See Also”- PostgreSQL — PostgreSQL-specific features (JSONB, arrays, GIN indexes)
- Data Models Lesson Plan — Progressive lessons on data modeling
- Regex — Pattern matching in queries
- CLI Pipelines — Piping query output