SQL Cheat Sheet
On this page 15 sections ▾
This cheat sheet puts the SQL you reach for most on one page: the order a query runs in, the clauses, joins, aggregates, subqueries, CTEs, set operators, window functions, the common string and date functions, and the rules for NULL. It's organized the way you actually reach for it while writing a query — not alphabetically. Most of it is standard SQL that works the same in PostgreSQL, MySQL, SQL Server and SQLite; where a database spells something differently, the table says so. Every example runs as-is against the sample tables in the Playground, which runs SQLite, so paste one in and see the result instead of taking it on faith.
The order SQL actually runs in
This is the single most useful thing to memorize, because it explains half of SQL's "weird" rules — like why most databases won't let you filter on a column alias in WHERE, and none let you use COUNT() there. You write a query top to bottom, but the database executes it in this order:
| Step | Clause | What happens |
|---|---|---|
| 1 | FROM / JOIN | Tables are combined into one working set of rows. |
| 2 | WHERE | Individual rows are filtered. No aggregates or aliases exist yet. |
| 3 | GROUP BY | Remaining rows are collapsed into groups. |
| 4 | HAVING | Groups are filtered — this is where aggregate conditions belong. |
| 5 | SELECT | Output columns and aliases are computed. |
| 6 | ORDER BY | The result is sorted. Aliases from SELECT can be used here. |
| 7 | LIMIT / OFFSET | The final row count is trimmed. |
SQLite, which runs the playground, is lenient here and accepts a SELECT alias in WHERE; PostgreSQL, MySQL and SQL Server reject it, so write the expression out in full if the query has to travel.
SELECT basics
| Syntax | Does |
|---|---|
| SELECT * | Every column. |
| SELECT a, b | Only the named columns, in that order. |
| SELECT a AS x | Renames a column in the output. |
| SELECT DISTINCT a | Removes duplicate rows from the result. |
| ORDER BY a DESC | Sorts by column a, highest first (ASC is the default). |
| LIMIT 10 | Returns at most 10 rows. SQL Server writes SELECT TOP 10; standard SQL writes FETCH FIRST 10 ROWS ONLY. |
| LIMIT 10 OFFSET 20 | Skips 20 rows, then returns 10: page three at ten rows a page. |
SELECT name, unit_price
FROM Products
ORDER BY unit_price DESC
LIMIT 3;
WHERE operators
| Operator | Example | Matches |
|---|---|---|
| = <> > < >= <= | unit_price > 100 | Standard comparisons. |
| AND / OR / NOT | a = 1 AND b = 2 | Combine conditions. Parenthesize when mixing AND/OR. |
| IN (...) | tier IN ('Gold','Platinum') | Any value in the list. Shorthand for chained ORs. |
| BETWEEN a AND b | unit_price BETWEEN 10 AND 50 | Inclusive of both endpoints. |
| LIKE | name LIKE 'A%' | % = any characters, _ = exactly one character. Case-sensitive in PostgreSQL; ignores case in SQLite (letters A–Z) and usually in MySQL and SQL Server. |
| IS NULL / IS NOT NULL | manager_id IS NULL | Missing values. = NULL never matches anything. |
JOINs
| Type | Returns |
|---|---|
| INNER JOIN | Only rows that match in both tables. The default when you write plain JOIN. |
| LEFT JOIN | Every row from the left table, matched data where it exists, NULLs where it doesn't. |
| RIGHT JOIN | The mirror of LEFT JOIN — every row from the right table. |
| FULL OUTER JOIN | Every row from both sides, matched where possible, NULLs where not. MySQL has no FULL OUTER JOIN; combine a LEFT JOIN and a RIGHT JOIN with UNION instead. |
| CROSS JOIN | Every row of one table paired with every row of the other. No ON clause. |
SELECT e.first_name, d.dept_name
FROM Employees e
LEFT JOIN Departments d ON e.dept_id = d.dept_id;
Aggregate functions
| Function | Returns |
|---|---|
| COUNT(*) | Number of rows (COUNT(col) skips NULLs in that column). |
| COUNT(DISTINCT col) | Number of different non-NULL values. |
| SUM(col) | Total of a numeric column. |
| AVG(col) | Average of a numeric column. |
| MIN(col) / MAX(col) | Smallest / largest value. Works on text and dates as well as numbers. |
| ROUND(n, d) | Rounds n to d decimal places. Often wraps AVG() or SUM(). |
Every aggregate collapses a set of rows into one value — used alone, that means the whole table; used with GROUP BY, one value per group. Apart from COUNT(*), which counts rows, they all skip NULLs, and over zero rows only COUNT gives 0: SUM, AVG, MIN and MAX give NULL, so wrap them in COALESCE(..., 0) when a report needs a number.
SELECT dept_id, COUNT(*) AS headcount, ROUND(AVG(salary), 0) AS avg_salary
FROM Employees
GROUP BY dept_id
HAVING COUNT(*) > 2
ORDER BY headcount DESC;
Subqueries
| Form | Use |
|---|---|
| WHERE col = (SELECT ...) | Scalar subquery — returns exactly one value, compared directly. |
| WHERE col IN (SELECT ...) | Returns a column of values to test membership against. |
| WHERE EXISTS (SELECT ...) | True when the subquery finds at least one row. NOT EXISTS is the safe way to ask for "no match". |
| FROM (SELECT ...) AS t | A subquery used as a table — must be aliased. |
SELECT first_name, salary
FROM Employees
WHERE salary > (SELECT AVG(salary) FROM Employees);
CTEs: the WITH clause
| Syntax | Does |
|---|---|
| WITH t AS (SELECT ...) SELECT ... FROM t | Names a subquery so the main query can read it like a table. |
| WITH a AS (...), b AS (...) | Several CTEs, separated by commas. Later ones can read earlier ones. |
| WITH RECURSIVE t AS (...) | A CTE that reads itself, for hierarchies such as who reports to whom. SQL Server writes plain WITH. |
WITH dept_pay AS (
SELECT dept_id, AVG(salary) AS avg_salary
FROM Employees
GROUP BY dept_id
)
SELECT d.dept_name, ROUND(p.avg_salary) AS avg_salary
FROM dept_pay p
JOIN Departments d ON d.dept_id = p.dept_id
ORDER BY avg_salary DESC;
Set operators: UNION, INTERSECT, EXCEPT
| Operator | Returns |
|---|---|
| UNION | Rows from both queries, duplicates removed. |
| UNION ALL | Rows from both queries, duplicates kept. Cheaper, because nothing has to be compared. |
| INTERSECT | Rows that appear in both. MySQL has it from 8.0.31. |
| EXCEPT | Rows in the first query but not the second. Oracle has long called it MINUS; MySQL has it from 8.0.31. |
Both queries must return the same number of columns, in compatible types, and a single ORDER BY goes at the very end.
SELECT city FROM Customers
UNION
SELECT location FROM Departments
ORDER BY city;
Window functions
| Syntax | Gives each row |
|---|---|
| ROW_NUMBER() OVER (ORDER BY x) | 1, 2, 3 … with no ties, even for equal values. |
| RANK() / DENSE_RANK() | Equal values share a rank. RANK then skips numbers (1, 1, 3); DENSE_RANK does not (1, 1, 2). |
| … OVER (PARTITION BY g ORDER BY x) | Starts again for each group g. |
| SUM(x) OVER (ORDER BY d) | A running total in order of d. Rows with the same d share one total. |
| LAG(x) / LEAD(x) | The value of x on the previous / next row. |
Unlike GROUP BY, a window function keeps every row and adds a value to it. MySQL needs version 8.0 or later.
SELECT first_name, dept_id, salary,
RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rank_in_dept
FROM Employees
ORDER BY dept_id, rank_in_dept;
CASE — conditional values
SELECT order_id, order_total,
CASE
WHEN order_total >= 8000 THEN 'Large'
WHEN order_total >= 3000 THEN 'Medium'
ELSE 'Small'
END AS size_band
FROM Orders;
CASE checks its WHEN branches top to bottom and stops at the first match — always include an ELSE, or unmatched rows come back NULL.
String functions
| Task | SQLite (runs here) | Elsewhere |
|---|---|---|
| Change case | UPPER(s), LOWER(s) | The same everywhere. |
| Length | LENGTH(s) | SQL Server: LEN(s). MySQL’s LENGTH counts bytes; CHAR_LENGTH counts characters. |
| Part of a string | SUBSTR(s, 1, 5) | SUBSTRING(s, 1, 5) everywhere. Positions count from 1. |
| Find text | INSTR(s, '@') | MySQL: INSTR. PostgreSQL: POSITION('@' IN s). SQL Server: CHARINDEX('@', s). |
| Replace text | REPLACE(s, 'old', 'new') | The same everywhere. |
| Trim spaces | TRIM(s), LTRIM(s), RTRIM(s) | The same (TRIM in SQL Server from 2017). |
| Join text | a || b, or CONCAT(a, b) | MySQL: CONCAT(a, b). SQL Server: a + b or CONCAT(a, b). |
| Join a group’s values | GROUP_CONCAT(s, ', ') | PostgreSQL and SQL Server: STRING_AGG(s, ', '). MySQL: GROUP_CONCAT(s SEPARATOR ', '). |
Date functions
| Task | SQLite (runs here) | PostgreSQL | MySQL | SQL Server |
|---|---|---|---|---|
| Today | date('now') | CURRENT_DATE | CURDATE() | CAST(GETDATE() AS date) |
| Year of d | strftime('%Y', d) | EXTRACT(YEAR FROM d) | YEAR(d) | YEAR(d) |
| d plus 7 days | date(d, '+7 days') | d + INTERVAL '7 days' | DATE_ADD(d, INTERVAL 7 DAY) | DATEADD(day, 7, d) |
| Days from a to b | julianday(b) - julianday(a) | b - a | DATEDIFF(b, a) | DATEDIFF(day, a, b) |
| First of d’s month | strftime('%Y-%m-01', d) | DATE_TRUNC('month', d) | DATE_FORMAT(d, '%Y-%m-01') | DATEFROMPARTS(YEAR(d), MONTH(d), 1) |
strftime returns text, so the year comes back as '2024'; the playground also adds YEAR(), MONTH() and DAY(), which plain SQLite lacks, so YEAR(d) from the MySQL and SQL Server columns runs there too. The date functions guide runs each of these.
NULL handling
| Syntax | Rule |
|---|---|
| x IS NULL | The only test that finds NULLs; x = NULL is never true. |
| COALESCE(x, y, ...) | The first argument that is not NULL. Portable; IFNULL, ISNULL and NVL are vendor spellings. |
| NULLIF(a, b) | NULL when a equals b, otherwise a. x / NULLIF(y, 0) avoids dividing by zero. |
| NOT IN (SELECT ...) | Returns no rows at all if the subquery produces a NULL. Use NOT EXISTS. |
| a || NULL | NULL in SQLite and PostgreSQL. CONCAT() skips NULLs, except in MySQL. |
| ORDER BY x | Ascending, NULLs come first in SQLite, MySQL and SQL Server, last in PostgreSQL and Oracle. |
SELECT
(SELECT COUNT(*) FROM Employees
WHERE emp_id NOT IN (SELECT manager_id FROM Employees)) AS not_in_count,
(SELECT COUNT(*) FROM Employees e
WHERE NOT EXISTS (SELECT 1 FROM Employees m WHERE m.manager_id = e.emp_id)) AS not_exists_count;
| not_in_count | not_exists_count |
|---|---|
| 0 | 7 |
● 1 row · produced by running this query on the sample database
Both halves ask how many employees manage nobody. NOT EXISTS finds seven; NOT IN finds none, because five employees have a NULL manager_id and NOT IN can never be sure a value is absent from a list that contains a NULL.
Changing data and tables
| Statement | Does |
|---|---|
| INSERT INTO t (a,b) VALUES (1,2) | Adds a new row. |
| UPDATE t SET a=1 WHERE id=5 | Changes existing rows. Omit WHERE and every row changes. |
| DELETE FROM t WHERE id=5 | Removes rows. Omit WHERE and the table empties. |
| CREATE TABLE t (id INTEGER PRIMARY KEY, name VARCHAR(50) NOT NULL) | Creates a table, its columns and their rules. |
| ALTER TABLE t ADD COLUMN c DATE | Adds a column. SQL Server leaves out the word COLUMN. |
| DROP TABLE t | Deletes the table and every row in it. |
| BEGIN; ... COMMIT; | Makes several changes succeed or fail together; ROLLBACK undoes them. SQL Server writes BEGIN TRANSACTION. |
The WHERE clause is doing the same job here as in SELECT — it's just as easy to forget, and far more expensive to forget in an UPDATE or DELETE. It's safe to experiment with all of these in the Playground: the sample database resets to its original state on every page refresh.
Put it into practice
Reading a cheat sheet and writing a query from scratch are different skills. The Practice section has 42 exercises that check your answer by running it, not by matching text.
Start practisingRelated reading
- SQL Commands: DDL, DML, DCL and TCL Explained
- SQL Aggregate Functions: COUNT, SUM, AVG, MIN and MAX
- The SQL SELECT Statement: A Beginner's Guide
- SQL JOINs Explained: INNER, LEFT, RIGHT & FULL
- GROUP BY vs HAVING in SQL: What's the Difference?
- SQL Subqueries Explained: Nested SELECT Statements
- SQL CASE WHEN Explained: Conditional Logic in Queries
- SQL CTEs Explained: How the WITH Clause Works
- UNION vs UNION ALL in SQL: Which One and Why
- SQL Window Functions: ROW_NUMBER, RANK & PARTITION BY
- SQL String Functions: UPPER, SUBSTRING, TRIM & More
- SQL Date Functions: YEAR, MONTH, DATEDIFF & Ranges
- PostgreSQL vs MySQL vs SQL Server vs SQLite: SQL Syntax Differences
About the author
SQL Practice
SQL Practice publishes free SQL tutorials, an in-browser playground and 42 practice exercises, built so you can run what you read. Every runnable example in our tutorials is executed against the site’s sample database before it is published. Read how we keep tutorials accurate, or report a mistake.