What are recursive queries and how do you implement them?

Answer

Recursive queries use Common Table Expressions (CTEs) to reference themselves, enabling traversal of hierarchical or tree-structured data. They’re essential for handling organizational charts, bill of materials, category trees, and graph-like data structures.

Basic Recursive CTE Structure

WITH RECURSIVE cte_name AS (
    -- Anchor member (base case)
    SELECT initial_columns
    FROM base_table
    WHERE base_condition
    
    UNION ALL
    
    -- Recursive member (recursive case)
    SELECT recursive_columns
    FROM base_table
    JOIN cte_name ON join_condition
    WHERE recursive_condition
)
SELECT * FROM cte_name;

Employee Hierarchy Example

Sample Data Structure

CREATE TABLE employees (
    employee_id INT PRIMARY KEY,
    name VARCHAR(100),
    manager_id INT,
    department VARCHAR(50),
    salary DECIMAL(10,2),
    hire_date DATE
);

INSERT INTO employees VALUES
(1, 'John CEO', NULL, 'Executive', 200000, '2020-01-01'),
(2, 'Jane VP Sales', 1, 'Sales', 150000, '2020-02-01'),
(3, 'Mike VP Tech', 1, 'Technology', 160000, '2020-02-15'),
(4, 'Sarah Sales Mgr', 2, 'Sales', 90000, '2020-03-01'),
(5, 'Tom Dev Lead', 3, 'Technology', 120000, '2020-03-15'),
(6, 'Lisa Sales Rep', 4, 'Sales', 60000, '2020-04-01'),
(7, 'Bob Developer', 5, 'Technology', 80000, '2020-04-15'),
(8, 'Alice Developer', 5, 'Technology', 85000, '2020-05-01');

Find All Subordinates

-- Get all employees under a specific manager
WITH RECURSIVE subordinates AS (
    -- Anchor: Start with the target manager
    SELECT 
        employee_id,
        name,
        manager_id,
        department,
        salary,
        0 as level,
        CAST(name AS VARCHAR(1000)) as hierarchy_path
    FROM employees
    WHERE employee_id = 2  -- Jane VP Sales
    
    UNION ALL
    
    -- Recursive: Find direct and indirect reports
    SELECT 
        e.employee_id,
        e.name,
        e.manager_id,
        e.department,
        e.salary,
        s.level + 1,
        CONCAT(s.hierarchy_path, ' -> ', e.name)
    FROM employees e
    JOIN subordinates s ON e.manager_id = s.employee_id
)
SELECT 
    employee_id,
    name,
    department,
    salary,
    level,
    hierarchy_path
FROM subordinates
ORDER BY level, name;

Find Management Chain

-- Get management chain for a specific employee
WITH RECURSIVE management_chain AS (
    -- Anchor: Start with target employee
    SELECT 
        employee_id,
        name,
        manager_id,
        department,
        0 as level_up,
        name as chain
    FROM employees
    WHERE employee_id = 7  -- Bob Developer
    
    UNION ALL
    
    -- Recursive: Go up the management chain
    SELECT 
        e.employee_id,
        e.name,
        e.manager_id,
        e.department,
        mc.level_up + 1,
        CONCAT(e.name, ' -> ', mc.chain)
    FROM employees e
    JOIN management_chain mc ON e.employee_id = mc.manager_id
)
SELECT 
    name,
    department,
    level_up,
    chain as management_chain
FROM management_chain
ORDER BY level_up DESC;

Category Tree Navigation

Product Category Hierarchy

CREATE TABLE categories (
    category_id INT PRIMARY KEY,
    category_name VARCHAR(100),
    parent_category_id INT,
    description TEXT
);

INSERT INTO categories VALUES
(1, 'Electronics', NULL, 'All electronic products'),
(2, 'Computers', 1, 'Computing devices'),
(3, 'Mobile Devices', 1, 'Portable electronic devices'),
(4, 'Laptops', 2, 'Portable computers'),
(5, 'Desktops', 2, 'Desktop computers'),
(6, 'Smartphones', 3, 'Smart mobile phones'),
(7, 'Tablets', 3, 'Tablet devices'),
(8, 'Gaming Laptops', 4, 'High-performance gaming laptops'),
(9, 'Business Laptops', 4, 'Professional laptops');

-- Get all subcategories under Electronics
WITH RECURSIVE category_tree AS (
    -- Anchor: Root category
    SELECT 
        category_id,
        category_name,
        parent_category_id,
        0 as depth,
        category_name as path
    FROM categories
    WHERE category_id = 1  -- Electronics
    
    UNION ALL
    
    -- Recursive: Get all child categories
    SELECT 
        c.category_id,
        c.category_name,
        c.parent_category_id,
        ct.depth + 1,
        CONCAT(ct.path, ' > ', c.category_name)
    FROM categories c
    JOIN category_tree ct ON c.parent_category_id = ct.category_id
)
SELECT 
    category_id,
    REPEAT('  ', depth) || category_name as indented_name,
    depth,
    path
FROM category_tree
ORDER BY path;

