Skip to main content

Recursive CTE

Editorial

Introduction

Recursive CTEs are the most powerful type of CTE. They can reference themselves, enabling you to traverse hierarchies, generate sequences, and solve problems that would otherwise require procedural code.

In this tutorial, you will learn:

  • What makes a CTE recursive
  • The structure of recursive CTEs (anchor + recursive member)
  • How recursive CTEs execute
  • Common use cases and patterns
  • Preventing infinite loops

What is a Recursive CTE?

Definition

A recursive CTE is a CTE that references itself in its own definition. This self-reference allows the query to iterate, building results step by step.

Key Difference from Regular CTEs

Regular CTERecursive CTE
Cannot reference itselfReferences itself
Executes onceExecutes repeatedly until done
Static resultBuilds result iteratively
No special keywordUses RECURSIVE keyword

When You Need Recursion

  • Hierarchical data: Organization charts, category trees, folder structures
  • Network/graph data: Friend connections, route finding
  • Sequences: Generate numbers, dates, series
  • Bill of materials: Product components and sub-components
  • Path finding: All routes between two points

Visualization

Diagram showing recursive CTE structure: anchor member produces base rows, recursive member references itself to add rows iteratively until termination condition

Recursive CTE -- Iterative Self-Referencing Queries

Basic Structure

Recursive CTE Anatomy

WITH RECURSIVE cte_name AS (
    -- ANCHOR MEMBER: Starting point (non-recursive)
    SELECT initial_columns
    FROM base_table
    WHERE starting_condition
    
    UNION ALL
    
    -- RECURSIVE MEMBER: Self-referencing part
    SELECT derived_columns
    FROM cte_name            -- References itself!
    JOIN base_table ON ...   -- Usually joins to get next level
    WHERE termination_condition
)
SELECT * FROM cte_name;

Visual Breakdown

Generated visual representation from text diagram for Basic Structure.

Important Notes

  • RECURSIVE keyword: Required in most databases (PostgreSQL, SQLite). SQL Server doesn't require it.
  • UNION ALL: Must use UNION ALL between anchor and recursive member
  • Termination: Recursive member must eventually return no rows, or query runs forever

How Recursive CTEs Execute

Execution Flow

Generated visual representation from text diagram for How Recursive CTEs Execute.

Simple Example: Counting to 5

WITH RECURSIVE counter AS (
    -- Anchor: Start at 1
    SELECT 1 AS n
    
    UNION ALL
    
    -- Recursive: Add 1 each iteration
    SELECT n + 1
    FROM counter
    WHERE n < 5  -- Stop condition
)
SELECT n FROM counter;

Execution trace:

  • Iteration 0 (anchor): n=1 → work table: [1]
  • Iteration 1: 1+1=2, 2<5 ✓ → work table: [2]
  • Iteration 2: 2+1=3, 3<5 ✓ → work table: [3]
  • Iteration 3: 3+1=4, 4<5 ✓ → work table: [4]
  • Iteration 4: 4+1=5, 5<5 ✗ → work table: [] → STOP

Result: 1, 2, 3, 4, 5

Common Patterns

Pattern 1: Number Sequence Generation

WITH RECURSIVE numbers AS (
    SELECT 1 AS n
    UNION ALL
    SELECT n + 1 FROM numbers WHERE n < 100
)
SELECT n FROM numbers;
-- Produces: 1, 2, 3, ..., 100

Pattern 2: Date Range Generation

WITH RECURSIVE dates AS (
    SELECT DATE('2023-01-01') AS dt
    UNION ALL
    SELECT DATE(dt, '+1 day')
    FROM dates
    WHERE dt < DATE('2023-01-31')
)
SELECT dt FROM dates;
-- Produces all dates in January 2023

Pattern 3: Hierarchy Traversal

-- Employee hierarchy (manager -> reports)
WITH RECURSIVE org_chart AS (
    -- Anchor: Start with CEO (no manager)
    SELECT id, name, manager_id, 0 AS level
    FROM employees
    WHERE manager_id IS NULL
    
    UNION ALL
    
    -- Recursive: Find employees who report to current level
    SELECT e.id, e.name, e.manager_id, o.level + 1
    FROM employees e
    JOIN org_chart o ON e.manager_id = o.id
)
SELECT * FROM org_chart ORDER BY level;

