ITPub博客

首页 > 数据库 > MySQL > 条码批次拆分合并怎么建模:父子批次关系表与递归查询

条码批次拆分合并怎么建模:父子批次关系表与递归查询

原创 MySQL 作者:制造业数字化观察 时间:2026-11-02 12:26:10 0 删除 编辑


条码批次的拆分与合并,用“批次主表+父子关系边表+批次事务表”三张表建模比较稳妥:批次码一经生成就不再修改,每一次拆分或合并都只新增关系边,再用递归公用表表达式向上追来源、向下追去向。这样无论一个批次被分了几次、又和别的批次合了几次,追溯都只是在一张边表上做图遍历。

什么是“拆合皆边”?

“拆合皆边”是指:批次的拆分和合并都不改动原批次记录,而是在关系表中新增“父批次→子批次”的有向边,并用同一个事务号把一次操作产生的所有边串起来。拆分是一父多子,合并是多父一子,两者在数据结构上是同一种东西,所以查询逻辑只需要写一套。

需要哪几张表?

批次建模的基本集合是三张表,外加一张可选的数量流水表用于账实核对。

表名 关键字段 用途
lot lot_no(主键)、material_code、qty_init、qty_remain、status、create_txn 每个批次一行,记录初始数量、剩余数量与状态(在用/已拆完/已合并/冻结)
lot_relation parent_lot、child_lot、rel_type、qty、txn_id、op_time 有向边:rel_type 取 SPLIT 或 MERGE,qty 为从父批次转到子批次的数量
lot_txn txn_id(主键)、txn_type、operator、station、op_time、remark 一次拆分或合并操作一行,用于审计和整单回滚判断
lot_qty_ledger(可选) lot_no、txn_id、delta_qty、balance_after 批次数量流水,便于按时间点重算剩余数量

邻接表、闭包表还是路径枚举?

对MES批次谱系,邻接表(父子边表)加递归查询是写入简单、足够灵活的默认选择;只有在谱系很深、反查非常频繁时才考虑闭包表。

方案 写入成本 查询成本 合并(多父)支持 适合
邻接表(父子边表) 每次只写几条边 需要递归查询 天然支持 多数车间的批次拆合
闭包表 每次要写入所有祖先与后代组合,行数随深度增长 单次关联即可,查询简单 支持,但写入组合更多 层级深、查询远多于写入
路径枚举(存 /A/B/C) 写入时拼接路径 LIKE 前缀查询 多父时一条路径表达不了,需要冗余多行 纯树结构,不推荐用于有合并的批次
嵌套集合 插入要重排左右值 子树查询快 不支持多父 基本不适合批次

判断标准很直接:只要存在合并,结构就是有向无环图而不是树,路径枚举和嵌套集合都会变得别扭。

建表与拆分写法

拆分和合并都必须在一个数据库事务里完成:写事务表、写边、扣减父批次剩余数量、创建子批次,任何一步失败整体回滚。

CREATE TABLE lot (
  lot_no        VARCHAR(40)    NOT NULL PRIMARY KEY,
  material_code VARCHAR(30)    NOT NULL,
  qty_init      DECIMAL(18,4)  NOT NULL,
  qty_remain    DECIMAL(18,4)  NOT NULL,
  status        VARCHAR(10)    NOT NULL,
  create_txn    VARCHAR(32)    NULL
) ENGINE=InnoDB;
CREATE TABLE lot_relation (
  parent_lot VARCHAR(40)   NOT NULL,
  child_lot  VARCHAR(40)   NOT NULL,
  rel_type   VARCHAR(5)    NOT NULL,
  qty        DECIMAL(18,4) NOT NULL,
  txn_id     VARCHAR(32)   NOT NULL,
  op_time    DATETIME      NOT NULL,
  PRIMARY KEY (parent_lot, child_lot, txn_id),
  KEY idx_child (child_lot, parent_lot)
) ENGINE=InnoDB;
-- 拆分示例:批次 L100 拆出 L101、L102
START TRANSACTION;
SELECT qty_remain FROM lot WHERE lot_no = 'L100' FOR UPDATE;
INSERT INTO lot_txn VALUES ('T9001','SPLIT','工号示例','分装工位',NOW(),NULL);
INSERT INTO lot VALUES ('L101','M-示例',30,30,'ACTIVE','T9001'),
                       ('L102','M-示例',20,20,'ACTIVE','T9001');
INSERT INTO lot_relation VALUES ('L100','L101','SPLIT',30,'T9001',NOW()),
                                ('L100','L102','SPLIT',20,'T9001',NOW());
UPDATE lot SET qty_remain = qty_remain - 50 WHERE lot_no = 'L100';
COMMIT;

边表的主键带上 txn_id,是为了允许同一对父子批次在不同操作中多次出现(例如部分退回后再次合并);idx_child 索引专门服务向上追溯。

向上追来源、向下追去向,SQL怎么写?

递归查询要同时带上深度和已访问路径两个字段:深度用来限制层数,路径用来防止数据异常形成环时无限递归。