Bill of Materials (BOM)

Manufacturing Components

CREATE TABLE parts (
    part_id VARCHAR(20) PRIMARY KEY,
    part_name VARCHAR(100),
    unit_cost DECIMAL(8,2)
);

CREATE TABLE bill_of_materials (
    parent_part_id VARCHAR(20),
    child_part_id VARCHAR(20),
    quantity_required INT,
    PRIMARY KEY (parent_part_id, child_part_id)
);

-- Sample data
INSERT INTO parts VALUES
('BIKE-001', 'Mountain Bike', 500.00),
('FRAME-001', 'Bike Frame', 150.00),
('WHEEL-001', 'Front Wheel', 75.00),
('WHEEL-002', 'Rear Wheel', 80.00),
('TIRE-001', 'Bike Tire', 25.00),
('RIM-001', 'Wheel Rim', 30.00),
('SPOKE-001', 'Wheel Spoke', 2.00);

INSERT INTO bill_of_materials VALUES
('BIKE-001', 'FRAME-001', 1),
('BIKE-001', 'WHEEL-001', 1),
('BIKE-001', 'WHEEL-002', 1),
('WHEEL-001', 'TIRE-001', 1),
('WHEEL-001', 'RIM-001', 1),
('WHEEL-001', 'SPOKE-001', 32),
('WHEEL-002', 'TIRE-001', 1),
('WHEEL-002', 'RIM-001', 1),
('WHEEL-002', 'SPOKE-001', 32);

-- Calculate total cost breakdown
WITH RECURSIVE bom_explosion AS (
    -- Anchor: Top-level product
    SELECT 
        parent_part_id,
        child_part_id,
        quantity_required,
        0 as level,
        CAST(child_part_id AS VARCHAR(500)) as path
    FROM bill_of_materials
    WHERE parent_part_id = 'BIKE-001'
    
    UNION ALL
    
    -- Recursive: Explode sub-assemblies
    SELECT 
        bom.parent_part_id,
        bom.child_part_id,
        be.quantity_required * bom.quantity_required,
        be.level + 1,
        CONCAT(be.path, ' -> ', bom.child_part_id)
    FROM bill_of_materials bom
    JOIN bom_explosion be ON bom.parent_part_id = be.child_part_id
)
SELECT 
    be.child_part_id,
    p.part_name,
    be.quantity_required,
    p.unit_cost,
    be.quantity_required * p.unit_cost as total_cost,
    be.level,
    be.path
FROM bom_explosion be
JOIN parts p ON be.child_part_id = p.part_id
ORDER BY be.level, be.child_part_id;

Graph Traversal

Social Network Connections

CREATE TABLE users (
    user_id INT PRIMARY KEY,
    username VARCHAR(50),
    email VARCHAR(100)
);

CREATE TABLE friendships (
    user_id INT,
    friend_id INT,
    created_date DATE,
    PRIMARY KEY (user_id, friend_id)
);

-- Find friends of friends (2nd degree connections)
WITH RECURSIVE friend_network AS (
    -- Anchor: Direct friends
    SELECT 
        user_id,
        friend_id,
        1 as degree,
        CAST(CONCAT(user_id, '->', friend_id) AS VARCHAR(1000)) as path
    FROM friendships
    WHERE user_id = 1
    
    UNION ALL
    
    -- Recursive: Friends of friends
    SELECT 
        fn.user_id,
        f.friend_id,
        fn.degree + 1,
        CONCAT(fn.path, '->', f.friend_id)
    FROM friend_network fn
    JOIN friendships f ON fn.friend_id = f.user_id
    WHERE fn.degree < 3  -- Limit to 3 degrees of separation
    AND f.friend_id != fn.user_id  -- Avoid cycles back to original user
)
SELECT DISTINCT
    fn.friend_id,
    u.username,
    MIN(fn.degree) as closest_degree,
    COUNT(*) as connection_paths
FROM friend_network fn
JOIN users u ON fn.friend_id = u.user_id
WHERE fn.friend_id != 1  -- Exclude the original user
GROUP BY fn.friend_id, u.username
ORDER BY closest_degree, u.username;

Advanced Recursive Patterns

Path Finding with Costs

CREATE TABLE locations (
    location_id INT PRIMARY KEY,
    location_name VARCHAR(100)
);

CREATE TABLE routes (
    from_location INT,
    to_location INT,
    distance INT,
    travel_time INT
);

-- Find shortest path between two locations
WITH RECURSIVE shortest_path AS (
    -- Anchor: Starting location
    SELECT 
        from_location,
        to_location,
        distance,
        travel_time,
        1 as hop_count,
        CAST(CONCAT(from_location, '->', to_location) AS VARCHAR(1000)) as path,
        distance as total_distance,
        travel_time as total_time
    FROM routes
    WHERE from_location = 1  -- Starting point
    
    UNION ALL
    
    -- Recursive: Extend paths
    SELECT 
        sp.from_location,
        r.to_location,
        r.distance,
        r.travel_time,
        sp.hop_count + 1,
        CONCAT(sp.path, '->', r.to_location),
        sp.total_distance + r.distance,
        sp.total_time + r.travel_time
    FROM shortest_path sp
    JOIN routes r ON sp.to_location = r.from_location
    WHERE sp.hop_count < 5  -- Prevent infinite loops
    AND sp.path NOT LIKE CONCAT('%', r.to_location, '%')  -- Avoid cycles
)
SELECT 
    to_location as destination,
    MIN(total_distance) as shortest_distance,
    MIN(total_time) as fastest_time
