SQL Cheat Sheet: Commands, Clause Order and Dialects
TL;DR: This SQL cheat sheet puts the commands you look up most in one table, then shows the order SQL actually runs clauses in, joins, aggregates, window functions and CTEs. Where PostgreSQL, MySQL and SQL Server disagree (row limits, upserts, string matching), the syntax is shown side by side so you can copy the version your database accepts.
A SQL cheat sheet is a one-page reference of SQL statement syntax. The core SQL commands cheat sheet fits in five groups: querying (SELECT, WHERE, GROUP BY, HAVING, ORDER BY), changing data (INSERT, UPDATE, DELETE, MERGE), defining structure (CREATE, ALTER, DROP), controlling access (GRANT, REVOKE) and transactions (BEGIN, COMMIT, ROLLBACK). Most mistakes come from two things this page covers early: the order clauses are evaluated in, and the dialect differences between PostgreSQL, MySQL and SQL Server.
SQL statement cheat sheet: the lookup table
| Task | Syntax | Notes |
|---|---|---|
| Select columns | SELECT a, b FROM t; | SELECT * only for exploring |
| Remove duplicates | SELECT DISTINCT a FROM t; | Applies to the whole row selected |
| Filter rows | WHERE amount > 100 AND status = 'paid' | Runs before grouping |
| Pattern match | WHERE email LIKE '%@acme.com' | % any chars, _ one char |
| List / range | WHERE id IN (1,2,3), BETWEEN 1 AND 10 | BETWEEN is inclusive |
| Null check | WHERE deleted_at IS NULL | = NULL is never true |
| Sort | ORDER BY created_at DESC | Only guaranteed order |
| Aggregate | COUNT(*), SUM(x), AVG(x), MIN, MAX | COUNT(col) skips NULLs |
| Group | GROUP BY country HAVING COUNT(*) > 5 | HAVING filters groups |
| Join | FROM a JOIN b ON b.a_id = a.id | See joins section |
| Combine results | UNION, UNION ALL, INTERSECT, EXCEPT | UNION removes duplicates |
| Insert | INSERT INTO t (a, b) VALUES (1, 'x'); | Multi-row VALUES works everywhere listed |
| Update | UPDATE t SET a = 2 WHERE id = 7; | Forget WHERE and every row changes |
| Delete | DELETE FROM t WHERE id = 7; | TRUNCATE empties the table |
| Create table | CREATE TABLE t (id INT PRIMARY KEY, ...); | Add NOT NULL, UNIQUE, FOREIGN KEY |
| Change table | ALTER TABLE t ADD COLUMN c TEXT; | |
| Index | CREATE INDEX idx_t_a ON t (a); | Speeds WHERE/JOIN on a |
| Transaction | BEGIN; ... COMMIT; or ROLLBACK; | SQL Server: BEGIN TRANSACTION |
What are the basic SQL commands?
The basic SQL commands are SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER and DROP. They split into DQL/DML (reading and changing rows) and DDL (changing structure), with GRANT/REVOKE (DCL) and COMMIT/ROLLBACK (TCL) completing the set. A typical analytical query uses only SELECT with its clauses:
SELECT country, COUNT(*) AS customers, SUM(mrr) AS total_mrr
FROM customers
WHERE status = 'active'
GROUP BY country
HAVING SUM(mrr) > 1000
ORDER BY total_mrr DESC
LIMIT 10;What is the order of SQL clauses?
SQL is written in one order and evaluated in another. You write SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT, but the database logically processes them like this:
| Step | Clause | What it does |
|---|---|---|
| 1 | FROM / JOIN | Builds the row set |
| 2 | WHERE | Drops rows |
| 3 | GROUP BY | Collapses rows into groups |
| 4 | HAVING | Drops groups |
| 5 | SELECT | Computes output columns and aliases; window functions run here |
| 6 | DISTINCT | Removes duplicate output rows |
| 7 | ORDER BY | Sorts |
| 8 | LIMIT / OFFSET / TOP | Cuts the result |

