SQL has no forgiveness. One wrong keyword and the query breaks. One bad JOIN condition and you get a million rows instead of a thousand. One WHERE clause in the wrong place and your LEFT JOIN silently becomes an INNER JOIN.
**SQL execution order**
``` FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → WINDOW → ORDER BY → LIMIT ```
Most SQL bugs come from putting logic in the wrong phase. WHERE filters rows before grouping—you can't use aggregates there. HAVING filters after. Window functions compute after aggregation but before output. Know this order and half your bugs disappear.
**Learning paths**
1. **Day-to-day SQL** (most common): Filtering → Joins → Aggregation → Sorting → Subqueries → CTEs → Window Functions → Data Modification
2. **Interview prep** (what shows up on screeners): Filtering → Joins → Aggregation → Subqueries → Window Functions → Sorting
3. **Backend safety** (production writes): Filtering → Sorting (pagination) → Data Modification (safe writes) → Joins → Aggregation
Popular topics include SQL Filtering Practice: WHERE, LIKE, IN, BETWEEN, NULL, SQL Joins Practice, SQL Aggregation Practice: GROUP BY, HAVING, COUNT, SUM, AVG.
Built for SQL Learners
- Analysts and data engineers who query production data.
- Backend devs who write SQL every week but still trip on syntax.
- Prepping for data, analytics, or backend interviews.
- Learning relational databases and want the syntax to stick.
- Avoid LEFT JOIN silently turning into INNER JOIN (ON vs WHERE).
- Write NULL-safe filters without guessing (IS NULL, IS DISTINCT FROM, NOT EXISTS).
- Paginate without duplicates or skipped rows (tie-breakers + keyset pagination).
- Use window functions correctly: ROWS vs RANGE frames, LAST_VALUE trap, CTE filtering pattern.
- Run UPDATE/DELETE safely: preview first, use RETURNING, wrap in transactions.
- Write GROUP BY and HAVING without confusing which filters where.
Practice by Topic
Pick a concept and train recall with small, focused reps.
SQL Filtering Practice
Stop the NULL comparison bugs and AND/OR precedence mistakes.
Show 3 gotchasHide gotchas
= NULLnever matches—useIS NULL. SQL uses three-valued logic.NOT IN (subquery)returns 0 rows if subquery contains NULL—useNOT EXISTS.- AND binds tighter than OR:
a OR b AND c=a OR (b AND c)—always parenthesize.
SQL Joins Practice
Stop LEFT JOIN silently becoming INNER JOIN.
Show 3 gotchasHide gotchas
- WHERE on the right table drops NULL rows—move the filter to ON.
- Joining on non-unique keys multiplies rows (row explosion).
- FULL OUTER JOIN not supported in MySQL—use UNION of LEFT + RIGHT.
SQL Aggregation Practice
Stop confusing WHERE vs HAVING and getting inflated counts.
Show 3 gotchasHide gotchas
- WHERE filters rows *before* GROUP BY; HAVING filters *after* aggregation.
- COUNT(*) counts all rows; COUNT(column) ignores NULLs—they differ with LEFT JOIN.
- JOINs can multiply rows before aggregation—pre-aggregate or use COUNT(DISTINCT).
SQL Sorting & Pagination Practice
Stop getting duplicates and skipped rows in pagination.
Show 3 gotchasHide gotchas
- LIMIT without deterministic ORDER BY gives unpredictable subsets—add a unique tie-breaker.
- OFFSET is slow for deep pages (scans + discards)—use keyset/cursor pagination.
- NULL ordering varies by database—use NULLS LAST or the portable
(col IS NULL)trick.
SQL Subqueries Practice
Know when IN vs EXISTS matters and avoid the NOT IN trap.
Show 3 gotchasHide gotchas
- NOT IN + NULL = 0 rows. The NULL comparison makes everything UNKNOWN.
- Correlated subqueries can run per-row—rewrite as JOIN + GROUP BY when possible.
- LATERAL lets subqueries in FROM reference outer columns (top-1 per row pattern).
SQL Window Functions Practice
Master frames, ranking, and the CTE filtering pattern.
Show 3 gotchasHide gotchas
- Can't filter window functions in WHERE—wrap in CTE, then filter (or use QUALIFY).
- Default frame is RANGE (ties grouped)—use ROWS for deterministic running totals.
- LAST_VALUE returns current row with default frame—extend to UNBOUNDED FOLLOWING.
SQL CTEs Practice
Write readable multi-step queries and recursive traversals.
Show 3 gotchasHide gotchas
- CTEs are materialized differently by database—Postgres 12+ inlines unless MATERIALIZED.
- WITH RECURSIVE needs a base case and a recursive case joined by UNION ALL.
- Circular recursion without a stopping condition = infinite loop.
SQL Data Modification Practice
Write UPDATE/DELETE without destroying production data.
Show 3 gotchasHide gotchas
- UPDATE/DELETE without WHERE affects ALL rows—preview with SELECT first.
- Always use RETURNING to verify what changed before COMMIT.
- ON CONFLICT requires a unique constraint on the conflict target.
SQL Cheat Sheets
Copy-ready syntax references for quick lookup
What You'll Practice
Real SQL patterns you'll type from memory
SELECT c.*
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.customer_id IS NULL;WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY amount DESC, id DESC
) AS rn
FROM orders
)
SELECT * FROM ranked WHERE rn <= 3;SELECT *
FROM users
WHERE (created_at, id) < (:cursor_ts, :cursor_id)
ORDER BY created_at DESC, id DESC
LIMIT 20;SELECT date, amount,
SUM(amount) OVER (
ORDER BY date, id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM transactions;BEGIN;
SELECT * FROM users WHERE status = 'inactive';
UPDATE users SET deleted_at = NOW()
WHERE status = 'inactive'
RETURNING id, email;
COMMIT;SELECT category,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
COUNT(*) AS total
FROM orders
GROUP BY category;SELECT * FROM orders
WHERE created_at >= '2024-01-01'
AND created_at < '2024-02-01';INSERT INTO stats (user_id, date, count)
VALUES (1, '2024-01-15', 1)
ON CONFLICT (user_id, date)
DO UPDATE SET count = stats.count + EXCLUDED.count;SELECT name,
COALESCE(nickname, name) AS display_name,
COALESCE(discount, 0) AS discount
FROM users;Sample Exercises
Select all orders from the orders table where the status equals 'shipped'.
SELECT * FROM orders WHERE status = 'shipped'Write a query to join the orders table with the users table on user_id, selecting all columns.
SELECT * FROM orders INNER JOIN users ON orders.user_id = users.idWhy this works
You lose SQL syntax fast if you don't use it regularly. The algorithm re-tests each pattern right before you'd forget it. A few rounds of that and the queries come out clean on the first try.
What to expect
Week 1: The algorithm exposes your weak spots—maybe NULL handling, maybe window function frames, maybe pagination. You'll see which patterns you blank on.
Week 2-4: Intervals stretch. Things you got right move to 2-day, 4-day, then weekly reviews. The gotchas that burned you become obvious.
Month 2+: SQL syntax is muscle memory. Reviews take 10 minutes. You're maintaining, not catching up.
FAQ
Standard SQL core, with dialect notes where syntax differs. You learn the portable patterns first, then we note where Postgres uses ON CONFLICT while MySQL uses ON DUPLICATE KEY, or where FULL OUTER JOIN needs a UNION workaround in MySQL.
Yes. The sorting topic covers both OFFSET pagination (with deterministic tie-breakers) and keyset/cursor pagination for performance. If pagination bugs have bitten you before, this fixes that.
Start with Filtering (WHERE clause, NULL handling), then Joins, then Aggregation (GROUP BY/HAVING). That's the foundation for everything else. Window functions and CTEs come after you're solid on the basics.
The exercises match what shows up in technical screenings: JOINs, aggregation, window functions (ROW_NUMBER for top-N), subqueries. The interview path focuses on these patterns.
Most tutorials skip the gotchas: ROWS vs RANGE default frames, LAST_VALUE returning current row, ORDER BY in OVER not sorting output, ROW_NUMBER needing tie-breakers. We drill the edge cases that bite you in production.