FROM shortest_path
WHERE to_location = 10  -- Destination
GROUP BY to_location;

Recursive Aggregations

-- Calculate running totals in hierarchical data
WITH RECURSIVE sales_hierarchy AS (
    -- Anchor: Top-level regions
    SELECT 
        region_id,
        region_name,
        parent_region_id,
        sales_amount,
        0 as level
    FROM sales_regions
    WHERE parent_region_id IS NULL
    
    UNION ALL
    
    -- Recursive: Child regions
    SELECT 
        sr.region_id,
        sr.region_name,
        sr.parent_region_id,
        sr.sales_amount,
        sh.level + 1
    FROM sales_regions sr
    JOIN sales_hierarchy sh ON sr.parent_region_id = sh.region_id
),
region_totals AS (
    SELECT 
        region_id,
        region_name,
        level,
        sales_amount,
        (SELECT SUM(sales_amount) 
         FROM sales_hierarchy sh2 
         WHERE sh2.region_id = sh1.region_id 
         OR sh2.parent_region_id = sh1.region_id) as total_including_children
    FROM sales_hierarchy sh1
)
SELECT * FROM region_totals ORDER BY level, region_name;

Performance Considerations

Limiting Recursion Depth

-- PostgreSQL: Set recursion limit
SET max_stack_depth = '2MB';

-- SQL Server: Use MAXRECURSION hint
WITH RECURSIVE hierarchy AS (
    -- Base case
    SELECT employee_id, name, manager_id, 0 as level
    FROM employees
    WHERE manager_id IS NULL
    
    UNION ALL
    
    -- Recursive case
    SELECT e.employee_id, e.name, e.manager_id, h.level + 1
    FROM employees e
    JOIN hierarchy h ON e.manager_id = h.employee_id
    WHERE h.level < 10  -- Explicit depth limit
)
SELECT * FROM hierarchy
OPTION (MAXRECURSION 100);  -- SQL Server syntax

Optimizing Recursive Queries

-- Create appropriate indexes
CREATE INDEX IX_employees_manager_id ON employees(manager_id);
CREATE INDEX IX_categories_parent_id ON categories(parent_category_id);

-- Use breadth-first vs depth-first traversal
WITH RECURSIVE breadth_first AS (
    SELECT employee_id, name, manager_id, 0 as level
    FROM employees
    WHERE employee_id = 1
    
    UNION ALL
    
    SELECT e.employee_id, e.name, e.manager_id, bf.level + 1
    FROM employees e
    JOIN breadth_first bf ON e.manager_id = bf.employee_id
)
SELECT * FROM breadth_first ORDER BY level, employee_id;

Database-Specific Implementations

PostgreSQL

-- PostgreSQL supports RECURSIVE keyword
WITH RECURSIVE t(n) AS (
    VALUES (1)
    UNION ALL
    SELECT n+1 FROM t WHERE n < 100
)
SELECT * FROM t;

SQL Server

-- SQL Server uses CTE without RECURSIVE keyword
WITH hierarchy AS (
    SELECT employee_id, name, manager_id, 0 as level
    FROM employees
    WHERE manager_id IS NULL
    
    UNION ALL
    
    SELECT e.employee_id, e.name, e.manager_id, h.level + 1
    FROM employees e
    INNER JOIN hierarchy h ON e.manager_id = h.employee_id
)
SELECT * FROM hierarchy;

MySQL

-- MySQL 8.0+ supports recursive CTEs
WITH RECURSIVE fibonacci AS (
    SELECT 1 as n, 0 as fib_n, 1 as fib_n1
    UNION ALL
    SELECT n + 1, fib_n1, fib_n + fib_n1
    FROM fibonacci
    WHERE n < 10
)
SELECT n, fib_n as fibonacci_number FROM fibonacci;

Common Use Cases

1. Organizational Charts

  • Employee hierarchies
  • Department structures
  • Reporting relationships

2. Product Catalogs

  • Category trees
  • Product classifications
  • Nested menus

3. Geographic Hierarchies

  • Country > State > City > District
  • Sales territories
  • Administrative divisions

4. Financial Structures

  • Account hierarchies
  • Cost center rollups
  • Budget allocations

Interview Tips

  • Understand the anchor and recursive member concepts
  • Know how to prevent infinite loops with depth limits
  • Be familiar with cycle detection techniques
  • Practice common hierarchical data scenarios
  • Understand performance implications of recursive queries
  • Know database-specific syntax differences
  • Be able to explain when to use recursive vs iterative approaches

Test Your Knowledge

Take a quick quiz to test your understanding of this topic.