TLDR reference
SQL cheatsheet
A searchable reference for the SQL you write most - joins, filtering, aggregation, and common query patterns - each with a copyable snippet. Type to search, or filter by group. Everything runs in your browser.
33 shown
-
Select specific columns
QueryingSELECT name, email FROM users;
Returns just the columns you name. Prefer this over SELECT * so the result is stable and lean.
-
Sort the result
QueryingSELECT name, created_at FROM users ORDER BY created_at DESC;
ORDER BY sorts rows; DESC reverses to newest-first. Add more columns for tie-breakers.
-
Limit how many rows
QueryingSELECT * FROM users ORDER BY id LIMIT 10 OFFSET 20;
LIMIT caps the row count; OFFSET skips ahead, the basis of pagination. SQL Server uses TOP / FETCH.
-
Rename a column or table
QueryingSELECT name AS full_name FROM users AS u;
AS gives a column or table an alias, handy for readable output and shorter join conditions.
-
Remove duplicate rows
QueryingSELECT DISTINCT country FROM users;
DISTINCT collapses identical rows so each country appears once.
-
Filter rows
FilteringSELECT * FROM orders WHERE total > 100;
WHERE keeps only rows that satisfy the condition. It runs before grouping and sorting.
-
Combine conditions
FilteringSELECT * FROM users WHERE active = true AND country = 'CZ';
Chain conditions with AND / OR; parenthesise to control precedence.
-
Match a set of values
FilteringSELECT * FROM orders WHERE status IN ('paid', 'shipped');IN tests membership of a list - cleaner than a chain of OR comparisons.
-
Match a range
FilteringSELECT * FROM orders WHERE created_at BETWEEN '2026-01-01' AND '2026-06-30';
BETWEEN is inclusive on both ends. Works for numbers and dates alike.
-
Pattern match text
FilteringSELECT * FROM users WHERE email LIKE '%@gmail.com';
LIKE matches patterns: % is any run of characters, _ is a single one. ILIKE is case-insensitive in Postgres.
-
Find rows with no value
FilteringSELECT * FROM users WHERE deleted_at IS NULL;
NULL is unknown, so test it with IS NULL / IS NOT NULL - never with = NULL, which is never true.
-
Keep only matching rows (INNER JOIN)
JoinsSELECT o.id, u.name FROM orders o INNER JOIN users u ON u.id = o.user_id;
Returns rows that have a match in both tables. Orders without a user, or users without an order, drop out.
-
Keep all left rows (LEFT JOIN)
JoinsSELECT u.name, o.id FROM users u LEFT JOIN orders o ON o.user_id = u.id;
Returns every user, with NULLs where they have no order. The way to find users who never ordered.
-
Keep all right rows (RIGHT JOIN)
JoinsSELECT u.name, o.id FROM users u RIGHT JOIN orders o ON o.user_id = u.id;
The mirror of LEFT JOIN: every order, with NULLs where the user is missing. Rare; usually rewritten as a LEFT JOIN.
-
Keep unmatched from both (FULL OUTER JOIN)
JoinsSELECT u.name, o.id FROM users u FULL OUTER JOIN orders o ON o.user_id = u.id;
Returns matched rows plus unmatched rows from each side, padded with NULLs. MySQL lacks it; emulate with UNION.
-
Find rows with no match
JoinsSELECT u.name FROM users u LEFT JOIN orders o ON o.user_id = u.id WHERE o.id IS NULL;
A LEFT JOIN then a NULL check on the right key is the classic "users with no orders" anti-join.
-
Count rows
AggregationSELECT COUNT(*) FROM users;
COUNT(*) counts all rows; COUNT(col) counts non-NULL values; COUNT(DISTINCT col) counts unique ones.
-
Sum, average, min, max
AggregationSELECT SUM(total), AVG(total), MIN(total), MAX(total) FROM orders;
The core aggregate functions. They ignore NULLs and collapse the whole table to one row.
-
Aggregate per group
AggregationSELECT user_id, COUNT(*) AS orders FROM orders GROUP BY user_id;
GROUP BY produces one row per group; every selected column must be grouped or aggregated.
-
Filter groups
AggregationSELECT user_id, COUNT(*) AS orders FROM orders GROUP BY user_id HAVING COUNT(*) > 5;
HAVING filters after aggregation, where WHERE cannot reach. Use it to test aggregate results.
-
Subquery in WHERE
Subqueries & CTEsSELECT * FROM users WHERE id IN (SELECT user_id FROM orders);
A subquery feeds a value or list to the outer query - here, users who have at least one order.
-
Common table expression (CTE)
Subqueries & CTEsWITH recent AS ( SELECT * FROM orders WHERE created_at > '2026-01-01' ) SELECT user_id, COUNT(*) FROM recent GROUP BY user_id;
WITH names a subquery so you can reuse it and read top-to-bottom instead of nesting.
-
EXISTS check
Subqueries & CTEsSELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
EXISTS stops at the first matching row, often faster than IN for a correlated check.
-
Rank rows
Window functionsSELECT name, total, RANK() OVER (ORDER BY total DESC) AS rank FROM orders;
A window function computes across a set of rows without collapsing them. RANK leaves gaps after ties.
-
One row per group (deduplicate)
Window functionsSELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn FROM orders ) t WHERE rn = 1;
ROW_NUMBER per partition keeps the latest order per user - the standard deduplication pattern.
-
Running total
Window functionsSELECT created_at, total, SUM(total) OVER (ORDER BY created_at) AS running FROM orders;
A windowed SUM with ORDER BY accumulates a running total row by row.
-
Insert rows
Modifying dataINSERT INTO users (name, email) VALUES ('Ada', 'ada@example.com');Adds a row. List multiple parenthesised tuples to insert several at once.
-
Update rows
Modifying dataUPDATE users SET active = false WHERE last_login < '2025-01-01';
Changes existing rows. Always include a WHERE clause - without one, every row is updated.
-
Delete rows
Modifying dataDELETE FROM users WHERE active = false;
Removes rows. Like UPDATE, a missing WHERE clause wipes the whole table.
-
Insert or update (upsert)
Modifying dataINSERT INTO settings (key, value) VALUES ('theme', 'dark') ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;Upsert inserts, or updates if the key already exists. MySQL spells it ON DUPLICATE KEY UPDATE.
-
Create a table
SchemaCREATE TABLE users ( id SERIAL PRIMARY KEY, email TEXT UNIQUE NOT NULL, created_at TIMESTAMP DEFAULT now() );
Defines a table with column types and constraints. SERIAL is Postgres auto-increment; MySQL uses AUTO_INCREMENT.
-
Add a column
SchemaALTER TABLE users ADD COLUMN age INT;
ALTER TABLE changes an existing table - add, drop, or rename columns and constraints.
-
Create an index
SchemaCREATE INDEX idx_users_email ON users (email);
An index speeds up lookups and joins on a column at the cost of slightly slower writes.
No patterns match your search.
How it works
A searchable SQL cheatsheet covering the queries you write most: selecting and filtering rows, joining tables, grouping and aggregating, modifying data, and shaping a schema. Each card pairs a copyable snippet with a plain-language explanation of what it does and when to reach for it. Type to search by keyword - "join", "group by", "upsert", "index" - or use the chips to browse a single group such as joins or aggregation.
The syntax sticks to standard SQL that works across PostgreSQL, MySQL, SQLite, and SQL Server, with a note where a dialect differs. It walks through the join types that trip people up - inner, left, right, and full outer - alongside the common patterns for counting, ranking, deduplicating, and updating rows safely. Everything is static and runs in your browser, so the lookup is instant and works offline once the page has loaded.
Example. Searching "join" lines up the four kinds: an INNER JOIN keeps only rows that match in both tables, a LEFT JOIN keeps every row from the left table and fills in NULLs where the right has no match, and a FULL OUTER JOIN keeps unmatched rows from both sides. Filtering by the Aggregation chip groups COUNT, GROUP BY, and HAVING together.
FAQ
What is the difference between an INNER JOIN and a LEFT JOIN?
An INNER JOIN returns only the rows that have a match in both tables - if a customer has no orders, that customer simply does not appear. A LEFT JOIN (short for LEFT OUTER JOIN) returns every row from the left table regardless, and fills the right-hand columns with NULL where there is no match, so the customer with no orders still shows up with empty order fields. Reach for a LEFT JOIN whenever "include everything from this table, even without a match" is the question, such as finding customers who have never ordered.
What is the difference between WHERE and HAVING?
WHERE filters individual rows before they are grouped, while HAVING filters the groups after aggregation. Because an aggregate like COUNT(*) or SUM(total) does not exist until the rows are grouped, you cannot reference it in WHERE - you put that condition in HAVING instead. A typical query uses both: WHERE narrows the raw rows (say, orders from this year), GROUP BY collapses them per customer, and HAVING keeps only the groups that meet an aggregate test (say, more than five orders).
How do I remove duplicate rows from a result?
For a quick distinct list, SELECT DISTINCT collapses identical rows in the output. When you need to keep one row per group based on some ordering - the latest order per customer, say - a window function is the standard tool: number the rows with ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY created_at DESC) in a subquery, then keep only the rows where that number equals 1. To delete duplicates from the table itself, the same row-numbering trick identifies which copies to remove.
Why should I always pair UPDATE and DELETE with a WHERE clause?
Because without a WHERE clause they apply to every row in the table. An UPDATE with no WHERE rewrites the column for all rows, and a DELETE with no WHERE empties the table entirely - both run instantly and both are easy to trigger by accident. The safe habit is to write the WHERE clause first, test it with a SELECT to confirm it matches exactly the rows you mean, and only then change the SELECT into an UPDATE or DELETE. Running inside a transaction gives you a ROLLBACK if something still goes wrong.