代码改变世界

制造业质量追溯02:用 Oracle 26ai 属性图(Property Graph)搞定工业网状追溯

2026-09-24 07:20  AlfredZhao  阅读(40)  评论(0)    收藏  举报

本文继续探讨轮胎制造场景(已简化,实际会更复杂),一旦某台设备报警,最头疼的问题往往是:这台设备生产的半成品,最终流到了哪些成品轮胎上?又是谁组装的?传统做法靠多表自联接加 CONNECT BY 递归硬凑,SQL 又长又难维护。此时若在合适的场景下使用 Oracle 属性图,查询思路往往会更清晰。

01 | 先建三张基础表

追溯的骨架是设备、人员、生产履历三张表。设备表记录密炼机、胎面挤出机、成型机等,状态含 NORMALALARM;人员表记录操作工与所属班组;生产履历表则把「哪个批次、由哪台设备、在哪个工人操作下生产」这条关系固定下来。

-- 前置:lot_master 与 lot_genealogy 沿用第 01 篇定义,此处不再重复
-- 若单独运行本文脚本,请先创建这两张表并插入对应批次数据

CREATE TABLE equipment_master (
    eq_id   VARCHAR2(50) PRIMARY KEY,
    eq_name VARCHAR2(100) NOT NULL,
    eq_type VARCHAR2(30)  NOT NULL,
    status  VARCHAR2(20)  DEFAULT 'NORMAL'
);

CREATE TABLE operator_master (
    op_id       VARCHAR2(50) PRIMARY KEY,
    op_name     VARCHAR2(100) NOT NULL,
    shift_group VARCHAR2(20)  NOT NULL
);

CREATE TABLE production_history (
    lot_id       VARCHAR2(50) NOT NULL,
    eq_id        VARCHAR2(50) NOT NULL,
    op_id        VARCHAR2(50) NOT NULL,
    produce_time DATE DEFAULT SYSDATE,
    CONSTRAINT pk_prod_hist PRIMARY KEY (lot_id, eq_id, op_id),
    CONSTRAINT fk_ph_lot FOREIGN KEY (lot_id) REFERENCES lot_master(lot_id),
    CONSTRAINT fk_ph_eq  FOREIGN KEY (eq_id)  REFERENCES equipment_master(eq_id),
    CONSTRAINT fk_ph_op  FOREIGN KEY (op_id)  REFERENCES operator_master(op_id)
);

--插入测试数据
BEGIN
    -- 插入设备数据(密炼机 EQ-MIXER-01 状态设为 ALARM 报警)
    INSERT INTO equipment_master VALUES ('EQ-MIXER-01', '1号密炼机', 'MIXER', 'ALARM');
    INSERT INTO equipment_master VALUES ('EQ-BUILD-02', '2号轮胎成型机', 'BUILDING_MACHINE', 'NORMAL');

    -- 插入人员数据
    INSERT INTO operator_master VALUES ('OP-ZHANG-01', '张工(密炼主操)', 'A_SHIFT');
    INSERT INTO operator_master VALUES ('OP-LI-02', '李工(成型主操)', 'B_SHIFT');

    -- 插入生产履历关系
    -- 混炼胶 SEMI-COMPOUND-101 是在 故障密炼机 EQ-MIXER-01 上由 张工 生产的
    INSERT INTO production_history VALUES ('SEMI-COMPOUND-101', 'EQ-MIXER-01', 'OP-ZHANG-01', DATE '2026-01-02');
    -- 成品轮胎 FG-TIRE-DOT-8888 是在 2号成型机 上由 李工 组装的
    INSERT INTO production_history VALUES ('FG-TIRE-DOT-8888', 'EQ-BUILD-02', 'OP-LI-02', DATE '2026-01-05');
    
    COMMIT;
END;
/

测试数据里,EQ-MIXER-01 被设为 ALARM,它生产的半成品 SEMI-COMPOUND-101 由张工操作;成品轮胎 FG-TIRE-DOT-8888 则在 EQ-BUILD-02 上由李工组装。

这三张表之间的引用关系,可以用一张图先理清楚——设备、人员、批次各自是节点,生产履历把批次分别连到设备和人员上:

