Exercises — CTEs (WITH clause) — recursive CTEs
Quick reminder of the vocabulary we lean on (all built in the parent):
- anchor = the base query that runs once, producing .
- recursive member = the query that references the CTE itself; on round it reads only (last round's new rows), producing .
- termination: the loop stops at the first with (empty).
- final result .

Level 1 — Recognition
Exercise 1.1
Look at this CTE. Which line is the anchor and which is the recursive member? Does it even loop?
WITH RECURSIVE t AS (
SELECT 10 AS x
UNION ALL
SELECT x - 2 FROM t WHERE x > 2
)
SELECT x FROM t;Recall Solution 1.1
- Anchor:
SELECT 10 AS x— runs once, seeds . - Recursive member:
SELECT x - 2 FROM t WHERE x > 2— it referencest, so yes it loops. - It does terminate because each round subtracts 2 and the guard
x > 2eventually fails.
Round trace — remember the recursive member only sees last round's rows:
| Round | passes x > 2? |
|
|---|---|---|
{10} |
yes → emit 8 | |
{8} |
yes → emit 6 | |
{6} |
yes → emit 4 | |
{4} |
yes → emit 2 | |
{2} |
no (2 > 2 false) → emit nothing | |
{} |
stop |
Result: 10, 8, 6, 4, 2.
Exercise 1.2
True or false: "A recursive CTE always needs UNION ALL; UNION is illegal here."
Recall Solution 1.2
False. Both are legal. UNION de-duplicates each round (set semantics) — sometimes used as a free cycle guard. UNION ALL keeps everything and is faster. See UNION vs UNION ALL.
Level 2 — Application
Exercise 2.1
Write a recursive CTE that produces the even numbers from 2 to 12 inclusive, and give the round-by-round trace.
Recall Solution 2.1
WITH RECURSIVE evens AS (
SELECT 2 AS n
UNION ALL
SELECT n + 2 FROM evens WHERE n < 12
)
SELECT n FROM evens;| Round | guard n < 12 |
|
|---|---|---|
{2} |
yes → 4 | |
{4} |
yes → 6 | |
{6} |
yes → 8 | |
{8} |
yes → 10 | |
{10} |
yes → 12 | |
{12} |
no (12 < 12 false) | |
{} |
stop |
Result: 2, 4, 6, 8, 10, 12 — six rows summing to 42.
Exercise 2.2
Table emp(id, name, manager_id):
| id | name | manager_id |
|---|---|---|
| 1 | Ada | NULL |
| 2 | Bo | 1 |
| 3 | Cy | 1 |
| 4 | Di | 2 |
| 5 | Ez | 4 |
Using the org-chart pattern from the parent note, list every employee with their level (lvl), where Ada (CEO) is level 1.
Recall Solution 2.2
WITH RECURSIVE chain AS (
SELECT id, name, manager_id, 1 AS lvl
FROM emp WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, e.manager_id, c.lvl + 1
FROM emp e JOIN chain c ON e.manager_id = c.id
)
SELECT id, name, lvl FROM chain ORDER BY lvl, id;Each round adds people whose manager was found last round (see SQL Joins for the join mechanics):
| Round | new rows (id, lvl) |
|---|---|
| (anchor) | Ada (1, lvl 1) |
| Bo (2, lvl 2), Cy (3, lvl 2) | |
| Di (4, lvl 3) | |
| Ez (5, lvl 4) | |
| none → stop |
Result:
| id | name | lvl |
|---|---|---|
| 1 | Ada | 1 |
| 2 | Bo | 2 |
| 3 | Cy | 2 |
| 4 | Di | 3 |
| 5 | Ez | 4 |
Deepest level = 4. Total employees returned = 5.
Level 3 — Analysis
Exercise 3.1
The following query is meant to count 1..3 but hangs forever. Diagnose exactly why using a trace, then fix it.
WITH RECURSIVE nums AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM nums -- no WHERE!
)
SELECT n FROM nums LIMIT 3;Recall Solution 3.1
Diagnosis: the recursive member has no stop guard, so never empties:
— infinitely. The outer LIMIT 3 cannot save you: the CTE must finish building before the outer SELECT runs (in standard engines), so it hangs during construction.
Fix — put the bound inside the recursive member so a round eventually yields zero rows:
WITH RECURSIVE nums AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM nums WHERE n < 3
)
SELECT n FROM nums; -- 1, 2, 3Now because 3 < 3 is false → termination. Result rows: 1, 2, 3 (three rows, sum 6).
Exercise 3.2
Graph edges(src, dst): A→B, B→C, C→A (a cycle), plus B→D. This query loops forever:
WITH RECURSIVE reach AS (
SELECT 'A' AS node
UNION ALL
SELECT e.dst FROM edges e JOIN reach r ON e.src = r.node
)
SELECT DISTINCT node FROM reach;Explain the loop with two rounds of trace, then give two independent fixes.
Recall Solution 3.2
Why it loops: with UNION ALL and a 3-cycle A→B→C→A, the working table keeps regenerating nodes:
(from ), — never empty (see Tree and Graph Data Structures).
Fix 1 — UNION (set semantics): de-duplicates each round. Once have all appeared, a new round adds nothing new → .
WITH RECURSIVE reach AS (
SELECT 'A' AS node
UNION -- dedupe = free cycle guard
SELECT e.dst FROM edges e JOIN reach r ON e.src = r.node
)
SELECT node FROM reach;Fix 2 — carry a visited path and refuse revisits:
WITH RECURSIVE reach AS (
SELECT 'A' AS node, ARRAY['A'] AS path
UNION ALL
SELECT e.dst, r.path || e.dst
FROM edges e JOIN reach r ON e.src = r.node
WHERE e.dst <> ALL(r.path)
)
SELECT DISTINCT node FROM reach;Both return the reachable set {A, B, C, D} — 4 distinct nodes.
Level 4 — Synthesis
Exercise 4.1
edges(src, dst): A→B, A→C, B→D, C→D, D→E. Find the shortest number of hops from A to E. Write a recursive CTE that carries a depth counter, and give the answer.
Recall Solution 4.1
Carry depth, start at 0 for A, add 1 each hop; then take the minimum depth where node = 'E'.
WITH RECURSIVE walk AS (
SELECT 'A' AS node, 0 AS depth, ARRAY['A'] AS path
UNION ALL
SELECT e.dst, w.depth + 1, w.path || e.dst
FROM edges e JOIN walk w ON e.src = w.node
WHERE e.dst <> ALL(w.path) -- cycle guard (safe even if none here)
)
SELECT MIN(depth) AS shortest FROM walk WHERE node = 'E';Trace of depths reaching each node:
| Round | new rows (node @ depth) |
|---|---|
| A @ 0 | |
| B @ 1, C @ 1 | |
| D @ 2 (via B), D @ 2 (via C) | |
| E @ 3 | |
| none → stop |
E first appears at depth 3, and there is no shorter route → shortest = 3 hops (A→B→D→E or A→C→D→E).
Exercise 4.2
Generate the running factorials as (n, n!) pairs using one recursive CTE (no built-in loop, no procedural code).
Recall Solution 4.2
Keep two carried values: the current n and the accumulated product f. Multiply as you climb.
WITH RECURSIVE fact AS (
SELECT 1 AS n, 1 AS f -- 1! = 1
UNION ALL
SELECT n + 1, f * (n + 1)
FROM fact
WHERE n < 5
)
SELECT n, f FROM fact ORDER BY n;| Round | (n, f) |
|---|---|
| (1, 1) | |
| (2, 2) | |
| (3, 6) | |
| (4, 24) | |
| (5, 120) | |
n<5 false → stop |
Result: (1,1) (2,2) (3,6) (4,24) (5,120). The last factorial 5! = 120.
Level 5 — Mastery
Exercise 5.1
Table emp(id, name, manager_id) — same data as 2.2. Build one recursive CTE that returns, for every employee, the full chain of command as a text path from the CEO down to them, e.g. Ada > Bo > Di. Give the output.
Recall Solution 5.1
Carry a growing string. Anchor = CEO with just their name; recursive member appends ' > ' || name.
WITH RECURSIVE chain AS (
SELECT id, name, name::text AS path_str, 1 AS lvl
FROM emp WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, c.path_str || ' > ' || e.name, c.lvl + 1
FROM emp e JOIN chain c ON e.manager_id = c.id
)
SELECT id, name, path_str, lvl FROM chain ORDER BY lvl, id;| id | name | path_str | lvl |
|---|---|---|---|
| 1 | Ada | Ada |
1 |
| 2 | Bo | Ada > Bo |
2 |
| 3 | Cy | Ada > Cy |
2 |
| 4 | Di | Ada > Bo > Di |
3 |
| 5 | Ez | Ada > Bo > Di > Ez |
4 |
Ez's path has 4 names; the longest chain length is 4.
Exercise 5.2 (capstone)
Graph edges(src, dst): A→B, B→C, C→A (a cycle) and A→D, D→E. Count how many distinct simple paths (no repeated node) start at A, including the trivial length-1 path A itself. Design the CTE and count.
Recall Solution 5.2
A simple path never revisits a node — so we carry the path array and forbid revisits with <> ALL. Every row of the CTE is one distinct simple path.
WITH RECURSIVE paths AS (
SELECT 'A' AS node, ARRAY['A'] AS path
UNION ALL
SELECT e.dst, p.path || e.dst
FROM edges e JOIN paths p ON e.src = p.node
WHERE e.dst <> ALL(p.path) -- keep it simple (no revisits)
)
SELECT COUNT(*) AS num_paths FROM paths;Enumerate the simple paths from A:
AA→BA→B→C(fromC, only edge isC→A, butAis already in the path → blocked)A→DA→D→E
The cycle A→B→C→A is cut exactly when C→A would revisit A. Distinct simple paths = 5.
trace (each entry is a path array):
| Round | new paths |
|---|---|
[A] |
|
[A,B], [A,D] |
|
[A,B,C], [A,D,E] |
|
none (C→A blocked, E has no out-edge) → stop |
Total rows = .
Recall One-look review of the whole ladder
L1 recognise parts ::: anchor vs recursive member; both UNION/UNION ALL legal
L2 apply patterns ::: even sequence; org-chart levels (join e.manager_id = c.id)
L3 diagnose failures ::: missing guard hangs; cycles need UNION or visited-set; outer LIMIT can't save it
L4 synthesise carriers ::: depth counter for shortest hops; accumulator for factorial
L5 invent under constraints ::: text path string; count simple paths with <> ALL
Connections
- Parent: recursive CTEs — the theory these exercises drill
- Common Table Expressions (WITH clause) — the non-recursive base concept
- SQL Joins — every recursive member here joins the CTE back to a base table
- UNION vs UNION ALL — the L3 cycle-guard decision hinges on this
- Tree and Graph Data Structures — org charts (trees) and reachability (graphs)
- Window Functions — the sibling advanced-read tool
- Mathematical Induction — anchor = base case, recursive member = inductive step