×+
🔒maaji.site/roadmaps/sql
← Back to roadmaps

SQL 0 to Hero

Twenty one days from query mechanics to production SQL, in five phases. Every day carries the concepts, the mental model to walk out with, practice problems, theory to answer cold, and the traps that cost people the offer.

21 days178 problems117 theory questions5 phases

Remember this first

SQL does not run in the order you write it

The order you write

SELECTFROMJOINWHEREGROUP BYHAVINGORDER BYLIMIT

The order it executes

  1. 1FROMPick the source table and build the starting row set.
  2. 2JOIN / ONPull in the other tables and match rows together.
  3. 3WHEREFilter individual rows, before any grouping happens.
  4. 4GROUP BYCollapse the surviving rows into groups.
  5. 5HAVINGFilter those groups, now that aggregates exist.
  6. 6SELECTCompute the output columns and assign aliases.
  7. 7DISTINCTDrop duplicate rows from the output.
  8. 8ORDER BYSort the result, where SELECT aliases are finally usable.
  9. 9LIMIT / OFFSETCut the final slice that gets returned.

Why it matters

  • WHERE cannot see aggregates, because it runs before GROUP BY. That is the entire reason HAVING exists.
  • An alias created in SELECT is unavailable to WHERE, GROUP BY and HAVING, since they all run first. ORDER BY runs after SELECT, so there the alias works.
  • Filtering early in WHERE beats filtering late in HAVING, because fewer rows ever reach the grouping step.
Query mechanicsAnalytical SQLThe analytics businesses actually ask forThe engine underneathProduction and the interview

Day 1 of 21

Query mechanics

Reading and filtering data, and the order SQL actually runs in

query, filter and sort without hesitating, and understand why `WHERE` can't see a column alias.

Why this matters on the job

Ninety percent of the SQL you'll write in the first month on any job is exploration. Someone says "the numbers look wrong in the dashboard" and you go poking at raw tables with SELECT and WHERE until you find the row that broke. Speed here compounds. If filtering is automatic you spend your thinking on the actual problem.

The part of day 1 that people skip and later regret is the logical processing order. Every confusing SQL error you hit for the next two years traces back to it.

Concepts to cover

  • SELECT, FROM, WHERE, ORDER BY, LIMIT (Postgres, MySQL) vs TOP (SQL Server) vs FETCH FIRST n ROWS ONLY (standard)
  • DISTINCT and why it is usually a symptom, not a solution
  • Comparison operators, and string comparison behaviour with collation and case
  • AND, OR, NOT, IN, BETWEEN, LIKE, ILIKE, and operator precedence (AND binds tighter than OR)
  • Three-valued logic: TRUE, FALSE, UNKNOWN. IS NULL, IS NOT NULL, IS DISTINCT FROM
  • Aliasing with AS, and quoting rules for identifiers with spaces or reserved words
  • The logical query processing order
  • ORDER BY with NULLS FIRST / NULLS LAST, and multi-column sorts with mixed directions

The mental model to walk out with

SQL is written in one order and evaluated in another. The evaluation order is:

FROM  ->  WHERE  ->  GROUP BY  ->  HAVING  ->  SELECT  ->  DISTINCT  ->  ORDER BY  ->  LIMIT

That one line explains a pile of things:

  • WHERE cannot use an alias defined in SELECT, because SELECT hasn't run yet.
  • ORDER BY can use a SELECT alias, because it runs after.
  • WHERE cannot use an aggregate, because grouping hasn't happened yet. That's HAVING.
  • LIMIT 10 without ORDER BY gives you ten arbitrary rows, and they can change between runs. On a distributed engine they almost certainly will.

The second model is null logic. NULL is not a value, it is the absence of one. Anything compared to NULL returns UNKNOWN, and WHERE only keeps rows that evaluate to TRUE. So WHERE salary != 100 silently drops every employee whose salary is null. This is the single most common source of quietly wrong numbers in production reporting, and you've seen it survive code review at real companies.

-- these are not the same query
SELECT * FROM employees WHERE department != 'Sales';                    -- drops NULL departments
SELECT * FROM employees WHERE department IS DISTINCT FROM 'Sales';      -- keeps them
SELECT * FROM employees WHERE department != 'Sales' OR department IS NULL;  -- also keeps them

