SQL Cheat Sheet
Query patterns you'll use every day.
These are the query patterns I reach for most, written for MySQL 8. Most of them work unchanged in PostgreSQL and SQL Server. Where they don't, it's usually the date functions.
The queries use made-up tables (sales, emp, students) so you can see the shape of each pattern. Swap in your own table and column names.
-- Filtering & sorting
SELECT * FROM t WHERE a BETWEEN 1 AND 10 AND b IN ('x','y') ORDER BY c DESC LIMIT 20;
-- Aggregation
SELECT city, COUNT(*), SUM(amount) FROM sales GROUP BY city HAVING SUM(amount) > 100000;
-- Joins
SELECT * FROM a LEFT JOIN b ON b.a_id = a.id;
-- CTE
WITH monthly AS (
SELECT DATE_FORMAT(dt, '%Y-%m') m, SUM(amount) total FROM sales GROUP BY m
)
SELECT * FROM monthly ORDER BY m;
-- Window functions
SELECT name, salary, RANK() OVER (PARTITION BY dept ORDER BY salary DESC) r FROM emp;
-- CASE
SELECT name, CASE WHEN marks >= 60 THEN 'Pass' ELSE 'Fail' END AS result FROM students;
When to use which
WHERE vs HAVING. WHERE filters rows before grouping, HAVING filters groups after. "Cities with total sales over 1 lakh" needs HAVING, because the total doesn't exist until the grouping is done.
LEFT JOIN when you can't afford to lose rows. An inner JOIN drops any row that doesn't match. If you're listing all customers with their order count, a customer with zero orders vanishes with an inner join. Use LEFT JOIN and COUNT(o.id) (not COUNT(*)) so they show up with 0.
CTEs make long queries readable. The WITH … AS (…) block is just a named subquery. When a query has three levels of nesting, pulling each level into a CTE makes it far easier to check and to explain to someone else.
Window functions don't collapse rows. GROUP BY gives you one row per group. A window function like RANK() OVER (PARTITION BY dept …) keeps every row and adds a column next to it. That's what makes "top earner per department" or "running total by month" easy.
Mistakes I see often
WHERE col = NULLnever matches anything. UseIS NULL.COUNT(col)skips NULLs,COUNT(*)doesn't. They can give different numbers on the same table.BETWEEN '2026-01-01' AND '2026-01-31'on aDATETIMEcolumn misses everything on the 31st after midnight. Use>= '2026-01-01' AND < '2026-02-01'.DATE_FORMATis MySQL-only. In PostgreSQL it'sTO_CHAR(dt, 'YYYY-MM').- Running
UPDATEorDELETEwithout aWHERE. Write theWHEREfirst, run it as aSELECTto see which rows match, and only then change it toUPDATE.