SQL cheat sheet
SELECT, joins, GROUP BY/HAVING and indexes: the SQL basics that cover the vast majority of everyday queries.
Reading data
SELECT name, email FROM users WHERE active = 1 ORDER BY name ASC LIMIT 20; SELECT DISTINCT country FROM customers; SELECT COUNT(*) AS total FROM orders WHERE status = 'paid';
Joins
SELECT c.name, o.total FROM orders o JOIN customers c ON c.id = o.customer_id; -- LEFT JOIN keeps every left-hand row even without a match SELECT c.name, o.total FROM customers c LEFT JOIN orders o ON o.customer_id = c.id;
| Join | Returns |
|---|---|
INNER JOIN |
Only rows that match on both sides |
LEFT JOIN |
All left-hand rows + matches on the right (NULL otherwise) |
RIGHT JOIN |
The mirror of LEFT JOIN |
FULL OUTER JOIN |
All rows from both sides |
Aggregation
SELECT customer_id, SUM(total) AS revenue, COUNT(*) AS order_count FROM orders GROUP BY customer_id HAVING SUM(total) > 1000 ORDER BY revenue DESC;
WHERE filters rows before grouping; HAVING filters groups after GROUP BY. You can't use an aggregate function (SUM, COUNT…) inside a WHERE.
Writing data
INSERT INTO users (name, email) VALUES ('Ana', 'ana@example.com'); UPDATE orders SET status = 'shipped' WHERE id = 42; DELETE FROM carts WHERE created_at < NOW() - INTERVAL 30 DAY;
Indexes and performance
CREATE INDEX idx_orders_customer ON orders (customer_id); EXPLAIN SELECT * FROM orders WHERE customer_id = 42;
Subqueries and CTEs
-- subquery in the WHERE clause SELECT name FROM customers WHERE id IN (SELECT customer_id FROM orders WHERE total > 500); -- CTE (WITH), more readable for complex queries WITH big_spenders AS ( SELECT customer_id, SUM(total) AS revenue FROM orders GROUP BY customer_id HAVING SUM(total) > 1000 ) SELECT c.name, b.revenue FROM big_spenders b JOIN customers c ON c.id = b.customer_id ORDER BY b.revenue DESC;
Window functions
-- rank each customer's orders without collapsing rows (unlike GROUP BY) SELECT customer_id, total, RANK() OVER (PARTITION BY customer_id ORDER BY total DESC) AS rank, SUM(total) OVER (PARTITION BY customer_id) AS customer_total FROM orders;
Unlike GROUP BY, which merges rows, a window function keeps every individual row while computing an aggregate value "alongside" it — perfect for a ranking, a running total, or a comparison to the group average without losing the row-level detail.
Frequently asked questions
What is the difference between DELETE and TRUNCATE?
`DELETE` removes rows one by one, can take a `WHERE` clause and is logged (can be rolled back in a transaction). `TRUNCATE` empties the whole table at once, faster, but irreversible and cannot take a `WHERE`.
Why is my query slow even with an index?
An index only helps if the column is used bare in the `WHERE`/`JOIN` (not wrapped in a function like `LOWER(column)`) and the optimizer judges it faster than a full scan. `EXPLAIN` shows the plan actually chosen.
When should I use a window function instead of GROUP BY?
Whenever you need each row's detail IN ADDITION TO an aggregate value (a rank, a running total, a comparison to the average) — GROUP BY only returns one row per group and loses the detail.
Thanks for the feedback!