Advanced Recursion — Bill of Materials, Cost Rollup & Shortest Path

Advanced Recursion — Bill of Materials, Cost Rollup & Shortest Path

We covered basic org-tree recursion earlier. This lesson pushes into the genuinely hard recursive patterns: bill of materials with aggregated cost rollup (each parent's cost = sum of all descendants) and shortest path in a weighted graph — both classic interview problems that require recursive CTEs to solve in pure SQL.

Bill of Materials with Cost Rollup

-- BOM: products made from sub-components made from raw materials
CREATE TABLE IF NOT EXISTS bom_components (
    id          SERIAL PRIMARY KEY,
    name        TEXT NOT NULL,
    parent_id   INT REFERENCES bom_components(id),
    qty_needed  NUMERIC(10,4) DEFAULT 1,  -- quantity of this component per parent unit
    unit_cost   NUMERIC(10,4) DEFAULT 0   -- leaf cost (0 for assemblies)
);

INSERT INTO bom_components (id, name, parent_id, qty_needed, unit_cost)
VALUES
    (1,  'Bicycle',          NULL, 1,    0),
    (2,  'Frame Assembly',   1,    1,    0),
    (3,  'Wheel Assembly',   1,    2,    0),   -- 2 wheels per bike
    (4,  'Drivetrain',       1,    1,    0),
    (5,  'Steel Tubing',     2,    3,    12.50),
    (6,  'Welds',            2,    8,    0.75),
    (7,  'Paint',            2,    1,    8.00),
    (8,  'Rim',              3,    1,    22.00),
    (9,  'Spokes',           3,    32,   0.30),
    (10, 'Tyre',             3,    1,    18.00),
    (11, 'Hub',              3,    1,    14.50),
    (12, 'Chain',            4,    1,    9.00),
    (13, 'Cassette',         4,    1,    28.00),
    (14, 'Derailleur',       4,    1,    45.00),
    (15, 'Crankset',         4,    1,    38.00)
ON CONFLICT DO NOTHING;

-- Recursive BOM explosion with cumulative quantity and cost rollup
WITH RECURSIVE bom AS (
    -- Anchor: top-level product
    SELECT
        id,
        name,
        parent_id,
        qty_needed,
        unit_cost,
        qty_needed::NUMERIC             AS cumulative_qty,  -- qty relative to root
        0                               AS depth,
        ARRAY[id]                       AS path,
        name                            AS full_path
    FROM bom_components
    WHERE parent_id IS NULL

    UNION ALL

    SELECT
        c.id,
        c.name,
        c.parent_id,
        c.qty_needed,
        c.unit_cost,
        b.cumulative_qty * c.qty_needed AS cumulative_qty,  -- multiply down the tree
        b.depth + 1,
        b.path || c.id,
        b.full_path || ' > ' || c.name
    FROM bom_components c
    JOIN bom b ON b.id = c.parent_id
)
SELECT
    REPEAT('  ', depth) || name       AS component,
    ROUND(cumulative_qty, 4)            AS qty_per_bicycle,
    ROUND(unit_cost, 2)                 AS unit_cost_usd,
    ROUND(cumulative_qty * unit_cost, 4) AS extended_cost_usd,
    depth,
    full_path
FROM bom
ORDER BY path;

Purchase this course to unlock the full lesson.

Sign up