Skip to content
Reference

SQL Cheat Sheet

ReferenceUpdated September 24, 2026Every example verified
On this page 15 sections ▾
  1. The order SQL actually runs in
  2. SELECT basics
  3. WHERE operators
  4. JOINs
  5. Aggregate functions
  6. Subqueries
  7. CTEs: the WITH clause
  8. Set operators: UNION, INTERSECT, EXCEPT
  9. Window functions
  10. CASE — conditional values
  11. String functions
  12. Date functions
  13. NULL handling
  14. Changing data and tables
  15. Related reading

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:

StepClauseWhat happens
1FROM / JOINTables are combined into one working set of rows.
2WHEREIndividual rows are filtered. No aggregates or aliases exist yet.
3GROUP BYRemaining rows are collapsed into groups.
4HAVINGGroups are filtered — this is where aggregate conditions belong.
5SELECTOutput columns and aliases are computed.
6ORDER BYThe result is sorted. Aliases from SELECT can be used here.
7LIMIT / OFFSETThe 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

SyntaxDoes
SELECT *Every column.
SELECT a, bOnly the named columns, in that order.
SELECT a AS xRenames a column in the output.
SELECT DISTINCT aRemoves duplicate rows from the result.
ORDER BY a DESCSorts by column a, highest first (ASC is the default).
LIMIT 10Returns at most 10 rows. SQL Server writes SELECT TOP 10; standard SQL writes FETCH FIRST 10 ROWS ONLY.
LIMIT 10 OFFSET 20Skips 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

OperatorExampleMatches
= <> > < >= <=unit_price > 100Standard comparisons.
AND / OR / NOTa = 1 AND b = 2Combine conditions. Parenthesize when mixing AND/OR.
IN (...)tier IN ('Gold','Platinum')Any value in the list. Shorthand for chained ORs.
BETWEEN a AND bunit_price BETWEEN 10 AND 50Inclusive of both endpoints.
LIKEname 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 NULLmanager_id IS NULLMissing values. = NULL never matches anything.

JOINs

TypeReturns
INNER JOINOnly rows that match in both tables. The default when you write plain JOIN.
LEFT JOINEvery row from the left table, matched data where it exists, NULLs where it doesn't.
RIGHT JOINThe mirror of LEFT JOIN — every row from the right table.
FULL OUTER JOINEvery 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 JOINEvery 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

FunctionReturns
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

FormUse
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 tA 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

SyntaxDoes
WITH t AS (SELECT ...) SELECT ... FROM tNames 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

OperatorReturns
UNIONRows from both queries, duplicates removed.
UNION ALLRows from both queries, duplicates kept. Cheaper, because nothing has to be compared.
INTERSECTRows that appear in both. MySQL has it from 8.0.31.
EXCEPTRows 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

SyntaxGives 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

TaskSQLite (runs here)Elsewhere
Change caseUPPER(s), LOWER(s)The same everywhere.
LengthLENGTH(s)SQL Server: LEN(s). MySQL’s LENGTH counts bytes; CHAR_LENGTH counts characters.
Part of a stringSUBSTR(s, 1, 5)SUBSTRING(s, 1, 5) everywhere. Positions count from 1.
Find textINSTR(s, '@')MySQL: INSTR. PostgreSQL: POSITION('@' IN s). SQL Server: CHARINDEX('@', s).
Replace textREPLACE(s, 'old', 'new')The same everywhere.
Trim spacesTRIM(s), LTRIM(s), RTRIM(s)The same (TRIM in SQL Server from 2017).
Join texta || b, or CONCAT(a, b)MySQL: CONCAT(a, b). SQL Server: a + b or CONCAT(a, b).
Join a group’s valuesGROUP_CONCAT(s, ', ')PostgreSQL and SQL Server: STRING_AGG(s, ', '). MySQL: GROUP_CONCAT(s SEPARATOR ', ').

Date functions

TaskSQLite (runs here)PostgreSQLMySQLSQL Server
Todaydate('now')CURRENT_DATECURDATE()CAST(GETDATE() AS date)
Year of dstrftime('%Y', d)EXTRACT(YEAR FROM d)YEAR(d)YEAR(d)
d plus 7 daysdate(d, '+7 days')d + INTERVAL '7 days'DATE_ADD(d, INTERVAL 7 DAY)DATEADD(day, 7, d)
Days from a to bjulianday(b) - julianday(a)b - aDATEDIFF(b, a)DATEDIFF(day, a, b)
First of d’s monthstrftime('%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

SyntaxRule
x IS NULLThe 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 || NULLNULL in SQLite and PostgreSQL. CONCAT() skips NULLs, except in MySQL.
ORDER BY xAscending, 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_countnot_exists_count
07

● 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

StatementDoes
INSERT INTO t (a,b) VALUES (1,2)Adds a new row.
UPDATE t SET a=1 WHERE id=5Changes existing rows. Omit WHERE and every row changes.
DELETE FROM t WHERE id=5Removes 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 DATEAdds a column. SQL Server leaves out the word COLUMN.
DROP TABLE tDeletes 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 practising

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.