Bills of Material

In manufacturing, a bill of materials (BOM) defines all of the items required to produce a good. Often times this consists of raw materials (e.g. steel, aluminum) that are transformed or other components that are assembled to make the finished product. It may also contain ancillary items used during manufacturing, such as tooling or consumable chemicals.

bom-graph.svg

Figure 1: Hypothetical multi-level bill of materials for a pen. (Lunn and Neff 1992)

Data Structures

Looking at the bill of materials depiction above, it is obvious that a BOM is really just a tree whose nodes are components (or subassemblies) and edges indicate a component’s belonging to its parent’s BOM. The quantity of a component in a parent’s BOM would also be indicated by an edge, as it is a property of the relationship between the parent and component.

Most material requirements planning (MRP) systems persist data using a typical relational database which stores information in a tabular fashion. Thus, the information encoded in the BOM tree structure must be transformed to fit within a table structure. This is typically done using an adjacency list which may be easily encoded as a table, with 1 row per relationship. The table below represents the pen bill of materials as it may appear in a database.

SELECT * FROM bom
Component Parent Quantity
Pen   1.0
Lower Pen Pen 1.0
Upper Barrel Pen 1.0
Ink Pen 1.0
Inner Lower Pen 2.0
Point Lower Pen 1.0
Outer Lower Pen 2.0
Sleeve Upper Barrel 1.0
Clip Upper Barrel 2.0
Tubing Inner 5.0
Tubing Outer 3.0
Top Sleeve 1.0
Tube Sleeve 1.0
Steel Clip 4.0
Tubing Tube 2.0

Applications

With the BOM set up as above, there are some interesting questions we can answer with SQL queries.

Finding Components of an (Sub-)Assembly

Finding all components that comprise a particular assembly’s bill of material is a common MRP question. Obtaining a complete BOM requires finding the immediate children of the assembly, followed by children of the children, and so forth. This is complicated by the fact that we generally do not know the depth of a particular BOM.

The solution is to recursively find children of a component until we’ve reached a component that has no children (a leaf node in tree parlance). Algorithmically, this is referred to as a breadth (BFS) or depth-first search (DFS), depending on the order in which results are reported.

Most database flavors implement this using a recursive common table expression (CTE). Using our Pen bill of materials example, the below SQL query is how a recursive CTE would be implemented in SQLite to find all components of the Upper Barrel subassembly.

-- A depth-first search of the Upper Barrel subassembly
WITH RECURSIVE
recursive_bom(Component, Level) AS (
    -- The root of the tree, i.e. the component whose BOM we want
    VALUES('Upper Barrel', 0)
    UNION ALL
    SELECT bom.Component, recursive_bom.level + 1
    FROM bom JOIN recursive_bom ON bom.Parent = recursive_bom.Component
    ORDER BY recursive_bom.level + 1 DESC
)
-- Add leading periods to help visualize bom depth
SELECT level, printf('%.*c', level * 3, '.') || Component AS Component  FROM recursive_bom
Level Component
0 .Upper Barrel
1 …Clip
2 ……Steel
1 …Sleeve
2 ……Top
2 ……Tube
3 ………Tubing

Finding Total Quantities Required for All Components

Another important question is how many of each component is required to build a complete assembly. For example, looking at the BOM graph, it can be seen that tubing is required in multiple subassemblies, namely the Inner, Outer, and Tube. To build a complete pen, we would want to know the total amount of tubing required across all components, regardless of bom depth.

Again a recursive query is used, with the added complexity of accumulating all quantities for identical components along the way. Accumulating component quantities is accomplished by the added sum() and corresponding GROUP BY clause in the sql query.

WITH RECURSIVE
recursive_bom(Component, Quantity) AS (
    -- The root of the tree, i.e. the component whose BOM we want
    VALUES('Pen', 1)
    UNION ALL
    SELECT bom.Component, bom.Quantity * recursive_bom.Quantity
    FROM bom JOIN recursive_bom ON bom.Parent = recursive_bom.Component
  )
SELECT Component, sum(Quantity) AS [Total Qty Required] FROM recursive_bom
GROUP BY Component
Component Total Qty Required
Clip 2.0
Ink 1.0
Inner 2.0
Lower Pen 1.0
Outer 2.0
Pen 1
Point 1.0
Sleeve 1.0
Steel 8.0
Top 1.0
Tube 1.0
Tubing 18.0
Upper Barrel 1.0

Thus, in order to build 1 Pen assembly, a total of 18 Tubing is required. To understand this, we can examine all components that require Tubing in their BOM.

SELECT * FROM bom WHERE Component IN ('Tubing')
Component Parent Quantity
Tubing Inner 5.0
Tubing Outer 3.0
Tubing Tube 2.0

Tracing the ancestry of each parent all the way to the top level Pen:

  • Inner 5 x 2 x 1 = 10 Tubing
    • 5 Tubing is required for each Inner subassembly.
    • 2 Inners are required for each Lower Pen.
    • 1 Lower Pen is required for each Pen.
  • Outer 3 x 2 x 1 = 6 Tubing)
    • 3 Tubing is required for each Outer subassembly.
    • 2 Outers are required for each Lower Pen.
    • 1 Lower Pen is required for each Pen.
  • Tube 2 x 1 x 1 x 1 = 2 Tubing
    • 2 Tubing is required for each Tube.
    • 1 Tube is required for each Sleeve.
    • 1 Sleeve is required for each Upper Barrel.
    • 1 Upper Barrel is required for each Pen.
  • Grand total of 18 Tubing is required per Pen assembly.