This cheat sheet covers standard SQL, which works in SQLite, PostgreSQL, MySQL and SQL Server unless a note says otherwise. The examples use the small tables from our free lessons (family_members, character and character_actor), so you can paste them into SQL Easy and run them straight away.
The order of a SQL query
You write clauses in this order, and every clause except SELECT and FROM is optional:
SELECT columns or expressions
FROM table
JOIN other_table ON condition
WHERE row filter
GROUP BY grouping columns
HAVING group filter
ORDER BY sort columns
LIMIT row count;
The database runs them in a different order: FROM and JOIN, then WHERE, GROUP BY, HAVING, SELECT, ORDER BY and finally LIMIT. That is why you can't use a SELECT alias inside WHERE, and why WHERE can't filter on a total (that is HAVING's job).
SELECT basics
| Goal | Syntax |
|---|---|
| All columns | SELECT * FROM family_members; |
| Specific columns | SELECT name, species FROM family_members; |
| Unique values | SELECT DISTINCT species FROM family_members; |
| Rename a column | SELECT name AS pet_name FROM family_members; |
| Rename a table | SELECT f.name FROM family_members AS f; |
| Sort ascending / descending | ORDER BY num_books_read ASC / DESC |
| First N rows | LIMIT 5 (SQL Server: SELECT TOP 5 ...) |
| Skip rows | LIMIT 5 OFFSET 10 |
| Comment | -- one line or /* many lines */ |
Practice: SELECT, choosing columns, DISTINCT, ORDER BY, LIMIT, column aliases. More: aliases, comments, TOP.
Filtering with WHERE
| Operator | Meaning | Example |
|---|---|---|
=, <> (or !=) | Equal, not equal | WHERE species = 'dog' |
>, >=, <, <= | Comparison | WHERE num_books_read > 100 |
AND, OR, NOT | Combine conditions | WHERE species = 'human' AND num_books_read > 100 |
IN | Matches any in a list | WHERE species IN ('dog', 'cat') |
BETWEEN | Inclusive range | WHERE num_books_read BETWEEN 10 AND 50 |
LIKE | Pattern match | WHERE name LIKE 'D%' |
IS NULL, IS NOT NULL | Missing values | WHERE favorite_book IS NULL |
Wildcards for LIKE: % matches any number of characters and _ matches exactly one. Text values use single quotes. NULL never equals anything, not even NULL, so = NULL never matches: use IS NULL.
Practice: WHERE, AND, OR, IN, LIKE, NULL. More: BETWEEN, wildcards, NULL values.
Aggregates and GROUP BY
| Function | Returns |
|---|---|
COUNT(*) | Number of rows |
COUNT(column) | Number of non-NULL values |
COUNT(DISTINCT column) | Number of unique values |
SUM(column), AVG(column) | Total and average |
MIN(column), MAX(column) | Smallest and largest |
SELECT species, COUNT(*) AS members, AVG(num_books_read) AS avg_books
FROM family_members
GROUP BY species
HAVING COUNT(*) > 1
ORDER BY members DESC;
GROUP BYmakes one output row per group. Every selected column must be grouped or aggregated.WHEREfilters rows before grouping.HAVINGfilters groups after.
Practice: COUNT, SUM, AVG, MAX and MIN, GROUP BY. More: GROUP BY guide, HAVING guide.
JOINs
A join combines rows from two tables using a matching condition, usually a foreign key pointing at a primary key.
| Join | Returns |
|---|---|
INNER JOIN (or just JOIN) | Only rows that match in both tables |
LEFT JOIN | All rows from the left table, plus matches from the right (NULL where none) |
RIGHT JOIN | All rows from the right table, plus matches from the left |
FULL OUTER JOIN | All rows from both tables, matched where possible |
CROSS JOIN | Every combination of rows from both tables |
| Self join | A table joined to itself, using two different aliases |
-- Inner join
SELECT character.name, character_actor.actor_name
FROM character
INNER JOIN character_actor
ON character.id = character_actor.character_id;
-- Left join: keep characters even if they have no actor
SELECT c.name, ca.actor_name
FROM character AS c
LEFT JOIN character_actor AS ca
ON c.id = ca.character_id;
-- Find rows with no match
SELECT c.name
FROM character AS c
LEFT JOIN character_actor AS ca ON c.id = ca.character_id
WHERE ca.character_id IS NULL;
SQLite only gained RIGHT and FULL OUTER JOIN in version 3.39, and MySQL has no FULL OUTER JOIN. When in doubt, swap the table order and use LEFT JOIN.
Practice: JOIN, multiple joins, LEFT JOIN, self join. More: INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL JOIN, joining 3 tables.
UNION, INTERSECT and EXCEPT
Set operators stack the results of two queries that return the same number of compatible columns.
| Operator | Result |
|---|---|
UNION | Rows from both queries, duplicates removed |
UNION ALL | Rows from both queries, duplicates kept (faster) |
INTERSECT | Rows that appear in both |
EXCEPT | Rows in the first query but not the second (Oracle: MINUS) |
More: UNION guide.
Common functions
| Task | Example |
|---|---|
| If/else logic | CASE WHEN num_books_read > 100 THEN 'bookworm' ELSE 'casual' END |
Replace NULL | COALESCE(favorite_book, 'none') |
| Substring | SUBSTR(name, 1, 3) (SQL Server: SUBSTRING) |
| Length of text | LENGTH(name) (SQL Server: LEN) |
| Upper / lower case | UPPER(name), LOWER(name) |
| Trim spaces | TRIM(name) |
| Join text | first_name || ' ' || last_name (MySQL / SQL Server: CONCAT(...)) |
| Round a number | ROUND(price, 2) |
| Change a type | CAST(price AS INTEGER) |
Practice: CASE, SUBSTR, COALESCE. More: CASE guide, ROUND, CAST, concatenating columns.
Window functions
A window function calculates something across a set of related rows without collapsing them into one row, which is how you rank rows or compare a row to the one before it. It always has an OVER clause.
SELECT name, species, num_books_read,
ROW_NUMBER() OVER (PARTITION BY species ORDER BY num_books_read DESC) AS rank_in_species,
LAG(num_books_read) OVER (ORDER BY num_books_read) AS previous_value,
SUM(num_books_read) OVER (PARTITION BY species) AS species_total
FROM family_members;
| Function | What it does |
|---|---|
ROW_NUMBER() | 1, 2, 3 ... with no ties |
RANK() | Ties share a rank, then a gap (1, 1, 3) |
DENSE_RANK() | Ties share a rank, no gap (1, 1, 2) |
LAG(col) / LEAD(col) | Value from the previous / next row |
SUM(col) OVER (...) | Any aggregate as a running or per-group total |
PARTITION BY splits rows into groups (like GROUP BY, without collapsing them) and ORDER BY inside OVER sets the order within each group. Window functions need SQLite 3.25+, MySQL 8+, PostgreSQL or SQL Server. More: ROW_NUMBER, DENSE_RANK, LAG and LEAD, PARTITION BY.
Subqueries and CTEs
-- Subquery in WHERE
SELECT name FROM family_members
WHERE num_books_read > (SELECT AVG(num_books_read) FROM family_members);
-- Common table expression (CTE): a named, reusable subquery
WITH readers AS (
SELECT species, SUM(num_books_read) AS total
FROM family_members
GROUP BY species
)
SELECT * FROM readers WHERE total > 100;
Practice: nested queries.
INSERT, UPDATE and DELETE
| Goal | Syntax |
|---|---|
| Add a row | INSERT INTO family_members (name, species) VALUES ('Rex', 'dog'); |
| Change rows | UPDATE family_members SET num_books_read = 5 WHERE name = 'Rex'; |
| Delete rows | DELETE FROM family_members WHERE name = 'Rex'; |
| Empty a table | TRUNCATE TABLE family_members; (not in SQLite: use DELETE FROM) |
Always include a WHERE on UPDATE and DELETE unless you really mean every row. Run the same WHERE in a SELECT first to see which rows you are about to change. More: INSERT INTO, UPDATE, DELETE, TRUNCATE vs DELETE vs DROP.
Creating and changing tables
CREATE TABLE pets (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
species TEXT DEFAULT 'dog',
owner_id INTEGER REFERENCES owners (id),
email TEXT UNIQUE
);
ALTER TABLE pets ADD COLUMN age INTEGER;
ALTER TABLE pets RENAME COLUMN name TO pet_name;
ALTER TABLE pets DROP COLUMN age; -- SQLite 3.35+, PostgreSQL, MySQL, SQL Server
CREATE INDEX idx_pets_owner ON pets (owner_id);
CREATE VIEW dogs AS SELECT * FROM pets WHERE species = 'dog';
DROP TABLE pets;
| Constraint | Rule it enforces |
|---|---|
PRIMARY KEY | Unique, non-null identifier for each row |
FOREIGN KEY / REFERENCES | Value must exist in another table's key |
NOT NULL | The column can't be empty |
UNIQUE | No duplicate values |
DEFAULT | Value used when none is given |
CHECK | Value must satisfy a condition |
Common data types: INTEGER, DECIMAL(p, s), VARCHAR(n) or TEXT, DATE, TIMESTAMP / DATETIME, BOOLEAN. Exact names vary by database. More: CREATE TABLE, ALTER, rename a column, drop a column, primary keys, indexes, views.
DDL, DML, DQL and DCL
You'll hear SQL commands grouped by what they do:
| Group | Stands for | Commands |
|---|---|---|
| DQL | Data Query Language | SELECT |
| DML | Data Manipulation Language | INSERT, UPDATE, DELETE |
| DDL | Data Definition Language | CREATE, ALTER, DROP, TRUNCATE |
| DCL | Data Control Language | GRANT, REVOKE |
| TCL | Transaction Control Language | BEGIN, COMMIT, ROLLBACK |
Details in What are DDL, DML, DQL and DCL?
Differences between databases
The core is identical everywhere. The differences are mostly in the small functions:
| Task | SQLite | PostgreSQL | MySQL | SQL Server |
|---|---|---|---|---|
| Limit rows | LIMIT n | LIMIT n | LIMIT n | TOP n or OFFSET ... FETCH |
| Today's date | date('now') | CURRENT_DATE | CURDATE() | CAST(GETDATE() AS DATE) |
| Concatenate | || | || | CONCAT() | CONCAT() or + |
| Text length | LENGTH() | LENGTH() | LENGTH() (bytes) / CHAR_LENGTH() | LEN() |
| Auto-increment key | INTEGER PRIMARY KEY | GENERATED ... AS IDENTITY or SERIAL | AUTO_INCREMENT | IDENTITY(1,1) |
| Quote an identifier | "name" | "name" | `name` | [name] |
Date handling differs the most. See our guides for MySQL, PostgreSQL, SQL Server (T-SQL) and SQLite.
Common mistakes this cheat sheet will save you from
WHERE column = NULLnever matches. UseIS NULL.WHERE COUNT(*) > 1is an error. UseHAVING COUNT(*) > 1.- Selecting a column that isn't in
GROUP BYor an aggregate. - Double quotes for text. Use single quotes for values:
'dog'. UPDATEorDELETEwith noWHERE.- A
JOINwith noONcondition, which multiplies rows.
Learn it by doing, not by reading
A cheat sheet helps you remember syntax you've already met. It won't teach you to think in SQL. For that you need to write queries and see results. SQL Easy has 31 short, free lessons, from your first SELECT to LEFT JOIN and CASE, that run in your browser with nothing to install and no signup. When you're ready to query your own data, Beekeeper Studio is a free, open source SQL editor for PostgreSQL, MySQL, SQLite and SQL Server. Not sure where to begin? Read Is SQL easy to learn?
Frequently asked questions
What are the most important SQL commands to learn first?
SELECT, FROM, WHERE, ORDER BY, GROUP BY and JOIN cover most day-to-day queries. After those, learn INSERT, UPDATE and DELETE for changing data, and CREATE TABLE for building it.
Is there a downloadable SQL cheat sheet PDF?
Use your browser's Print function on this page and choose "Save as PDF". You get an up-to-date copy that includes every table and example above.
Does this cheat sheet work for MySQL, PostgreSQL and SQL Server?
Yes. The queries in the sections above are standard SQL and run on all of them. Where syntax differs (limiting rows, dates, string functions), the notes and the comparison table call it out.
What is the difference between WHERE and HAVING?
WHERE filters individual rows before grouping, so it can't use aggregates like COUNT(*). HAVING filters groups after GROUP BY, so it can.
What is the difference between INNER JOIN and LEFT JOIN?
INNER JOIN keeps only rows with a match in both tables. LEFT JOIN keeps every row from the left table and fills the right-hand columns with NULL when there is no match.