-- 向上:L205 的全部来源批次
WITH RECURSIVE up AS (
  SELECT parent_lot, child_lot, qty, 1 AS depth,
         CAST(CONCAT(',', child_lot, ',', parent_lot, ',') AS CHAR(2000)) AS path
  FROM lot_relation WHERE child_lot = 'L205'
  UNION ALL
  SELECT r.parent_lot, r.child_lot, r.qty, u.depth + 1,
         CONCAT(u.path, r.parent_lot, ',')
  FROM lot_relation r JOIN up u ON r.child_lot = u.parent_lot
  WHERE u.depth < 20
    AND LOCATE(CONCAT(',', r.parent_lot, ','), u.path) = 0
)
SELECT DISTINCT parent_lot, depth FROM up ORDER BY depth;
-- 向下:L100 去了哪些批次(把 JOIN 方向反过来)
WITH RECURSIVE down AS (
  SELECT parent_lot, child_lot, 1 AS depth FROM lot_relation WHERE parent_lot = 'L100'
  UNION ALL
  SELECT r.parent_lot, r.child_lot, d.depth + 1
  FROM lot_relation r JOIN down d ON r.parent_lot = d.child_lot
  WHERE d.depth < 20
)
SELECT DISTINCT child_lot, depth FROM down ORDER BY depth;

MySQL 8.0 起支持 WITH RECURSIVE,系统变量 cte_max_recursion_depth 默认为1000层,超过会报错而不是死循环;PostgreSQL 写法基本相同,14版起还可以用 CYCLE 子句检测环;Oracle 既可用递归 WITH,也可用 CONNECT BY 加 NOCYCLE。

数量守恒怎么校验?

数量守恒是批次模型能否被信任的底线:同一事务里,所有父批次被扣减的数量之和必须等于所有子批次获得的数量之和。

SELECT r.txn_id, SUM(r.qty) AS moved_qty,
       (SELECT SUM(l.qty_init) FROM lot l WHERE l.create_txn = r.txn_id) AS child_init_qty
FROM lot_relation r
GROUP BY r.txn_id
HAVING moved_qty <> child_init_qty;

这条语句适用于“每次操作都新建子批次”的约定;如果合并是并入一个已存在的批次,就要改为核对该批次的数量流水。把它放进夜间巡检,任何返回行都说明有一次操作没写完整或被人工改过数据。

实施步骤

步骤一,和仓库、生产约定批次码规则与“拆分必生成新码、合并必生成新码”的操作规范。

步骤二,建三张表和必要索引,边表同时建父→子与子→父两个方向的索引。

步骤三,把拆分、合并做成服务端事务接口,前端扫码只传父批次码、数量和工位。

步骤四,实现向上、向下两条递归查询,并加深度和环路保护。

步骤五,上线数量守恒巡检,和库存账做一次全量核对。

步骤六,选一个真实物料走完“来料→分装→合并投料→成品”全链路,演练正反向追溯。

常见坑

1. 拆分时直接改原批次码后缀(L100改成L100-1),历史单据引用全部失效,追溯链断裂。

2. 合并沿用某一个父批次码,导致另一批来源在谱系里“消失”。

3. 拆分没放在一个事务里,网络中断后出现子批次已建、父批次未扣减的半成品数据。

4. 递归查询不做深度和环路保护,脏数据形成环时查询直到撞上递归上限才报错。

5. 边表只建了父批次索引,向上追溯时每一层都要全表扫描。

6. 数量用 FLOAT 存储,多次拆分后出现小数误差,守恒校验总是对不上,应使用 DECIMAL。

7. 允许人工直接改 qty_remain,事后无法从事务表和流水重算出当前数量。

车间场景

来料检验后,一卷原材料登记为批次 L100。分装工位按工单需要把它拆成 L101 和 L102 两个小包装,分别贴新码。L101 用掉一部分后,余量与另一卷材料拆出来的 L150 在投料口合并成新批次 L205 投入生产。三周后成品抽检发现问题,质量工程师在追溯页输入成品对应的投料批次 L205,向上查询得到 L101、L150,再往上到 L100 和另一卷原材料;随后对 L100 做向下查询,列出同一卷料拆出的 L102 去了哪些工单,决定是否需要扩大隔离范围。

验收项

1. 任意一次拆分或合并都能通过 txn_id 查到完整的边、操作人、工位和时间。

2. 数量守恒巡检语句在验收数据上返回空结果。

3. 人为构造一条成环的脏数据,递归查询能正常结束并给出提示。

4. 向上、向下查询结果与人工按单据追踪的结果一致。

5. 原批次码在系统中未被修改,历史单据全部可关联。

6. 在实际数据量下测试多层谱系的查询耗时,并记录测试条件和结果(以现场实测为准)。

FAQ

问:拆分后剩下的那部分,要不要也生成一个新批次码?

答:两种约定都可行:原批次保留余量、只新建拆出的部分,或者原批次全部拆完。关键是选定一种并写进规范,守恒校验按约定来写。

问:闭包表什么时候值得用?

答:当谱系层级很深、追溯查询频率远高于拆合频率,并且递归查询的耗时实测达不到要求时,可以在边表之外再维护一张闭包表作为查询加速,事实来源仍然只有边表这一张。

问:合并时不同父批次的物料编码不一样怎么办?

答:在合并接口里校验:同物料才允许合并,不同物料的组合属于投料或装配关系,应记录在工序追溯表中,而不是批次关系表。

问:批次冻结后还能拆分吗?

答:建议在拆合接口中检查父批次状态,冻结、已拆完、已报废的批次一律拒绝操作,并把被拒绝的请求记入日志。

问:历史数据量很大时,边表需要分区吗?

答:边表通常比过站记录少得多,一般先靠索引解决;确实需要分区时,注意递归查询会跨越时间,按时间分区的裁剪效果有限。

更新于2026年11月

本文为作者原创,转载请注明出处与作者。

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70051529/viewspace-3187327/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
关注离散制造MES、WMS、条码追溯与设备数据采集的实施经验分享。

注册时间:2026-10-24

  • 博文量
    7
  • 访问量
    4262