1. Subqueries in WHERE
-- customers whose average order total is above the overall average
SELECT name FROM customers
WHERE id IN (
SELECT customer_id FROM orders
GROUP BY customer_id
HAVING AVG(total) > (SELECT AVG(total) FROM orders)
);
The inner query runs first (conceptually), producing a set of values the outer
query's WHERE ... IN (...) then filters against. This particular
query nests two levels deep — an aggregate subquery for the overall average, inside
a grouped subquery for per-customer averages, inside the outer filter — which is
exactly the kind of query that gets hard to read past two or three levels, and
exactly what CTEs (next) are for.
2. Subqueries in FROM & CTEs
SELECT customer_totals.name, customer_totals.total_spent
FROM (
SELECT customers.name, SUM(orders.total) AS total_spent
FROM customers
JOIN orders ON customers.id = orders.customer_id
GROUP BY customers.name
) AS customer_totals
WHERE customer_totals.total_spent > 100;
WITH customer_totals AS (
SELECT customers.name, SUM(orders.total) AS total_spent
FROM customers
JOIN orders ON customers.id = orders.customer_id
GROUP BY customers.name
)
SELECT name, total_spent
FROM customer_totals
WHERE total_spent > 100;
A CTE (Common Table Expression), introduced with WITH,
is functionally identical to a subquery in FROM — the difference is
purely readability: it's named, appears before the main query instead of nested
inside it, and multiple CTEs can chain together, each referencing the ones before
it, which reads top-to-bottom instead of inside-out.
3. Window Functions: ROW_NUMBER & RANK
A window function computes a value across a set of related rows — like an
aggregate — but without collapsing them into one row per group the way
GROUP BY does. Every original row stays in the result, with the
computed value attached.
SELECT
customer_id,
id AS order_id,
total,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY id DESC) AS order_rank
FROM orders;
PARTITION BY is GROUP BY's window-function equivalent —
it splits rows into groups for the function to operate within — but unlike
GROUP BY, every row from every partition still appears in the output.
RANK() works the same way as ROW_NUMBER() but assigns the
same rank to tied rows (and skips the next number), where ROW_NUMBER()
always assigns a strictly increasing, unique number regardless of ties.
4. Running Totals & Moving Averages
SELECT
created_at,
total,
SUM(total) OVER (ORDER BY created_at) AS running_total
FROM orders
ORDER BY created_at;
With no PARTITION BY, the window spans the whole result set — each
row's running_total is the sum of every total up to and
including that row's position in the ORDER BY. Aggregate functions
(SUM, AVG, COUNT) become window functions
simply by adding OVER (...) — the same functions from Week 2, used
differently.
5. Hands-on Exercise
Build a leaderboard and a revenue trend report
Apply CTEs and window functions to the orders data from earlier weeks.
Requirements:
- A CTE computing total revenue per customer, then a main query selecting only customers above a chosen revenue threshold — rewriting an equivalent nested subquery version first, then the CTE version, and comparing readability in a comment.
- A query using
RANK() OVER (ORDER BY total_spent DESC)to produce a customer leaderboard by total spend. - A query using
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY created_at)to number each customer's orders chronologically (their 1st order, 2nd order, and so on). - A query computing a running total of daily revenue using
SUM(...) OVER (ORDER BY created_at). - A query using
ROW_NUMBER()with a CTE to find each customer's single most recent order — a genuinely common real-world pattern.
If a query using ROW_NUMBER() to find "each customer's most recent order" returns more than one row per customer, remember ROW_NUMBER() itself doesn't filter anything — it only numbers rows. Wrap it in a CTE, then filter the outer query for WHERE row_num = 1; you cannot filter on a window function's result directly in the same query's WHERE clause, since WHERE runs before window functions are computed.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
What is the actual difference between a subquery nested in FROM and an equivalent CTE defined with WITH?
What is the actual difference between a subquery nested in FROM and an equivalent CTE defined with WITH?
None, functionally — a CTE is evaluated the same way and produces the same result as an equivalent FROM subquery. The difference is entirely about readability and structure: a CTE is named and defined before the main query rather than nested inside it, and multiple CTEs can chain together in a way that reads top-to-bottom instead of requiring the reader to parse inside-out.
Q2
How does PARTITION BY in a window function differ from GROUP BY, given both split rows into groups?
How does PARTITION BY in a window function differ from GROUP BY, given both split rows into groups?
GROUP BY collapses each group into a single summary row, discarding the individual rows that made it up. PARTITION BY defines groups for a window function to compute within, but every original row remains in the output — the computed value (a rank, a running total) is simply attached to each row rather than replacing the rows with one aggregate per group.
Q3
Why can RANK() assign the number 1 to two different rows, followed immediately by 3 (skipping 2), while ROW_NUMBER() never does this?
Why can RANK() assign the number 1 to two different rows, followed immediately by 3 (skipping 2), while ROW_NUMBER() never does this?
RANK() assigns the same rank to rows that tie on the ORDER BY criteria, and then skips ahead by however many rows tied — two rows tied for first both get 1, and the next distinct row gets 3, reflecting that two rows occupied positions 1 and 2. ROW_NUMBER() ignores ties entirely and always assigns a strictly sequential, unique number to every row regardless of whether values are equal.
Q4
Why can't a window function's result be filtered directly in the same query's WHERE clause?
Why can't a window function's result be filtered directly in the same query's WHERE clause?
WHERE is evaluated before window functions are computed, as part of SQL's logical execution order — at the point WHERE runs, no window function result exists yet to filter on. Filtering on a window function's output requires wrapping it in a subquery or CTE first, computing the window function there, and then filtering in an outer query against that already-computed column.