SQL Cheat Sheet
Every clause, join, aggregate, window function, and CTE. Searchable, filterable by dialect, copy-ready.
Every clause, join, aggregate, window function, and CTE. Searchable, filterable by dialect, copy-ready.
Most developers don't memorize every SQL command. They keep a reliable SQL cheat sheet close by instead.
And honestly? That's the smarter move.
Whether you're writing a SELECT statement for the first time or trying to remember the exact syntax for a window function at 11pm before a deadline, having a solid reference saves time. SQL covers a lot of ground, from basic data retrieval and JOIN operations to aggregate functions, subqueries, and CTEs.
This guide covers the most-used SQL commands across MySQL, PostgreSQL, and SQL Server, with clean syntax examples you can actually copy and use.
No filler. Just the queries you need.
SQL (Structured Query Language) is a programming language for managing and querying data in relational databases.
It lets you retrieve, insert, update, and delete records, create and modify table structures, and control access permissions, all within a single standardized language.
Every major back-end development stack touches SQL in some form. PostgreSQL, MySQL, Microsoft SQL Server, Oracle, SQLite - they all use it.
SQL runs on a simple idea: you describe what data you want, not how to get it. The database engine handles the rest.
SQL commands split into four groups. Each group has a distinct job.
DDL defines and modifies database structure. Think schemas, tables, indexes.
CREATE - makes a new table, database, index, or view
ALTER - changes an existing structure (add/remove columns, rename)
DROP - deletes a table or database permanently
TRUNCATE - removes all rows from a table without logging individual deletes
DDL changes are usually auto-committed. No ROLLBACK after a DROP.
DML handles the actual data inside tables. This is what you'll use most.
SELECT - reads data
INSERT - adds new rows
UPDATE - modifies existing rows
DELETE - removes rows
SELECT is technically read-only, but it's grouped here since it operates on table data.
DCL manages permissions and access. Two commands cover everything.
GRANT - gives a user or role access to specific actions
REVOKE - removes that access
Relevant in any multi-user database setup. Critical in production environments where software security is a concern.
TCL groups multiple DML operations into a single unit of work.
BEGIN / START TRANSACTION - opens a transaction
COMMIT - saves all changes made during the transaction
ROLLBACK - undoes all changes if something fails
SAVEPOINT - marks a point you can roll back to without canceling the whole transaction
This is where ACID properties (Atomicity, Consistency, Isolation, Durability) actually kick in.
Data types define what kind of value a column can store. Getting this right affects storage, performance, and query behavior.
|
Type |
Use |
|---|---|
|
|
Whole numbers |
|
|
Large whole numbers |
|
|
Fixed-point numbers, exact precision |
|
|
Approximate decimal values |
|
|
Like DECIMAL, preferred for financial data |
FLOAT is tricky for currency. Use DECIMAL or NUMERIC when precision matters.
|
Type |
Use |
|---|---|
|
|
Fixed-length string, always pads to |
|
|
Variable-length string, up to |
|
|
Unlimited-length string (PostgreSQL/MySQL) |
|
|
Unicode variable-length (SQL Server) |
CHAR is faster for fixed-width data like country codes. VARCHAR is the default for most use cases.
|
Type |
Use |
|---|---|
|
|
Year, month, day only |
|
|
Time of day only |
|
|
Date + time (MySQL, SQL Server) |
|
|
Date + time, often auto-updated |
|
|
Duration between two points (PostgreSQL) |
Dialect differences matter here. MySQL uses DATETIME; PostgreSQL prefers TIMESTAMP WITH TIME ZONE.
BOOLEAN stores TRUE or FALSE. MySQL uses TINYINT(1) as a stand-in. SQL Server uses BIT.
NULL is not zero. Not an empty string. NULL means "unknown" or "no value." Comparisons with NULL always return NULL, never TRUE or FALSE, which catches a lot of developers off guard.
Use IS NULL and IS NOT NULL, not = NULL.
A few rules that apply across all SQL dialects.
Case sensitivity - SQL keywords are case-insensitive. SELECT and select work the same. Column and table names may or may not be case-sensitive depending on the database and OS.
Statements end with a semicolon (;). Required in multi-statement scripts; optional for single queries in many tools.
Whitespace doesn't matter. You can break a query across multiple lines freely.
Two comment styles:
-- Single-line comment
/* Multi-line
comment */
String values use single quotes: 'value'. Double quotes are for identifiers (table/column names) in most databases. SQL Server uses square brackets: [column name].
Reserved words used as column names need quoting. Best practice: don't name columns order, select, table, or other SQL keywords.
SELECT is the most-used command in SQL. It retrieves rows from one or more tables.
Basic syntax:
SELECT column1, column2
FROM table_name;
Use * to select all columns - fine for exploring, bad for production queries. Always specify columns explicitly in real code.
WHERE filters which rows come back.
SELECT first_name, last_name
FROM employees
WHERE department = 'Engineering';
The WHERE clause runs before SELECT, so you can filter on columns you're not selecting.
ORDER BY sorts the result set. Default is ascending (ASC); add DESC to flip it.
SELECT product_name, price
FROM products
ORDER BY price DESC;
You can sort by multiple columns: ORDER BY category ASC, price DESC.
LIMIT caps how many rows are returned. OFFSET skips rows before starting.
SELECT *
FROM orders
ORDER BY created_at DESC
LIMIT 10 OFFSET 20;
SQL Server uses TOP instead of LIMIT. Oracle uses ROWNUM or FETCH FIRST n ROWS ONLY.
LIMIT without ORDER BY returns rows in unpredictable order. Always pair them.
DISTINCT removes duplicate rows from the result.
SELECT DISTINCT country
FROM customers;
DISTINCT applies to the entire row, not just one column. SELECT DISTINCT first_name, last_name returns unique combinations, not unique first names alone.
Heavy use of DISTINCT often signals a join problem worth fixing.
Filtering is how you narrow down query results. SQL gives you a full set of operators for this.
|
Operator |
Meaning |
|---|---|
|
|
Equal to |
|
|
Not equal to |
|
|
Greater / less than |
|
|
Greater or equal / less or equal |
Standard across all dialects. Use <> for maximum compatibility; != works in most databases but isn't ANSI SQL.
BETWEEN filters within a range, inclusive on both ends:
WHERE salary BETWEEN 50000 AND 80000
IN matches against a list of values:
WHERE country IN ('US', 'CA', 'UK')
LIKE matches string patterns. Two wildcards: % (any number of characters), _ (exactly one character):
WHERE email LIKE '%@gmail.com'
IS NULL checks for missing values. Never use = NULL - it always returns false.
Combine multiple conditions with AND, OR, NOT. Use parentheses to control evaluation order.
WHERE (status = 'active' AND country = 'US')
OR role = 'admin'
Without parentheses, AND evaluates before OR. Explicit grouping prevents logic bugs.
Aggregate functions collapse multiple rows into a single value.
|
Function |
Returns |
|---|---|
|
|
Total number of rows |
|
|
Rows where column is not NULL |
|
|
Total of all values |
|
|
Mean value |
|
|
Lowest value |
|
|
Highest value |
All aggregate functions ignore NULL values except COUNT(*).
GROUP BY segments rows into groups so aggregate functions run per group, not across the whole table.
SELECT department, AVG(salary)
FROM employees
GROUP BY department;
Every column in SELECT that isn't an aggregate must appear in GROUP BY. Skipping this causes errors in strict SQL modes.
WHERE filters rows before grouping. HAVING filters groups after aggregation.
SELECT department, COUNT(*) AS headcount
FROM employees
GROUP BY department
HAVING COUNT(*) > 10;
Can't use aggregate functions in a WHERE clause. That's what HAVING is for.
JOINs combine rows from two or more tables based on a related column.
Returns only rows with matching values in both tables. Non-matching rows from either side are dropped.
SELECT o.order_id, c.name
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id;
Most common join type. If a row has no match, it disappears from results.
Returns all rows from the left table, with matched rows from the right. Unmatched right-side columns return NULL.
SELECT c.name, o.order_id
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id;
Good for finding customers who have never placed an order: WHERE o.order_id IS NULL.
Mirror of LEFT JOIN - keeps all rows from the right table. Rarely used; most developers rewrite as a LEFT JOIN with tables swapped for readability.
Returns all rows from both tables. NULLs fill in where there's no match on either side.
Not supported in MySQL natively. Workaround: LEFT JOIN + UNION + RIGHT JOIN.
Returns every combination of rows from both tables. No ON condition.
SELECT * FROM colors CROSS JOIN sizes;
Produces a Cartesian product. Useful for generating test data or combination matrices. Dangerous on large tables.
A table joined to itself. Useful for hierarchical data like org charts or parent-child relationships.
SELECT e.name, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
Always use aliases when self-joining, or the query won't parse.
A subquery is a SELECT statement nested inside another query.
Filters based on results from another query.
SELECT name FROM employees
WHERE department_id IN (
SELECT id FROM departments WHERE location = 'NYC'
);
Can often be rewritten as a JOIN. JOINs are usually faster on large datasets.
Treats the subquery result as a temporary table. Also called a derived table.
SELECT dept, avg_salary
FROM (
SELECT department AS dept, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
) AS dept_averages
WHERE avg_salary > 60000;
Must be aliased in most databases.
Runs once per row of the outer query, referencing the outer query's columns.
SELECT name, salary
FROM employees e
WHERE salary > (
SELECT AVG(salary) FROM employees
WHERE department = e.department
);
Powerful but slow at scale. If the outer table has 100k rows, the subquery runs 100k times.
A CTE uses the WITH clause to define a named, temporary result set that you can reference in the main query.
WITH high_earners AS (
SELECT name, salary FROM employees WHERE salary > 80000
)
SELECT * FROM high_earners ORDER BY salary DESC;
CTEs are cleaner than nested subqueries and easier to debug. They don't improve performance on their own - they're a readability tool.
Recursive CTEs handle hierarchical data like category trees or org charts:
WITH RECURSIVE org AS (
SELECT id, name, manager_id FROM employees WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, e.manager_id
FROM employees e
JOIN org ON e.manager_id = org.id
)
SELECT * FROM org;
PostgreSQL, SQL Server, and MySQL 8+ support recursive CTEs. MySQL 5.x does not.
Window functions run calculations across a set of rows related to the current row, without collapsing them into a single result like aggregates do.
Basic syntax:
function_name() OVER (
PARTITION BY column
ORDER BY column
)
PARTITION BY divides rows into groups. ORDER BY sets the order within each partition. Both are optional.
All three assign a number to each row within a partition.
|
Function |
Ties |
|---|---|
|
|
No ties, always unique |
|
|
Ties get the same rank; next rank skips |
|
|
Ties get the same rank; next rank does not skip |
Use ROW_NUMBER() for pagination. Use DENSE_RANK() when gaps in ranking are confusing to end users.
LAG accesses the value from a previous row; LEAD accesses the next row - without a self-join.
SELECT order_date,
revenue,
LAG(revenue) OVER (ORDER BY order_date) AS prev_revenue
FROM monthly_sales;
Offset defaults to 1 row. You can pass a second argument to go back/forward multiple rows.
Standard aggregate functions work as window functions when paired with OVER().
SELECT name, salary,
SUM(salary) OVER (PARTITION BY department) AS dept_total
FROM employees;
Returns the department total on every row, not a single summary row. Useful for calculating percentages: salary / SUM(salary) OVER (PARTITION BY department).
Core string functions across MySQL, PostgreSQL, and SQL Server.
|
Function |
What it does |
|---|---|
|
|
Joins strings together |
|
|
Returns character count ( |
|
|
Changes case |
|
|
Removes leading and trailing spaces |
|
|
Extracts part of a string |
|
|
Swaps one substring for another |
|
|
Finds position of a substring |
PostgreSQL uses || for concatenation instead of CONCAT(), though CONCAT() also works.
TRIM only removes spaces by default. To remove specific characters, use LTRIM/RTRIM with a character argument in SQL Server, or TRIM('x' FROM str) in PostgreSQL.
Date handling varies more across dialects than almost anything else in SQL.
|
Operation |
MySQL |
PostgreSQL |
SQL Server |
|---|---|---|---|
|
Current date+time |
|
|
|
|
Current date |
|
|
|
|
Add to date |
|
|
|
|
Difference |
|
|
|
|
Format date |
|
|
|
When storing timestamps for app deployment logs or audit trails, always use UTC. Convert to local time in the application layer, not in SQL.
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL UNIQUE,
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Constraints available inline: NOT NULL, UNIQUE, DEFAULT, CHECK. Define PRIMARY KEY and FOREIGN KEY inline for single columns, or as table-level constraints for composite keys.
Primary key uniquely identifies each row; one per table; cannot be NULL.
A foreign key links a column to the primary key of another table, enforcing referential integrity:
ALTER TABLE orders
ADD CONSTRAINT fk_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE CASCADE;
ON DELETE CASCADE deletes child rows when the parent is deleted. ON DELETE RESTRICT blocks the delete if children exist.
-- Add a column
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- Drop a column
ALTER TABLE users DROP COLUMN phone;
-- Change a column type
ALTER TABLE users MODIFY COLUMN status CHAR(10); -- MySQL
ALTER TABLE users ALTER COLUMN status TYPE CHAR(10); -- PostgreSQL
-- Rename a column
ALTER TABLE users RENAME COLUMN status TO account_status; -- PostgreSQL/MySQL 8+
|
Command |
Removes |
Rollback |
Speed |
|---|---|---|---|
|
|
Selected rows |
Yes |
Slow (logs each row) |
|
|
All rows |
No (usually) |
Fast |
|
|
Entire table + structure |
No |
Instant |
TRUNCATE resets auto-increment counters. DELETE does not.
Insert one row:
INSERT INTO users (name, email) VALUES ('Sara', 'sara@example.com');
Insert multiple rows in one statement:
INSERT INTO users (name, email) VALUES
('Tom', 'tom@example.com'),
('Ana', 'ana@example.com');
Copy rows from another table: INSERT INTO archive SELECT * FROM orders WHERE status = 'closed'.
UPDATE employees
SET salary = salary * 1.10
WHERE department = 'Sales';
Always include a WHERE clause. An UPDATE without WHERE modifies every row in the table. Test your WHERE condition with a SELECT first.
DELETE FROM sessions
WHERE last_active < NOW() - INTERVAL 30 DAY;
Same rule as UPDATE: missing WHERE deletes all rows. Use LIMIT in MySQL to cap how many rows get deleted in one run.
An index speeds up reads by creating a separate data structure the query optimizer can scan instead of the full table.
-- Create an index
CREATE INDEX idx_email ON users (email);
-- Create a unique index
CREATE UNIQUE INDEX idx_email ON users (email);
-- Drop an index
DROP INDEX idx_email ON users; -- MySQL
DROP INDEX idx_email; -- PostgreSQL
Index every column used in WHERE, JOIN ON, and ORDER BY clauses on large tables.
Downsides: indexes slow down INSERT, UPDATE, and DELETE because the index must be updated too. Don't index every column reflexively.
Composite indexes work left-to-right. An index on (last_name, first_name) helps queries filtering by last_name alone, but not first_name alone.
A view is a saved SELECT query treated as a virtual table.
CREATE VIEW active_users AS
SELECT id, name, email
FROM users
WHERE status = 'active';
-- Query it like a table
SELECT * FROM active_users;
Views don't store data. They re-run the underlying query every time they're accessed.
Simple views are often updatable (you can INSERT/UPDATE through them). Views with JOIN, GROUP BY, DISTINCT, or aggregate functions are read-only.
Drop a view: DROP VIEW active_users;
A transaction groups multiple SQL statements so they either all succeed or all fail.
BEGIN;
UPDATE accounts SET balance = balance - 500 WHERE id = 1;
UPDATE accounts SET balance = balance + 500 WHERE id = 2;
COMMIT;
If the second UPDATE fails, ROLLBACK undoes the first one too. Nothing partial makes it to the database.
SAVEPOINT lets you roll back to a specific point without canceling the entire transaction:
SAVEPOINT before_update;
-- do some work
ROLLBACK TO SAVEPOINT before_update;
This maps directly to ACID properties: Atomicity (all or nothing), Consistency (valid state after), Isolation (concurrent transactions don't interfere), Durability (committed data persists).
Slow queries are usually fixable. Most performance problems fall into a small set of patterns.
EXPLAIN (or EXPLAIN ANALYZE in PostgreSQL) shows how the database executes a query without running it.
EXPLAIN SELECT * FROM orders WHERE customer_id = 42;
Look for Seq Scan on large tables - that means no index is being used. Index Scan or Index Seek is what you want.
Index columns used in WHERE, JOIN, and ORDER BY
Avoid functions on indexed columns in WHERE: WHERE YEAR(created_at) = 2024 breaks index usage; WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31' doesn't
Cover queries with composite indexes when multiple columns are filtered together
Remove unused indexes - they consume space and slow down writes
SELECT * in production code pulls unnecessary columns and breaks when schemas change
LIKE '%term%' with a leading wildcard can't use an index; full-text search is better for this
N+1 queries: running a query inside a loop instead of a single query with a JOIN
Missing indexes on foreign key columns - every JOIN becomes a full table scan
The code review process should catch unindexed queries before they reach production.
Standard SQL exists, but every database extends it differently. What works in one might fail in another.
|
Feature |
MySQL |
PostgreSQL |
|---|---|---|
|
String concat |
|
|
|
Auto-increment |
|
|
|
Limit rows |
|
|
|
Boolean type |
|
Native |
|
Full-text search |
|
|
|
JSON support |
|
|
PostgreSQL is stricter about SQL standards. MySQL is more forgiving with implicit conversions, which can hide bugs.
SQL Server uses T-SQL, Microsoft's SQL extension.
Key differences:
TOP n instead of LIMIT n: SELECT TOP 10 * FROM orders
GETDATE() instead of NOW()
ISNULL(value, fallback) instead of COALESCE (both work, but ISNULL is T-SQL specific)
Square brackets for identifiers: [order], [user]
PRINT for debug output instead of SELECT in scripts
The web development IDE you use often determines which SQL dialect you interact with most.
SQLite is file-based, serverless, and intentionally minimal. Common in mobile application development and database for mobile apps scenarios.
What SQLite doesn't support:
RIGHT JOIN or FULL OUTER JOIN (use LEFT JOIN + UNION)
Stored procedures
Window functions (added in version 3.25.0 - check your version)
ALTER TABLE is very limited; can't drop columns in older versions
User management and permissions (single-file, no multi-user access control)
Fine for development and embedded use. Not for high-concurrency production workloads.
Start with SELECT, WHERE, INSERT, UPDATE, and DELETE. These five DML commands cover most real-world tasks. Add JOIN and GROUP BY once the basics feel solid. Everything else builds on this foundation.
WHERE filters rows before grouping. HAVING filters after aggregation. You can't use aggregate functions like COUNT() or SUM() in a WHERE clause - that's what HAVING exists for.
JOINs are generally faster and easier to read. Use subqueries when the logic genuinely needs a nested result set, like correlated subqueries. For simple lookups across tables, an INNER JOIN or LEFT JOIN is almost always the better call.
DELETE removes selected rows and supports ROLLBACK. TRUNCATE wipes all rows fast, no logging. DROP deletes the entire table structure. Use each at the right level - row, data, or schema.
Aggregate functions collapse rows into one result. Window functions keep every row intact and calculate across a related set using OVER(). Use them for running totals, rankings, or comparing each row to a group average.
Use DECIMAL or NUMERIC with explicit precision and scale, like DECIMAL(10, 2). Never use FLOAT for money - floating-point arithmetic introduces rounding errors that compound over time in financial calculations.
A Common Table Expression uses the WITH clause to name a temporary result set. Use it to break complex queries into readable steps. It doesn't improve performance on its own, but it makes nested subquery logic far easier to debug.
It pulls every column, including ones you don't need, which wastes I/O and memory. It also breaks silently when table schemas change. Always list specific columns in production SQL queries.
Missing indexes on WHERE and JOIN columns are the most common cause. Other culprits: SELECT *, leading wildcards in LIKE, functions applied to indexed columns, and correlated subqueries running once per row on large tables.
Yes. Key differences include LIMIT vs TOP, AUTO_INCREMENT vs SERIAL, date functions like NOW() vs GETDATE(), and string concatenation (CONCAT vs ||). Core DML commands are consistent; functions and extensions vary significantly by dialect.