IS DISTINCT FROM is null-safe equality. MySQL spells it <=>. It comes up constantly in change-data-capture comparisons where you're asking "did this column actually change?" and both sides might be null.

When to reach for each thing on the job

DISTINCT deserves a note of its own. When you find yourself typing it, the honest question is: why are there duplicates? Usually it's a fan-out from a join you did wrong, and DISTINCT is papering over a modelling bug. On a big table it also forces a sort or hash of the entire result set, which is expensive. Real duplicates from a genuinely many-to-many source, fine. Reflexive DISTINCT on every query is a smell that interviewers notice.

LIKE 'foo%' can use an index. LIKE '%foo' cannot, because a B-tree is sorted left to right and a leading wildcard gives it no prefix to seek on. That's a day 17 topic but worth planting now.

Problems

LeetCode:

DataLemur:

PGExercises: the whole Basic section, it's about 10 questions and takes under an hour.

Theory to answer cold

What is SQL and what is it for?

A declarative language for describing the result set you want. You describe what, the query optimiser decides how. That distinction is the whole reason query plans exist and the reason two queries that look different can run identically. Reference

SQL vs NoSQL.

Relational databases give you a fixed schema, joins, and ACID transactions, and they scale vertically by default. NoSQL covers several different things: document stores (MongoDB), key-value (Redis, DynamoDB), wide-column (Cassandra), graph (Neo4j). They trade joins and strict consistency for horizontal scale and schema flexibility. As a data engineer the honest answer is that you use both, and the choice follows the access pattern. If you know the queries in advance and they're key-based at huge volume, wide-column wins. If the questions are unknown and analytical, relational wins. Reference

What is a PRIMARY KEY?

A column or set of columns that uniquely identifies a row. It is NOT NULL and UNIQUE, and most databases build an index on it automatically. In Postgres and SQL Server it also usually determines physical row ordering if it's the clustered index. One per table. Reference

Worth adding the warehouse caveat, because it's an easy way to look senior: Snowflake, BigQuery and Redshift accept primary key declarations but do not enforce them. They're metadata hints for the optimiser. So uniqueness in a warehouse is your job, enforced with a test, not the database's job.

CHAR vs VARCHAR.

CHAR(n) is fixed length and pads with spaces. VARCHAR(n) is variable length up to n. Use CHAR only when the value is genuinely always the same width, like a two-letter country code. In Postgres there is no performance benefit to VARCHAR(n) over TEXT, the limit is a constraint and nothing more. Reference

What does NULL mean and how do you handle it?

Absence of a value. Not zero, not empty string. Check it with IS NULL or IS NOT NULL, never = NULL, which returns UNKNOWN and therefore filters everything out. Replace it with COALESCE(col, fallback). Compare null-safely with IS DISTINCT FROM. And remember that NULL in an aggregate is skipped, not treated as zero: AVG(salary) over 10 rows with 3 nulls divides by 7.

What is the logical order of execution and why does it matter?

Covered above. If you can recite it and then use it to explain why a specific error happened, that's a strong signal in an interview.

What interviewers actually probe here

  • They hand you a WHERE col != 'x' query and ask why a row is missing. They want to hear "nulls".
  • They ask what LIMIT without ORDER BY returns. Correct answer: undefined, non-deterministic, and dangerous in a pipeline because the result changes between runs.
  • They ask the difference between WHERE and HAVING on day 1 to see whether you know the execution order.
  • Screen-share rounds watch whether you write SELECT *. On a columnar warehouse that's a real cost, since the engine only reads the columns you name.

Traps that cost people the offer

  • WHERE a = 1 OR a = 2 AND b = 3 does not mean what it looks like. AND binds first. Parenthesise.
  • NOT IN with a subquery that returns even one NULL gives you zero rows. Always. Use NOT EXISTS instead. This is the single nastiest gotcha in beginner SQL and it appears in interviews constantly.
  • BETWEEN on timestamps. BETWEEN '2026-01-01' AND '2026-01-31' excludes almost all of January 31st, because the end value becomes midnight. Use >= '2026-01-01' AND < '2026-02-01'. Half-open intervals, always.

Done when

  • You can recite the logical processing order and use it to explain a specific error.
  • You can explain, without hedging, why NOT IN (subquery with NULL) returns nothing.
  • All problems above solved without looking at solutions.