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.
Remember this first
The order you write
The order it executes
Why it matters
Day 1 of 21
Query mechanicsquery, filter and sort without hesitating, and understand why `WHERE` can't see a column alias.
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.
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 solutionAND, OR, NOT, IN, BETWEEN, LIKE, ILIKE, and operator precedence (AND binds tighter than OR)IS NULL, IS NOT NULL, IS DISTINCT FROMAS, and quoting rules for identifiers with spaces or reserved wordsORDER BY with NULLS FIRST / NULLS LAST, and multi-column sorts with mixed directionsSQL 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.
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.
LeetCode:
DataLemur:
PGExercises: the whole Basic section, it's about 10 questions and takes under an hour.
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.
WHERE col != 'x' query and ask why a row is missing. They want to hear "nulls".LIMIT without ORDER BY returns. Correct answer: undefined, non-deterministic, and dangerous in a pipeline because the result changes between runs.WHERE and HAVING on day 1 to see whether you know the execution order.SELECT *. On a columnar warehouse that's a real cost, since the engine only reads the columns you name.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.NOT IN (subquery with NULL) returns nothing.