Pattern 4: Path Building

-- Build path from root to each node
WITH RECURSIVE category_path AS (
    SELECT id, name, parent_id, name AS path
    FROM categories
    WHERE parent_id IS NULL
    
    UNION ALL
    
    SELECT c.id, c.name, c.parent_id, 
           cp.path || ' > ' || c.name
    FROM categories c
    JOIN category_path cp ON c.parent_id = cp.id
)
SELECT * FROM category_path;
-- Result: "Electronics > Computers > Laptops"

Preventing Infinite Loops

The Danger of Infinite Recursion

If the recursive member never stops producing rows, the query runs forever (or until the database stops it).

❌ Infinite loop example:

WITH RECURSIVE infinite AS (
    SELECT 1 AS n
    UNION ALL
    SELECT n + 1 FROM infinite  -- No WHERE clause = never stops!
)
SELECT * FROM infinite;

Prevention Strategies

1. WHERE clause with termination condition:

WITH RECURSIVE cte AS (
    SELECT 1 AS n
    UNION ALL
    SELECT n + 1 FROM cte
    WHERE n < 100  -- ✅ Stops at 100
)

2. LIMIT in main query:

WITH RECURSIVE cte AS (...)
SELECT * FROM cte
LIMIT 100;  -- ✅ Safety net

3. Track visited nodes (for graphs):

WITH RECURSIVE paths AS (
    SELECT id, name, id AS visited
    FROM nodes WHERE id = 1
    
    UNION ALL
    
    SELECT n.id, n.name, p.visited || ',' || n.id
    FROM nodes n
    JOIN paths p ON n.parent_id = p.id
    WHERE INSTR(p.visited, n.id) = 0  -- ✅ Don't revisit
)

4. Maximum depth limit:

WITH RECURSIVE hierarchy AS (
    SELECT id, name, 0 AS depth
    FROM org WHERE manager_id IS NULL
    
    UNION ALL
    
    SELECT o.id, o.name, h.depth + 1
    FROM org o
    JOIN hierarchy h ON o.manager_id = h.id
    WHERE h.depth < 10  -- ✅ Max 10 levels
)

Practice

Let's practice writing recursive CTEs. Note: Some examples use simulated data since our e-commerce dataset doesn't have natural hierarchies.

Exercise 1: Generate Number Sequence

Generate a sequence of numbers from 1 to 10:

-- Generate numbers 1 to 10
WITH RECURSIVE number_sequence AS (
    -- Anchor: Start at 1
    SELECT 1 AS n
    
    UNION ALL
    
    -- Recursive: Increment until 10
    SELECT n + 1
    FROM number_sequence
    WHERE n < 10
)
SELECT 
    n,
    n * n AS square,
    n * n * n AS cube
FROM number_sequence;

Exercise 2: Generate Multiplication Table

Use recursive CTE to generate a multiplication table:

-- Multiplication table for 7
WITH RECURSIVE mult_table AS (
    SELECT 1 AS multiplier
    
    UNION ALL
    
    SELECT multiplier + 1
    FROM mult_table
    WHERE multiplier < 12
)
SELECT 
    '7 x ' || multiplier || ' = ' || (7 * multiplier) AS equation
FROM mult_table;

Exercise 3: Fibonacci Sequence

Generate Fibonacci numbers using recursion:

-- Fibonacci sequence (first 15 numbers)
WITH RECURSIVE fibonacci AS (
    -- Anchor: First two Fibonacci numbers
    SELECT 
        1 AS n,
        0 AS fib_n,
        1 AS fib_n_plus_1
    
    UNION ALL
    
    -- Recursive: Calculate next Fibonacci
    SELECT 
        n + 1,
        fib_n_plus_1,
        fib_n + fib_n_plus_1
    FROM fibonacci
    WHERE n < 15
)
SELECT 
    n AS position,
    fib_n AS fibonacci_number
FROM fibonacci;

Exercise 4: Simulated Hierarchy

Create and traverse a simulated category hierarchy:

