摘要:最近一直在做有关仓库管理方面的项目,想把相关概念捋一捋,同时设计个简易的仓库模型来讨论一下所面临的挑战及解决方案。
最近一直在做有关仓库管理方面的项目,想把相关概念捋一捋,同时设计个简易的仓库模型来讨论一下所面临的挑战及解决方案。
企业A是一家制造型企业,他们拥有一间大型的仓库,存放所有用于生产的原材料,原材料都是整箱包装的。他们利用SAP系统中的仓库管理模块对仓库进行管理。
但企业A具有多个生产线,每条生产线旁边都具有一个小型的仓库(线边仓),但现在线边仓的入库,出库(领料)并未有任何系统进行管理。为了进一步精细化管理,企业A决定开发一套系统对线边仓进行管理。
线边仓往往较小,类似一个10-20平方小房间里面有多个小型货架,每个货架有多层,每层有多个隔间用于存放生产所需的原材料。
线边仓的原材料都是从大仓库中整箱拆箱而来,所以线边仓的最小存储粒度是原材料的编码和数量。
根据需求,显然我们可以得出存储位置为一个实体。原材料是另一个实体。某个货架位置上有多个入库和出库的事件。根据这个简单的概念我们可以设计出数据模型如下:
Locations:表示货架的位置。
Products:表示原材料。
Location_Events:表示某个货架位置上的入库、出库事件。(从数据库建模角度看是一个多对多关系)
入库事件:实际生产中,入库可能来自于总仓,也可能是由产线退回,也可能是其他仓库调拨。所以为了对这个需求进行建模我们单独设置了EventType字段进行记录。
出库事件:同上,出库也包括一般正常领料出库,也包括调拨出库,和退回出库。这个需求也可以用EventType字段进行表示。
Qty:入库数量,出库数量。入库记为正数,出库记为负数。
解决方案1:直接根据Location_Events中的记录按位置和产品编码进行分组,汇总Quantity字段即可得到每个产品在每个位置上的库存数量。
select LocationCode,p.ProductCode,SUM(Quantity)as Quantity from Location_Events as e
innerjoin Products as p on p.ProductCode=e.ProductCodegroupby LocationCode,p.ProductCode这个方案简单直接,但是每次查看库存都执行一次查询显然在性能上有些浪费。所以可以采用方案2.
解决方案2:使用物化视图,把上面的查询变成一个物化视图,在SQLServer中体现为索引视图。索引视图其实就是将视图变成一个带有聚集索引的实体表,当对基础表进行INSERT,UPDATE,DELETE操作时会自动更新索引视图。
--Set the options to support indexed views.
SET NUMERIC_ROUNDABORT OFF;SET ANSI_PADDING, ANSI_WARNINGS, CONCAT__YIELDS_, ARITHABORT, QUOTED_IDENTIFIER, ANSI_S ON;--Createviewwith SCHEMABINDING.IF OBJECT_ID('dbo.Inventory''view')ISNOT DROPVIEW dbo.Inventory;GOCREATEVIEW dbo.Inventory WITH SCHEMABINDINGASselect LocationCode,p.ProductCode,SUM(Quantity)as Quantity,COUNT_BIG(*)asCountfrom dbo.Location_Eventsas einnerjoin dbo.Productsas p on p.ProductCode=e.ProductCodegroupby LocationCode,p.ProductCode,p.ProductNameGO--Create an indexon the view.CREATEUNIQUE CLUSTERED INDEX IDX_V1 ON dbo.Inventory( LocationCode, ProductCode);GO解决方案3:物化视图手动实现。创建一个实体表(包含LocationCode、ProductCode、Quantity),在Location_Events表中创建一个触发器,当表中数据新增时,触发器捕获新增的数据,如果是入库操作则根据LocationCode和ProductCode作为唯一键去更新Quantity数量。(这里又存在上篇写过的一个问题,如果存在则更新,如果不存在则插入。)
性能问题:以上三种方案大并发情形下都存在性能问题。尤其是在SQLServer,MySQL这种默认为提交读事务隔离级别下会存在读写相互阻塞的情况。解决方案是修改数据库的默认隔离级别为行版本控制,SQLServer为READ_COMMITTED_SNAPSHOT。MySQL为MVCC。如果库存的实时性要要求不高,那完全可以采用定时更新物化视图的方案,例如每天夜间进行库存的刷新。
其实库存粒度就是库位上存放的最小库存单位。
对于线边仓主要是存放散装原材料,例如:一个零件,一个刀具,一盒锡球。
对于大仓库主要是存放整包原材料,例如:一包100个单位的零件,一包10个单位的刀具,一包2000个单位的锡球。
例如:供应商A生产的切割刀型号为N001,批次为20250102,12个一包,那么这把刀具的库存唯一编码为:A-N001-20250102-12。型号、批次、包装规格的不同都会形成不同的编码。这个编码为库存而存在。
仓库类型
存放货品类型
编码样例
总仓(Warehouse)
原材料整包存放
A-N001-20250102-12
散件存放
其实零售业SKU的概念很好解释了这个编码。
这里引用亚马逊对SKU的解释
SKU 是库存单位 (stock-keeping unit) 的缩写,代表零售商库存中的一种商品。SKU 编码是一个包含字母和数字的唯一标识符,用于说明商品的重要特征,例如品牌、颜色和尺寸。这些编码由零售商生成,每个企业的 SKU 都是独一无二的。这意味着企业的每种商品都应该有一个不同的编码,并且不同零售商对同一商品的编码应该是不同的。供应商负责生成,供应商将原材料打包好,每个包装上按照材料类型,型号,批号,包装规格生成一个唯一码贴在外包装上。但有时候供应商的唯一码不能满足企业的要求。
企业自己生成,企业自己维护各种原材料类型,并将供应商提供的整箱包装进行编码。这种编码更符合企业要求,但必须有专人进行维护。(SAP系统中都有编码工具可以帮助生成这类编码)
企业线边仓往往都是零散入库和出库,企业往往直接使用供应商的编码作为最小库存编码。
最终的编码表可能类似下面这样
扩展讨论
有时候货架上存放的东西并不是原材料,而是半成品。无论货架上存放的是什么,总归有一个编码对应到货架编码上。这个编码可以是企业业务使用的例如Lot编码之类,也可以是成品仓中最后的发货所用的编码。
库位和物品是多对多的关系。入库和出库是这种关系的体现。无论入库出库业务要求多么复杂,都是不同事件而已。
最小库存单位是非常重要的,要准确的定义库存编码。(例如:由材料类型、型号、批次、生产商、包装规格来确定)
并发小则采用最简单的物化视图方案,实现简单查询效率高。
并发大则要考虑启用数据库行版本特性或延迟更新库存。
来源:opendotnet