4.4.10 · D4Databases

Exercises — CTEs (WITH clause) — recursive CTEs

2,884 words13 min readBack to topic

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 .
Figure — CTEs (WITH clause) — recursive CTEs

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 references t, so yes it loops.
  • It does terminate because each round subtracts 2 and the guard x > 2 eventually 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, 3

Now 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:

  • A
  • A→B
  • A→B→C (from C, only edge is C→A, but A is already in the path → blocked)
  • A→D
  • A→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