erDiagram equipment_master ||--o{ production_history : "被生产于" operator_master ||--o{ production_history : "被操作" lot_master ||--o{ production_history : "对应批次" lot_master ||--o{ lot_genealogy : "作为父批次" lot_master ||--o{ lot_genealogy : "作为子批次" equipment_master { VARCHAR2 eq_id PK VARCHAR2 eq_name VARCHAR2 eq_type VARCHAR2 status } operator_master { VARCHAR2 op_id PK VARCHAR2 op_name VARCHAR2 shift_group } production_history { VARCHAR2 lot_id PK VARCHAR2 eq_id PK VARCHAR2 op_id PK DATE produce_time }

02 | 把关系表声明成属性图

属性图的价值在于:把「点」和「边」显式声明出来,查询时就能用图模式匹配,而不是反复自联接。

CREATE PROPERTY GRAPH industrial_trace_graph
  VERTEX TABLES (
    lot_master KEY (lot_id) LABEL LOT
      PROPERTIES (lot_id, item_name, item_type),
    equipment_master KEY (eq_id) LABEL EQUIPMENT
      PROPERTIES (eq_id, eq_name, eq_type, status),
    operator_master KEY (op_id) LABEL OPERATOR
      PROPERTIES (op_id, op_name, shift_group)
  )
  EDGE TABLES (
    lot_genealogy
      KEY (parent_lot_id, child_lot_id)
      SOURCE KEY (parent_lot_id) REFERENCES lot_master(lot_id)
      DESTINATION KEY (child_lot_id) REFERENCES lot_master(lot_id)
      LABEL CONSUMES
      PROPERTIES (quantity_used),
    production_history AS lot_produced_on_eq
      KEY (lot_id, eq_id)
      SOURCE KEY (lot_id) REFERENCES lot_master(lot_id)
      DESTINATION KEY (eq_id) REFERENCES equipment_master(eq_id)
      LABEL PRODUCED_ON,
    production_history AS lot_operated_by_op
      KEY (lot_id, op_id)
      SOURCE KEY (lot_id) REFERENCES lot_master(lot_id)
      DESTINATION KEY (op_id) REFERENCES operator_master(op_id)
      LABEL OPERATED_BY
  );

这里定义了三种边:批次消耗批次(CONSUMES)、批次生产于设备(PRODUCED_ON)、批次由人员操作(OPERATED_BY)。

需要留意的是,production_history 这张表被复用了两次,分别声明成 lot_produced_on_eqlot_operated_by_op 两条边。也就是说,同一张关系表可以按不同的语义拆成多条边,这正是属性图把「关系」显式化的体现——表还是那张表,但图里它承担了两种角色。

graph LR EQ["EQUIPMENT<br/>equipment_master"] SEMI["LOT<br/>SEMI-COMPOUND-101"] TIRE["LOT<br/>FG-TIRE-DOT-8888"] OP1["OPERATOR<br/>张工"] OP2["OPERATOR<br/>李工"] SEMI -- "PRODUCED_ON" --> EQ SEMI -- "OPERATED_BY" --> OP1 SEMI -- "CONSUMES" --> TIRE TIRE -- "OPERATED_BY" --> OP2

03 | 三种查询写法对比

传统 CONNECT BY 写法要先用子查询找故障设备的初始批次,再沿物料树递归,最后外联生产履历找组装工人,多层嵌套,可读性差。

-- 1.传统 CONNECT BY 写法:多层子查询与自联接硬凑
SELECT 
    eq.eq_name          AS alarm_equipment,
    t.trace_path,
    t.final_lot_id,
    m_final.item_name   AS final_item_name,
    op.op_name          AS final_assembly_operator
FROM (
    -- 第一步:找故障设备生产的初始批次
    SELECT ph.lot_id AS start_lot_id, ph.eq_id
    FROM production_history ph
    JOIN equipment_master eq ON ph.eq_id = eq.eq_id
    WHERE eq.status = 'ALARM'
) eq_start
-- 第二步:用 CONNECT BY 沿着物料树向下递归
JOIN (
    SELECT 
        CONNECT_BY_ROOT parent_lot_id AS root_lot,
        SYS_CONNECT_BY_PATH(parent_lot_id, ' -> ') || ' -> ' || child_lot_id AS trace_path,
        child_lot_id AS final_lot_id
    FROM lot_genealogy
    CONNECT BY PRIOR child_lot_id = parent_lot_id
) t ON eq_start.start_lot_id = t.root_lot
JOIN equipment_master eq ON eq_start.eq_id = eq.eq_id
JOIN lot_master m_final ON t.final_lot_id = m_final.lot_id
-- 第三步:再外联生产履历查找终极成品的组装工人
LEFT JOIN production_history ph_final ON t.final_lot_id = ph_final.lot_id
LEFT JOIN operator_master op ON ph_final.op_id = op.op_id;

属性图的 GRAPH_TABLE 写法则是声明式的:先匹配报警设备生产的半成品,再沿 CONSUMES 变长递归 1~5 跳找到成品轮胎,最后匹配组装人员。

-- 2.属性图的写法
SELECT 
    gt.alarm_eq_name,
    gt.affected_semi_lot,
    gt.final_tire_lot,
    gt.final_tire_name,
    gt.assembly_op_name
FROM GRAPH_TABLE ( industrial_trace_graph
  MATCH 
    -- 1. 匹配故障设备生产的半成品胶料
    (eq IS EQUIPMENT WHERE eq.status = 'ALARM')<-[IS PRODUCED_ON]-(semi IS LOT)
    -- 2. 从半成品胶料变长递归 1~5 跳找到最终成品轮胎
    -[IS CONSUMES]->{1, 5} (tire IS LOT WHERE tire.item_type = 'FG' OR tire.item_type = 'CUST')
    -- 3. 匹配成品轮胎的组装人员
    -[IS OPERATED_BY]->(op IS OPERATOR)
  COLUMNS (
    eq.eq_name         AS alarm_eq_name,
    semi.lot_id        AS affected_semi_lot,
    tire.lot_id        AS final_tire_lot,
    tire.item_name     AS final_tire_name,
    op.op_name         AS assembly_op_name
  )
) gt;

如果只想让图引擎专注物料树遍历,人员关联放到外层做传统 LEFT JOIN 也可以,这就是混合模式,能保证记录不漏。

-- 3.属性图专注物料树遍历,人员关联放到外层的混合查询写法
SELECT 
    gt.alarm_eq_name,
    gt.affected_semi_lot,
    gt.target_lot_id,
    gt.target_item_name,
    gt.target_item_type,
    op.op_name AS assembly_op_name -- 外层关联出来的工人名
FROM GRAPH_TABLE ( industrial_trace_graph
  MATCH 
    -- 图引擎只专注于复杂的网状物料树遍历
    (eq IS EQUIPMENT WHERE eq.status = 'ALARM')<-[IS PRODUCED_ON]-(semi IS LOT)
    -[IS CONSUMES]->{1, 5} (target IS LOT)
  COLUMNS (
    eq.eq_name         AS alarm_eq_name,
    semi.lot_id        AS affected_semi_lot,
    target.lot_id      AS target_lot_id,
    target.item_name   AS target_item_name,
    target.item_type   AS target_item_type
  )
) gt
-- 扩展节点(工人)放到外层做传统 LEFT JOIN,绝不漏掉记录
LEFT JOIN production_history ph ON gt.target_lot_id = ph.lot_id
LEFT JOIN equipment_master eq_final ON ph.eq_id = eq_final.eq_id AND eq_final.eq_type = 'BUILDING_MACHINE'
LEFT JOIN operator_master op   ON ph.op_id = op.op_id;

三种写法的差异,可以用一张流程图概括:传统写法把「找起点、递归、外联」拆成三段各自为战;属性图写法把整条追溯路径压进一个 MATCH 模式;混合模式则在图遍历之后,把非核心的关联交回关系型 LEFT JOIN

flowchart TD A["报警设备 status = ALARM"] --> B{查询方式} B -->|传统 CONNECT BY| C1["子查询找初始批次"] C1 --> C2["CONNECT BY 递归物料树"] C2 --> C3["外联 production_history 找工人"] C3 --> C4["多层嵌套,可读性差"] B -->|属性图 GRAPH_TABLE| D1["MATCH 匹配报警设备"] D1 --> D2["CONSUMES 变长递归 1~5 跳"] D2 --> D3["OPERATED_BY 匹配组装人员"] D3 --> D4["声明式,模式即路径"] B -->|混合模式| E1["图引擎只做物料树遍历"] E1 --> E2["外层 LEFT JOIN 关联工人"] E2 --> E3["保证记录不漏"]

04 | 小结

属性图把复杂的网状物料追溯变成了一句声明式匹配,递归跳数、节点条件都写在模式里,在本文这类关系密集、路径较深的场景下可读性更好。是否带来性能与维护收益,仍需结合所用 Oracle 版本、数据规模和实际执行计划评估。

关注我,和AI一起成长~