INNER, LEFT, RIGHT, and FULL joinsPlannedSelf joins and many-to-many dataPlannedSubqueries and correlated subqueriesPlannedUNION, INTERSECT, and EXCEPTPlanned
Designing reliable schemas
CREATE TABLE and schema designPlannedConstraints and data integrityPlannedNormalization and intentional denormalizationPlannedViews and materialized viewsPlanned
Writing and protecting data
INSERT, UPDATE, and DELETEPlannedUpserts and conflict handlingPlannedTransactions and savepointsPlannedIsolation, locks, and concurrencyPlanned
Performance and administration
Indexes and access pathsPlannedEXPLAIN and query plansPlannedQuery tuning patternsPlannedBackups, restores, and migrationsPlanned
Security and production workflow
Users, roles, and least privilegePlannedPreventing SQL injectionPlannedStored procedures, functions, and triggersPlannedProduction data projectPlanned
Reports, aggregation, and analytics · Lesson 12 135 min
Common Table Expressions
A common table expression, or CTE, names a query result for use by the statement that follows. WITH lets you express a report as reviewable stages; WITH RECURSIVE lets a stage produce more rows from rows it already found.
Name each transformation
SourceChoose the rows the report means to include.
StageGive an intermediate result a useful name.
InspectRun a stage alone to check its rows and columns.
FinishApply the final filter, projection, and order.
SOURCE8 orderspaid · pending · refunded
Only five orders are paid.
CTEpaid_orders5 rows
A named result holds the filtered population for this statement.
FINAL12,000 centsSUM(amount_cents)
The outer SELECT reads the named result.
WITH names a result inside one statement
The paid_orders CTE contains five paid rows. The following SELECT reads it like a table and returns a count of five and a 12,000-cent sum. Its name exists only for this statement; it does not create a permanent table or view.
SQL BROWSER RUNNER
Build a paid-orders CTE
Name a filtered result and summarize it in the outer SELECT.
Edit the query, predict the rows it will return, then run it.
Inspect an intermediate step before trusting the report
Temporarily select from the CTE itself to verify its grain and population. This version shows five paid order IDs. That check is especially useful before adding aggregates, rankings, or a second CTE.
Edit the query, predict the rows it will return, then run it.
Chain CTEs for a multi-step report
Later CTEs can read earlier ones. Here paid_orders filters first, customer_totals groups second, and the final SELECT keeps customers with at least 3,000 paid cents. Customers 10 and 12 remain. Keep each stage focused on one question.
Edit the query, predict the rows it will return, then run it.
An optional column list makes the shape explicit
WITH customer_totals(customer_id, paid_cents) AS (...) gives the CTE output columns explicit names. The number of listed names must match the number of selected expressions. This is helpful when an expression would otherwise have a database-generated label.
SQL BROWSER RUNNER
Name a CTE output shape
Declare two result-column names beside the CTE name.
Edit the query, predict the rows it will return, then run it.
Filter a window result in an outer stage
The previous lesson used ROW_NUMBER() to rank rows. A base WHERE cannot filter that computed position in the same SELECT. Put the window expression in a CTE, then apply WHERE position = 1 outside it. The result is the largest paid order per customer, with order ID breaking ties.
SQL BROWSER RUNNER
Keep each customer's top paid order
Compute row numbers first, then filter them outside the CTE.
CREATETABLE orders (
order_id INTEGERPRIMARYKEY,
customer_id INTEGERNOTNULL,statusTEXTNOTNULL,
amount_cents INTEGER,
placed_on TEXTNOTNULL);INSERTINTO orders (order_id, customer_id,status, amount_cents, placed_on)VALUES(1,10,'paid',1000,'2026-09-01'),(2,11,'paid',2500,'2026-09-02'),(3,10,'pending',NULL,'2026-09-03'),(4,12,'paid',4000,'2026-09-04'),(5,11,'refunded',0,'2026-09-05'),(6,13,'paid',1500,'2026-09-06'),(7,10,'paid',3000,'2026-09-07'),(8,13,'pending',NULL,'2026-09-08');WITH ranked_paid AS(SELECT order_id, customer_id, amount_cents,
ROW_NUMBER()OVER(PARTITIONBY customer_id ORDERBY amount_cents DESC, order_id
)AS position
FROM orders WHEREstatus='paid')SELECT customer_id, order_id, amount_cents
FROM ranked_paid WHERE position =1ORDERBY customer_id;
QUERY OUTPUT
Edit the query, predict the rows it will return, then run it.
Reuse a named result where it clarifies the metric
The final SELECT reads customer_totals as its row source and in a scalar subquery for the average customer total. That average is 3,000 cents across four customers—not the 2,400-cent average order amount. The metric changes because its unit is a customer rather than an order. Whether a database recomputes or materializes a CTE is engine- and plan-dependent; a CTE is not automatically a performance optimization.
SQL BROWSER RUNNER
Compare each customer with the customer average
Reference one named stage from the outer query and a subquery.
Edit the query, predict the rows it will return, then run it.
Recursive CTEs need a seed and a stopping rule
WITH RECURSIVE starts with an anchor query, then repeatedly runs a recursive member against the rows found so far. UNION ALL appends each wave. In the number example, one is the anchor and n < 5 prevents expansion after five. The outer ORDER BY controls display order.
SQL BROWSER RUNNER
Generate a bounded sequence
Observe the anchor, recursive step, and termination condition.
WITH RECURSIVE numbers(n)AS(SELECT1UNIONALLSELECT n +1FROM numbers WHERE n <5)SELECT n FROM numbers ORDERBY n;
QUERY OUTPUT
Edit the query, predict the rows it will return, then run it.
Traverse a reporting hierarchy
The hierarchy query anchors at Bilal, employee 2. Each recursive step finds employees whose manager_id matches a previously found employee. It returns Bilal at depth zero, Dina and Elias at depth one, and Farah at depth two. The depth guard is a safety bound, not cycle detection. Real mutable graphs need an explicit cycle-prevention strategy; a bad cycle could otherwise revisit nodes.
SQL BROWSER RUNNER
Walk a manager's team
Traverse a six-person employee hierarchy with a bounded depth.
CREATETABLE employees (
employee_id INTEGERPRIMARYKEY,
employee_name TEXTNOTNULL,
manager_id INTEGERREFERENCES employees(employee_id));INSERTINTO employees (employee_id, employee_name, manager_id)VALUES(1,'Amina',NULL),(2,'Bilal',1),(3,'Celia',1),(4,'Dina',2),(5,'Elias',2),(6,'Farah',4);WITH RECURSIVE team(employee_id, employee_name, manager_id, depth)AS(SELECT employee_id, employee_name, manager_id,0FROM employees WHERE employee_id =2UNIONALLSELECT e.employee_id, e.employee_name, e.manager_id, team.depth +1FROM employees AS e
JOIN team ON e.manager_id = team.employee_id
WHERE team.depth <10)SELECT employee_id, employee_name, depth
FROM team ORDERBY depth, employee_id;
QUERY OUTPUT
Edit the query, predict the rows it will return, then run it.
Common CTE mistakes
WITH means saved table
Wrong lifetime
A CTE name is scoped to one statement. Use a table or view for durable reuse.
CTE means faster
Unproven plan
Execution and materialization vary by engine and optimizer. Measure performance.
Recursive step without bound
Runaway expansion
Give recursion a terminating condition and guard against cycles in graph data.
Implicit row order
Unstable presentation
Use an outer ORDER BY when the final result needs an order.
Independent lab: top paid order and customer total
Build a report with each paying customer's highest-value order and total paid cents. The starter uses paid_orders to establish the population and ranked to compute position and customer total. It returns four customers: 10 and 12 tie on 4,000-cent totals, followed by 11 at 2,500 and 13 at 1,500. The outer ORDER BY breaks the total tie by customer ID.
SQL BROWSER RUNNER
Audit a staged customer report
Filter paid rows, calculate windows, then publish one result per customer.
CREATETABLE orders (
order_id INTEGERPRIMARYKEY,
customer_id INTEGERNOTNULL,statusTEXTNOTNULL,
amount_cents INTEGER,
placed_on TEXTNOTNULL);INSERTINTO orders (order_id, customer_id,status, amount_cents, placed_on)VALUES(1,10,'paid',1000,'2026-09-01'),(2,11,'paid',2500,'2026-09-02'),(3,10,'pending',NULL,'2026-09-03'),(4,12,'paid',4000,'2026-09-04'),(5,11,'refunded',0,'2026-09-05'),(6,13,'paid',1500,'2026-09-06'),(7,10,'paid',3000,'2026-09-07'),(8,13,'pending',NULL,'2026-09-08');-- Highest-value paid order per customer, with each customer's total.WITH paid_orders AS(SELECT order_id, customer_id, amount_cents
FROM orders WHEREstatus='paid'), ranked AS(SELECT order_id, customer_id, amount_cents,SUM(amount_cents)OVER(PARTITIONBY customer_id)AS customer_cents,
ROW_NUMBER()OVER(PARTITIONBY customer_id ORDERBY amount_cents DESC, order_id
)AS position
FROM paid_orders
)SELECT customer_id, order_id, amount_cents, customer_cents
FROM ranked WHERE position =1ORDERBY customer_cents DESC, customer_id;
QUERY OUTPUT
Edit the query, predict the rows it will return, then run it.
Lesson review
CTEs give a complex statement named, inspectable stages. Use ordinary CTEs to make transformations easier to reason about, and recursive CTEs when rows must lead to more rows. Check scope, grain, termination, cycles, and final ordering instead of assuming a CTE handles them automatically.
I can write and inspect a single named result.
I can chain CTEs without losing the intended population.
I can filter a computed window value outside its CTE.
I can identify an anchor, recursive member, and stop condition.
I know CTE readability does not guarantee faster execution.
KNOWLEDGE CHECK
Check CTE reasoning
Answer all ten questions, then revisit any example whose scope or recursion surprised you.