60. Спроектировать схему БД для состава изделия (BOM)
Условие задачи:
Необходимо спроектировать схему БД для хранения состава изделия — BOM (Bill of Materials).
Пример структуры:
Датчик
Корпус — 1 шт
Винт — 8 шт
Винт — 4 шт
Необходимо:
хранить единый справочник изделий и деталей;
хранить связи «изделие → компонент»;
указывать количество компонента, необходимое для сборки;
поддерживать вложенную структуру произвольной глубины;
позволять одной и той же детали использоваться в разных изделиях и на разных уровнях.
Спойлеры к решению
Подсказки
💡 В таблице
items храни сами изделия и детали.💡 В таблице
bom храни связь «родительское изделие → компонент» и количество.💡 Уровень вложенности хранить необязательно — его можно определить при рекурсивном обходе BOM.
💡 Одна и та же запись из
items может встречаться во множестве строк bom.Решение
Для базовой модели достаточно двух таблиц:
CREATE TABLE items (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
item_code VARCHAR(50) UNIQUE,
item_type VARCHAR(50),
unit VARCHAR(20) NOT NULL DEFAULT 'шт',
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE bom (
id BIGSERIAL PRIMARY KEY,
parent_item_id BIGINT NOT NULL
REFERENCES items(id),
component_item_id BIGINT NOT NULL
REFERENCES items(id),
quantity NUMERIC(12, 3) NOT NULL
CHECK (quantity > 0),
position INT,
CHECK (parent_item_id <> component_item_id)
);
items — единый справочник номенклатуры:
id | name | item_type
---+---------+----------
1 | Датчик | ASSEMBLY
2 | Корпус | ASSEMBLY
3 | Винт | PART
bom описывает непосредственный состав каждого изделия:
id | parent_item_id | component_item_id | quantity
---+----------------+-------------------+---------
1 | 1 | 2 | 1
2 | 1 | 3 | 4
3 | 2 | 3 | 8
Например:
INSERT INTO bom (
parent_item_id,
component_item_id,
quantity,
position
)
VALUES
(1, 2, 1, 1), -- Датчик -> Корпус: 1 шт
(1, 3, 4, 2), -- Датчик -> Винт: 4 шт
(2, 3, 8, 1); -- Корпус -> Винт: 8 шт
Одна и та же деталь Винт хранится в items только один раз, но может использоваться в разных строках bom с разным количеством.
Связи выглядят так:
items
1 Датчик
│
├── 1 × Корпус ─── 8 × Винт
│
└── 4 × Винт
Поле level хранить в bom обычно не нужно. Уровень зависит не от самой связи, а от пути, по которому мы пришли к компоненту, и вычисляется при рекурсивном обходе.
Например, получить всё дерево изделия в PostgreSQL можно через рекурсивный CTE:
WITH RECURSIVE product_tree AS (
SELECT
b.parent_item_id,
b.component_item_id,
b.quantity,
1 AS depth
FROM bom b
WHERE b.parent_item_id = 1
UNION ALL
SELECT
b.parent_item_id,
b.component_item_id,
pt.quantity * b.quantity,
pt.depth + 1
FROM bom b
JOIN product_tree pt
ON b.parent_item_id = pt.component_item_id
)
SELECT
i.id,
i.name,
pt.quantity,
pt.depth
FROM product_tree pt
JOIN items i
ON i.id = pt.component_item_id;
Для быстрого поиска компонентов и обратного поиска изделий, в которых используется деталь, полезны индексы:
CREATE INDEX idx_bom_parent
ON bom (parent_item_id);
CREATE INDEX idx_bom_component
ON bom (component_item_id);
Итого:
items 1 ─── N bom N ─── 1 items
parent component
items отвечает на вопрос «что это за изделие или деталь», а bom — «из каких компонентов и в каком количестве оно состоит».