DiagramDB / SQL Cheat Sheet

SQL Cheat Sheet

The statements you reach for every week, each with a working example you can copy. Search it, or jump to a section.

Querying data

SELECT columns

Return specific columns. Avoid SELECT * in application code.

SELECT id, email FROM users;

WHERE

Filter rows before grouping.

SELECT * FROM orders WHERE status = 'paid' AND total > 100;

ORDER BY + LIMIT

Sort, then take the first rows. SQL Server: SELECT TOP 10 …; Oracle/standard: FETCH FIRST 10 ROWS ONLY.

SELECT * FROM posts ORDER BY published_at DESC LIMIT 10 OFFSET 20;

DISTINCT

Remove duplicate result rows.

SELECT DISTINCT country FROM customers;

IN, BETWEEN, LIKE

Set membership, inclusive ranges, pattern match (% any run, _ one char).

SELECT * FROM products
WHERE category IN ('tea', 'coffee')
  AND price BETWEEN 5 AND 20
  AND name LIKE 'Earl%';

IS NULL

NULL never equals anything, not even NULL. Use IS NULL / IS NOT NULL.

SELECT * FROM users WHERE deleted_at IS NULL;

CASE

Conditional values inside a query.

SELECT name,
  CASE WHEN total >= 1000 THEN 'gold'
       WHEN total >= 100 THEN 'silver'
       ELSE 'bronze' END AS tier
FROM customers;

COALESCE

First non-NULL value; handy for defaults.

SELECT COALESCE(nickname, first_name, 'Guest') AS display FROM users;

Aggregation

Aggregate functions

COUNT, SUM, AVG, MIN, MAX over all rows.

SELECT COUNT(*), SUM(total), AVG(total) FROM orders;

GROUP BY

One result row per group. Every selected column must be grouped or aggregated.

SELECT customer_id, COUNT(*) AS orders
FROM orders
GROUP BY customer_id;

HAVING

Filter groups after aggregation (WHERE filters rows before).

SELECT customer_id, SUM(total) AS spent
FROM orders
GROUP BY customer_id
HAVING SUM(total) > 500;

COUNT(DISTINCT)

Count unique values.

SELECT COUNT(DISTINCT customer_id) FROM orders;

Joins and set operations

INNER JOIN

Rows that match in both tables.

SELECT o.id, c.name
FROM orders o
JOIN customers c ON c.id = o.customer_id;

LEFT JOIN

All rows from the left table; NULLs where nothing matches.

SELECT c.name, o.id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id;

Anti join

Rows with no match.

SELECT c.*
FROM customers c
WHERE NOT EXISTS (
  SELECT 1 FROM orders o WHERE o.customer_id = c.id
);

Self join

A table joined to itself via two aliases.

SELECT e.name, m.name AS manager
FROM employees e
LEFT JOIN employees m ON m.id = e.manager_id;

UNION / UNION ALL

Stack results. UNION removes duplicates (slower); UNION ALL keeps them.

SELECT email FROM customers
UNION
SELECT email FROM newsletter_signups;

Subqueries, CTEs and window functions

Subquery

A query inside a query.

SELECT * FROM products
WHERE price > (SELECT AVG(price) FROM products);

CTE (WITH)

Name a subquery to make long queries readable.

WITH big_spenders AS (
  SELECT customer_id FROM orders
  GROUP BY customer_id HAVING SUM(total) > 1000
)
SELECT c.* FROM customers c
JOIN big_spenders b ON b.customer_id = c.id;

Recursive CTE

Walk a hierarchy such as an org chart or category tree.

WITH RECURSIVE chain AS (
  SELECT id, manager_id, name FROM employees WHERE id = 7
  UNION ALL
  SELECT e.id, e.manager_id, e.name
  FROM employees e JOIN chain ON e.id = chain.manager_id
)
SELECT * FROM chain;

Window: ROW_NUMBER

Number rows within a partition without collapsing them. Great for “latest per group”.