This explains the most common error: you cannot use a SELECT alias in WHERE, because WHERE runs first. ORDER BY can use the alias because it runs after. It also explains why you filter aggregates with HAVING, not WHERE. For a deeper look at filtering, see our guide to CASE WHEN in PostgreSQL.
Joins cheat sheet
| Join | Returns |
|---|---|
INNER JOIN | Rows with a match in both tables |
LEFT JOIN | All left rows, NULLs where the right has no match |
RIGHT JOIN | All right rows (rewrite as LEFT for readability) |
FULL OUTER JOIN | All rows from both sides; not in MySQL |
CROSS JOIN | Every combination of rows |
| Self join | A table joined to itself with two aliases |
-- customers with no orders
SELECT c.id, c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;
MySQL has no FULL OUTER JOIN; emulate it with a LEFT JOIN UNION a RIGHT JOIN. Joining when you only need columns from several tables without a key is covered in selecting from multiple tables without JOIN.
What differs between MySQL and PostgreSQL syntax?
The core of SQL is portable; row limits, upserts, string handling and identifiers are not. These are the differences you hit most:
| Task | PostgreSQL | MySQL | SQL Server | ||
|---|---|---|---|---|---|
| First 10 rows | LIMIT 10 | LIMIT 10 | SELECT TOP (10) or OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY | ||
| Skip 20, take 10 | LIMIT 10 OFFSET 20 | LIMIT 20, 10 or LIMIT 10 OFFSET 20 | OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY | ||
| Upsert | INSERT ... ON CONFLICT (id) DO UPDATE | INSERT ... ON DUPLICATE KEY UPDATE | MERGE | ||
| Case-insensitive match | ILIKE | LIKE (default collations are case-insensitive) | LIKE (depends on collation) | ||
| Concatenate | a \ | \ | b or CONCAT(a, b) | CONCAT(a, b) | a + b or CONCAT(a, b) |
| Quote identifiers | "column" | ` column | [column]` | ||
| Return changed rows | RETURNING * | not supported | OUTPUT inserted.* | ||
| Current time | now() | NOW() | GETDATE() / SYSDATETIME() | ||
| Auto-increment | GENERATED ALWAYS AS IDENTITY | AUTO_INCREMENT | IDENTITY(1,1) |
-- PostgreSQL upsert
INSERT INTO plans (id, price) VALUES (1, 30)
ON CONFLICT (id) DO UPDATE SET price = EXCLUDED.price
RETURNING *;
-- MySQL upsert
INSERT INTO plans (id, price) VALUES (1, 30)
ON DUPLICATE KEY UPDATE price = 30;Version notes worth knowing: ILIKE is a PostgreSQL extension, not standard SQL (PostgreSQL docs). PostgreSQL got MERGE in version 15 (release notes). In SQL Server, TOP ... WITH TIES requires ORDER BY and can return more rows than asked for when the last value ties (Microsoft Learn). In every dialect, a row limit without ORDER BY returns an unpredictable subset, and rows skipped by a large OFFSET are still computed by the server (PostgreSQL docs). Date functions differ most of all; see GETDATE in PostgreSQL and DATEDIFF in PostgreSQL.
How do window functions work?
A window function computes a value for each row from a set of related rows (the window) without collapsing them the way GROUP BY does. You define the window with OVER (PARTITION BY ... ORDER BY ...). They are supported in PostgreSQL, MySQL 8.0 and later, SQL Server and SQLite 3.25.0 and later (SQLite docs).
| Function | Returns |
|---|---|
ROW_NUMBER() | 1, 2, 3 per partition, no ties |
RANK() / DENSE_RANK() | Rank with gaps / without gaps |
LAG(x) / LEAD(x) | Previous / next row's value |
SUM(x) OVER (ORDER BY d) | Running total |
NTILE(4) | Quartile bucket |
-- latest order per customer
SELECT *
FROM (
SELECT o.*, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY created_at DESC) AS rn
FROM orders o
) x
WHERE rn = 1;Window functions run at the SELECT step, so you cannot filter on them in WHERE; wrap the query as above. More patterns in our PARTITION BY guide.
CTEs (WITH queries)
A common table expression is a named subquery defined with WITH that exists for one statement. It makes multi-step logic readable, and WITH RECURSIVE walks hierarchies such as org charts or category trees.
WITH monthly AS (
SELECT date_trunc('month', created_at) AS month, SUM(amount) AS revenue
FROM orders
GROUP BY 1
)
SELECT month, revenue,
revenue - LAG(revenue) OVER (ORDER BY month) AS change
FROM monthly
ORDER BY month;In PostgreSQL a non-recursive CTE referenced once is inlined into the main query; add MATERIALIZED or NOT MATERIALIZED after AS to override that (PostgreSQL docs). MySQL supports CTEs from 8.0. date_trunc is PostgreSQL; in MySQL use DATE_FORMAT(created_at, '%Y-%m-01').
Common mistakes the cheat sheet saves you from
WHERE col = NULLreturns nothing; useIS NULL.COUNT(col)andCOUNT(*)differ whencolhas NULLs.NOT IN (subquery)returns no rows if the subquery contains a NULL; useNOT EXISTS.- A
LEFT JOINturns into an inner join when you filter the right table inWHERE; move that condition intoON. - Joining two one-to-many tables multiplies rows and inflates
SUM; aggregate each side in a CTE first. Duplicate rows are easy to find with this duplicates query.
When you'd rather not write the query
A cheat sheet helps when you know SQL and forget syntax. When the person with the question is in ops, sales or finance, the bottleneck is the query itself. AI for Database connects to PostgreSQL, MySQL, SQL Server and other sources, answers plain-English questions with the SQL it ran shown next to the result, and can pin the answer to a self-refreshing dashboard or an alert. Try AI for Database free at aifordatabase.com, sample data included.
Frequently Asked Questions
What are the basic SQL commands?
SELECT reads rows, INSERT adds them, UPDATE changes them, DELETE removes them, and CREATE, ALTER and DROP manage tables and indexes. GRANT/REVOKE control access and COMMIT/ROLLBACK end transactions.
What is the order of SQL clauses?
Written order is SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT. Logical order is FROM/JOIN, WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY, LIMIT, which is why WHERE cannot see SELECT aliases.
How do window functions work?
They compute a value per row across a window defined by OVER (PARTITION BY ... ORDER BY ...) while keeping every row, unlike GROUP BY. Use them for rankings, running totals and previous-row comparisons.
What differs between MySQL and PostgreSQL syntax?
Mainly upserts (ON DUPLICATE KEY UPDATE vs ON CONFLICT), case-insensitive matching (LIKE collation vs ILIKE), identifier quoting (backticks vs double quotes), RETURNING (PostgreSQL only), FULL OUTER JOIN (PostgreSQL only) and date functions.
What is the difference between WHERE and HAVING?
WHERE filters individual rows before grouping; HAVING filters groups after aggregation. HAVING SUM(amount) > 100 works, WHERE SUM(amount) > 100 is an error.
Is SQL the same in every database?
The ANSI core (SELECT, joins, GROUP BY, CTEs, window functions) is shared. Row limits, upserts, string and date functions, and procedural code differ by vendor, so check the dialect table above before copying a query between databases.