-- Create a simulated category hierarchy and traverse it
WITH 
-- First, create a simulated hierarchy table
category_data AS (
    SELECT 1 AS id, 'All Products' AS name, NULL AS parent_id
    UNION ALL SELECT 2, 'Electronics', 1
    UNION ALL SELECT 3, 'Clothing', 1
    UNION ALL SELECT 4, 'Computers', 2
    UNION ALL SELECT 5, 'Phones', 2
    UNION ALL SELECT 6, 'Men', 3
    UNION ALL SELECT 7, 'Women', 3
    UNION ALL SELECT 8, 'Laptops', 4
    UNION ALL SELECT 9, 'Desktops', 4
    UNION ALL SELECT 10, 'Gaming Laptops', 8
),

-- Now traverse the hierarchy with a recursive CTE
RECURSIVE category_tree AS (
    -- Anchor: Start with root category
    SELECT 
        id, 
        name, 
        parent_id, 
        0 AS level,
        name AS path
    FROM category_data
    WHERE parent_id IS NULL
    
    UNION ALL
    
    -- Recursive: Find children of current level
    SELECT 
        c.id,
        c.name,
        c.parent_id,
        ct.level + 1,
        ct.path || ' > ' || c.name
    FROM category_data c
    JOIN category_tree ct ON c.parent_id = ct.id
)

-- Display the hierarchy with indentation
SELECT 
    SUBSTR('          ', 1, level * 2) || name AS category,
    level,
    path
FROM category_tree
ORDER BY path;

Exercise 5: Running Total Simulation

Use recursive CTE to calculate running totals:

-- Simulate monthly revenue with running totals
WITH RECURSIVE months AS (
    SELECT 
        1 AS month_num,
        'January' AS month_name,
        1000 AS revenue
    UNION ALL SELECT 2, 'February', 1200
    UNION ALL SELECT 3, 'March', 1500
    UNION ALL SELECT 4, 'April', 1100
    UNION ALL SELECT 5, 'May', 1800
    UNION ALL SELECT 6, 'June', 2000
),

running_totals AS (
    -- Anchor: First month
    SELECT 
        month_num,
        month_name,
        revenue,
        revenue AS running_total
    FROM months
    WHERE month_num = 1
    
    UNION ALL
    
    -- Recursive: Add each subsequent month
    SELECT 
        m.month_num,
        m.month_name,
        m.revenue,
        rt.running_total + m.revenue
    FROM months m
    JOIN running_totals rt ON m.month_num = rt.month_num + 1
)

SELECT 
    month_name,
    revenue,
    running_total,
    ROUND(revenue * 100.0 / running_total, 1) || '%' AS pct_of_ytd
FROM running_totals
ORDER BY month_num;

Limitations and Considerations

What You Cannot Do in Recursive Member

Most databases restrict what can appear in the recursive part:

FeatureAllowed in Recursive Member?
Aggregate functions (SUM, COUNT)❌ No
GROUP BY❌ No
DISTINCT❌ No (use UNION instead of UNION ALL)
Subqueries referencing CTE❌ No
LIMIT/TOP❌ No
LEFT/RIGHT OUTER JOIN❌ No (use INNER JOIN)

Performance Considerations

FactorImpact
Recursion depthMore iterations = more work
Rows per iterationLarge working sets slow execution
Join complexitySimple joins are faster
Termination clarityClear conditions prevent runaway queries

SQLite-Specific Notes

  • SQLite uses WITH RECURSIVE syntax
  • Default recursion limit is 1000 iterations
  • Can be changed with PRAGMA recursive_triggers

Summary

Key Takeaways

  1. Structure:

    • WITH RECURSIVE cte AS (...)
    • Anchor member (starting point)
    • UNION ALL
    • Recursive member (self-referencing)
  2. Execution:

    • Anchor executes once
    • Recursive member executes repeatedly
    • Stops when recursive member returns no rows
  3. Use Cases:

    • Number/date sequences
    • Hierarchical data traversal
    • Path building
    • Graph navigation
  4. Safety:

    • Always have a termination condition
    • Use LIMIT as a safety net
    • Track visited nodes for graphs

Quick Reference

WITH RECURSIVE cte_name AS (
    -- Anchor: Starting rows
    SELECT columns FROM table WHERE starting_condition
    
    UNION ALL
    
    -- Recursive: Build on previous iteration
    SELECT columns
    FROM table
    JOIN cte_name ON relationship
    WHERE termination_condition
)
SELECT * FROM cte_name;

What's Next?

Finally, we'll wrap up with the CTE Summary – a comprehensive review of everything you've learned about Common Table Expressions.