SELECT * FROM (
  SELECT o.*, ROW_NUMBER() OVER (
    PARTITION BY customer_id ORDER BY placed_at DESC) AS rn
  FROM orders o
) t WHERE rn = 1;

Window: running total

Aggregates over an ordered frame.

SELECT placed_at, total,
  SUM(total) OVER (ORDER BY placed_at) AS running_total
FROM orders;

Window: LAG / LEAD

Look at the previous or next row.

SELECT day, revenue,
  revenue - LAG(revenue) OVER (ORDER BY day) AS change
FROM daily_revenue;

Changing data

INSERT

Add rows; list the columns explicitly.

INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada'), ('alan@example.com', 'Alan');

INSERT … SELECT

Copy rows from a query.

INSERT INTO archived_orders
SELECT * FROM orders WHERE placed_at < '2025-01-01';

UPDATE

Always test the WHERE clause with a SELECT first.

UPDATE users SET plan = 'pro' WHERE id = 42;

DELETE

Without WHERE, deletes every row.

DELETE FROM sessions WHERE expires_at < NOW();

Upsert

Insert or update on conflict. PostgreSQL/SQLite shown; MySQL: ON DUPLICATE KEY UPDATE; SQL Server/Oracle: MERGE.

INSERT INTO stock (sku, qty) VALUES ('A1', 5)
ON CONFLICT (sku) DO UPDATE SET qty = stock.qty + EXCLUDED.qty;

Transaction

All-or-nothing changes.

BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 1;
UPDATE accounts SET balance = balance + 50 WHERE id = 2;
COMMIT;  -- or ROLLBACK;

Schema (DDL)

CREATE TABLE

Define columns, keys and constraints.

CREATE TABLE orders (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  customer_id BIGINT NOT NULL REFERENCES customers(id),
  total NUMERIC(12,2) NOT NULL CHECK (total >= 0),
  placed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

ALTER TABLE

Add, change or drop columns and constraints.

ALTER TABLE users ADD COLUMN last_login TIMESTAMPTZ;
ALTER TABLE users DROP COLUMN legacy_flag;

CREATE INDEX

Speed up lookups on columns you filter, join or sort by.

CREATE INDEX idx_orders_customer ON orders (customer_id);
CREATE UNIQUE INDEX idx_users_email ON users (email);

CREATE VIEW

A saved query that behaves like a table.

CREATE VIEW paid_orders AS
SELECT * FROM orders WHERE status = 'paid';

DROP / TRUNCATE

DROP removes the table; TRUNCATE empties it fast.

TRUNCATE TABLE staging_events;
DROP TABLE IF EXISTS tmp_import;

EXPLAIN

Show the query plan. PostgreSQL: EXPLAIN ANALYZE runs it and shows real timings.

EXPLAIN ANALYZE
SELECT * FROM orders WHERE customer_id = 42;

Not on the sheet? Ask a SQL question

Answered by an AI model, scoped to SQL and database design. Only your question is sent.

How to read a query: the logical order

SQL is written SELECT … FROM … WHERE … but evaluated roughly as FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT. Once you know that order, most “why doesn’t this work” moments explain themselves: an alias defined in SELECT doesn’t exist yet in WHERE, and an aggregate can’t appear in WHERE because the groups haven’t been formed. See joins in action on the SQL join types page, and tidy any example with the SQL formatter.

Questions

In what order does SQL run a query?

Logically: FROM and JOIN, then WHERE, GROUP BY, HAVING, SELECT (including window functions), DISTINCT, ORDER BY, and finally LIMIT/OFFSET. That is why you cannot use a SELECT alias in WHERE, but you can in ORDER BY.

What is the difference between WHERE and HAVING?

WHERE filters individual rows before they are grouped; HAVING filters groups after aggregation, so it can use COUNT, SUM and other aggregates.

Does this cheat sheet work for MySQL, PostgreSQL and SQL Server?

The core statements are standard SQL and work everywhere. Where syntax differs, as in LIMIT vs TOP, upserts and identity columns, the description names the alternatives for each database.

Can I print it?

Yes. Use your browser’s print command; the page prints without navigation, with each command and its example.

Related tools