Row-level calculations across a related set of rows — without collapsing the result
Window Functions are the single biggest leap in SQL capability. They let you compute rankings, running totals, moving averages, and comparisons to previous rows — all while keeping every individual row in the output. The same calculations without window functions require self-joins or correlated subqueries that are harder to read and often much slower.
A window function adds a column to each row based on a calculation over a "window" of related rows — without collapsing rows the way GROUP BY does. The OVER() clause defines the window: PARTITION BY resets the window per group (like GROUP BY but without collapsing), and ORDER BY controls which rows are "before" the current one for running calculations.
Think of the window as a sliding frame that the database drags over each row in turn, computing the function over the visible rows in that frame.
LAG and LEAD are the idiomatic way to compare a row to its predecessor or successor — no self-join needed. The ROWS BETWEEN frame clause controls exactly which rows are in the window for running calculations.
Building complex queries in readable layers — and when to use each approach
Real-world SQL questions require multiple steps — filter, aggregate, rank, then filter again. Subqueries and CTEs let you name intermediate results and build queries in layers. CTEs in particular transform deeply nested unreadable SQL into a step-by-step structure that reads almost like code comments, and are preferred over subquery nesting for readability.
A subquery is a query embedded inside another query — either in FROM (derived table), WHERE (scalar or list subquery), or SELECT (correlated subquery). A CTE (Common Table Expression) is like giving a subquery a name at the top of the query. They're often equivalent in what they compute, but CTEs are almost always more readable.
Think of CTEs as named intermediate steps you build up, left to right, and then compose in the final SELECT — like variable assignment in a programming language.
Correlated subqueries run once per outer row — they can be very slow on large tables. Rewrite them as a join or CTE with a pre-computed aggregate when performance matters.
Recursive CTEs are the standard way to traverse hierarchical data — org charts, category trees, bill of materials. The anchor member (base case) runs once; the recursive member repeats until no new rows are added.
INSERT, UPDATE, DELETE, and UPSERT — writing data safely
Writes are irreversible unless you wrap them in a transaction. An UPDATE without a WHERE clause updates every row in the table — a mistake that has ended careers. Understanding DML (Data Manipulation Language) and how transactions protect you from these mistakes is essential before touching production data.
DML statements (INSERT, UPDATE, DELETE) write data; DDL statements (CREATE, ALTER, DROP) change the schema. Both should be wrapped in a transaction when making changes that need to be atomic — either all succeed or all roll back. Always preview writes with a SELECT first using the same WHERE clause you plan to use.
The pattern of wrapping UPDATE/DELETE in BEGIN ... COMMIT and keeping a ROLLBACK ready is standard practice for any manual data change in production. Run the equivalent SELECT first to confirm row count.
UPSERT is idempotent — safe to run multiple times, which makes it ideal for data pipelines where records may be re-ingested.
Table design, keys, constraints, and how indexes make or break query speed
A well-designed schema prevents entire classes of bugs — referential integrity violations, duplicate records, invalid states. Indexes are the primary lever for query performance: a missing index on a join column can turn a 10ms query into a 10-second full table scan. Understanding when and why indexes help (and when they hurt) is essential for both schema design and query tuning.
A B-tree index is like a book's index — it sorts values so the database can jump to a row in O(log n) rather than scanning the whole table O(n). The cost: every write (INSERT, UPDATE, DELETE) must also update the index. Indexes make reads faster and writes slightly slower — the trade-off is almost always worth it for columns used frequently in WHERE, JOIN, and ORDER BY.
Normalization reduces redundancy by splitting data into separate tables linked by foreign keys. The trade-off: fewer writes to update in one place, but more joins to read data back together.
Reading execution plans, finding bottlenecks, and making slow queries fast
Writing correct SQL is the baseline. Writing SQL that stays fast at scale is what separates strong engineers. A query that works in dev on 10k rows can become a minute-long scan in production on 100M rows. EXPLAIN tells you exactly what the database is doing — which indexes it's using, how many rows it's scanning, where the cost is. Without it, optimisation is guesswork.
The query optimizer reads your SQL, generates several possible execution plans, estimates the cost of each, and picks the cheapest one based on table statistics. EXPLAIN shows you that chosen plan. EXPLAIN ANALYZE actually runs the query and shows real timing — the gap between estimated and actual rows is where optimizer mistakes live.
The two things to look for: Seq Scan on a large table (bad — full scan, missing index) and Nested Loop on large outer sets (can be bad — O(n) index lookups). Index Scan and Hash Join are usually what you want.