☰
库存管理系统设计:从数据库一致性到VB+Access避坑
2026/10/31 18:18:15 网站建设 项目流程

简介:面向企业信息化学习、课程设计与毕业设计场景的库存管理系统设计方案文档,适合需要理解管理信息系统(MIS)与数据库应用开发流程的读者参考。压缩包内仅含1个doc文件,大小约604KB,以完整设计文档形式组织,便于集中阅读和资料归档。已有127人学习下载,具备一定的参考热度。内容围绕库存管理系统的需求分析、模块划分、数据库设计、应用程序实现与设计总结展开,涉及入库、出库、库存查询、库存预警等功能,并讲解SQL语言、Visual Basic编程环境与Access 2000数据库组件的应用。读者可从中获取系统设计思路、数据表与关系模型设计方法、业务逻辑处理方式及源代码组织参考,用于梳理课程设计框架或完善库存管理方案。

1. 库存管理系统:真正难的不是增删改查,而是同一笔货在两处算得不一样

一份库存管理系统的设计方案摆在面前,最容易看走眼的地方是:这类系统的技术难度从来不在功能清单上。入库、出库、查询、报表,四个动作一句话就能说完;真正把系统做崩的,往往是同一笔货在入库明细、出库流水、库存余量表里算出了三个不同的数字。库存管理系统的本质不是「记录货品」,而是在一张不断被并发改写的账本上维持一致性——这个判断直接决定了表怎么拆、权限放在哪一层、界面上显示的数字从哪里取。搞懂这套设计为什么这么写,比照着它把代码敲一遍有用得多。

2. 数据库设计五步法:从 DFD 到物理表,中间到底发生了什么

库存系统的骨架是数据库,方案里反复强调这一点,但「数据库设计」四个字底下藏着的是一套有严格先后次序的方法:需求分析、概念设计、逻辑设计、物理设计、加载测试。这五步不是为了把流程书写厚,而是每一层的输出恰好是下一层的输入——需求分析产出数据流图和数据字典,数据字典喂给 E-R 图,E-R 图定出表名与属性,属性再定出字段类型与宽度。跳过任何一层,后面的返工成本都会成倍放大。

2.1 需求怎么变成实体:从单证流到数据字典

仓库里真实存在的东西是单证。一张入库单上印着单号、日期、供应商、经办人、货品编号、数量、单价、金额、仓库号。需求分析要做的第一件事,就是把这些印在纸上的字段逐条抄成数据项,再给每一项补上类型、宽度、取值范围、来源和校验规则,最后汇总成数据字典。这一步看着枯燥,但它决定了后面建表时字段是VARCHAR(12)还是VARCHAR(20),也决定了「数量必须为正整数」这条约束是写在代码里还是写在数据库里。

数据项类型宽度取值范围校验规则
ProdCode文本12非空必须存在于货品表
Qty长整型41 ~ 999999入库为正,出库记负
Price货币8≥ 0保留 2 位小数
InDate日期81990-01-01 起不得晚于当前日期

有了数据字典再画数据流图(DFD),事情就顺了:仓库管理员是一个外部实体,「入库登记」「出库登记」「盘点」是三个加工,货品表、入库流水表、供应商表是数据存储,加工之间用带方向的数据流连起来。常见的实现思路是:一个加工对应一个窗体,一条数据流对应一次写库动作,这样 DFD 和后面的模块划分能对上号,评审的时候两边一起看,谁对不上谁就有问题。

这里有个容易被忽略的取舍。「库存余量」这个数字,既可以由流水表 SUM 出来,也可以在货品表里单独存一个字段。方案里选了后者——货品信息表带一个「数量」字段。理由是查询快:列表页不需要每次对几万行流水做聚合。代价是同一份信息存了两份,任何一次入库出库都必须保证两边同步,这个代价会在后面反复出现。

2.2 一张大表为什么必须拆:更新异常与函数依赖

把入库单上的所有字段塞进一张表,是新手最容易踩的坑,因为它看起来最直观。假设设计出这样一张表 R(入库单号,入库日期,供应商编号,供应商名称,供应商电话,货品编号,货品名称,规格,单价,数量)。这张表里其实混了两个主题:供应商和入库流水。混在一起会同时出现三类异常,而且这三类异常在真实系统里都会发生。

更新异常:某个供应商换了联系电话,因为一个供应商供过很多批货,这条记录会出现很多行,改的时候漏掉一行,报表上就出现两个电话。插入异常:新签了一个供应商但还没进货,没有入库单号,就没法把供应商信息录进去,只能干等着。删除异常:某个供应商只供过一批货,把这批货的入库记录删掉,供应商信息跟着一起没了,下次还得重新录。这三类异常的共同根源是函数依赖错位——供应商名称只依赖供应商编号,不依赖入库单号,把它塞进以入库单号为主键的表里,就形成了部分依赖。

规范的解法是拆表,让每个非主属性只依赖于该表的主键。落到库存系统上是四张表:供应商表、货品表、入库主表、入库明细表。

-- 供应商表:一个供应商一行,改名改电话只改这里 CREATE TABLE Supplier ( SupCode VARCHAR(8) NOT NULL PRIMARY KEY, SupName VARCHAR(60) NOT NULL, Phone VARCHAR(20), Address VARCHAR(120) ); -- 货品表:Stock 是冗余字段,靠事务与流水表保持同步 CREATE TABLE Product ( ProdCode VARCHAR(12) NOT NULL PRIMARY KEY, ProdName VARCHAR(60) NOT NULL, Category VARCHAR(4) NOT NULL, Unit VARCHAR(8), SafeStock INTEGER DEFAULT 0, -- 安全库存,低于此值触发预警 Stock INTEGER DEFAULT 0 -- 当前余量,冗余存储 ); -- 入库主表:一次入库一行,只放单据级信息 CREATE TABLE InStock ( InNo VARCHAR(16) NOT NULL PRIMARY KEY, InDate DATETIME NOT NULL, SupCode VARCHAR(8) NOT NULL, OperID VARCHAR(8) NOT NULL -- 经办人,对应操作员表 ); -- 入库明细表:一张单可以有多行货品,复合主键防重复录入 CREATE TABLE InStockDetail ( InNo VARCHAR(16) NOT NULL, ProdCode VARCHAR(12) NOT NULL, Qty INTEGER NOT NULL, Price CURRENCY NOT NULL, PRIMARY KEY (InNo, ProdCode) );

这张图里有两个设计点值得单独说。第一,明细表用 (InNo, ProdCode) 做复合主键,含义是「同一张入库单里同一个货品只能出现一行」,从数据库层面堵住了重复录入。第二,单价放在明细表而不是货品表,因为同一件货不同批次的进价会变,放货品表就只能保留最后一次进价,历史成本再也算不出来——做成本核算的时候这条会要命。货品表里的Stock字段则是前面提到的冗余,它和明细汇总出来的是同一份信息,靠事务保证两边一致。

2.3 索引和元数据:性能背后的隐形账本

数据库是自描述的,这句话的意思是它自己存了一份关于自己的说明。表里有多少列、哪列是主键、每列什么类型、建了哪些索引,这些信息被 DBMS 存在系统表里,和用户数据一样可以用查询语句读出来。这份「关于数据的数据」就是元数据。用表来存元数据不只是 DBMS 自己方便,对使用者也方便——查系统表和查业务表用的是同一套 SQL,不需要另学一门接口。

索引属于第三类数据,有时候叫开销数据。它不改变业务语义,只改变访问速度。方案里举了一个很具体的场景:货品数据在磁盘上按货品编号递增排列,现在要打一张按货品名称排序的报表。没有索引,只能把整张表读出来在内存里排序,表一大就非常费时;在货品名称上建一个索引,索引条目本身按名称排好序,沿着索引走就能按顺序取行,排序这一步被省掉了。

-- 报表按名称排序:无索引时是「全表读 + 内存排序」 SELECT ProdCode, ProdName, Stock FROM Product ORDER BY ProdName; -- 在名称列建索引后,优化器可沿索引顺序直接取行 CREATE INDEX idx_product_name ON Product (ProdName); -- 低基数列(类别只有几个取值)单建索引收益有限, -- 但配合名称一起用于覆盖查询时仍然有价值 CREATE INDEX idx_product_cat_name ON Product (Category, ProdName);

索引不是免费的。货品表每改一行,这张表上的每个索引都要跟着改一次,写操作被放大成「一次写 + N 次索引维护」。所以索引应该建在真正会被 WHERE、ORDER BY、JOIN 命中的列上,而不是「看着重要就建一个」。库存系统的查询热点其实很集中:按货品编号精确找一件货、按类别列出某一类货、按日期区间拉一段流水、按供应商汇总进货量。把这四条查询写出来,索引清单基本就定了,剩下的都是锦上添花。

3. VB 的事件驱动与数据绑定:界面上的数字是从哪来的

界面上的数字有一个很具体的来源:一个叫 Recordset 的游标对象。理解这一点,是理解 VB 数据库程序为什么长成那个样子的钥匙。VB 把「界面」和「数据」之间的关系做成了属性赋值,一行代码就能让文本框跟着记录指针走,这种便利背后是一整套绑定机制在起作用。

3.1 事件驱动:控制权在谁手里

过程式程序有一个明确的起点,从第一行往下跑,跑到最后一行结束。VB 程序不是这样。程序启动后进入一个消息循环等待用户动作,用户点了某个按钮,系统才把控制权交给这个按钮对应的那段代码,代码跑完,控制权交还回去。这就是事件驱动。好处是程序员不用写「怎么判断按钮被按了」这类样板代码,只需要回答「按钮被按了之后做什么」;代价是程序的执行顺序不再由代码文本决定,而是由用户的操作顺序决定。

库存系统里最典型的翻车场景就出在这里:用户在入库窗体上连点两次「确认」,事件过程被调用了两次,同一批货入了两次库。防御的办法不是教育用户别点快,而是在事件过程开头就把按钮禁用,处理完再恢复。

' 入库窗体的「确认入库」按钮:一次点击 = 一个事务边界 Private Sub cmdInStock_Click() Dim ws As Workspace Set ws = DBEngine.Workspaces(0) cmdInStock.Enabled = False ' 防连点,进入即禁用 If Not ValidateInput() Then ' 校验数量、单价、货品是否存在 cmdInStock.Enabled = True Exit Sub End If On Error GoTo Rollback ws.BeginTrans ' 开事务 InsertInStockMaster ' 1. 写入库主表 InsertInStockDetail ' 2. 写明细 UpdateProductStock ' 3. 货品表 Stock = Stock + Qty ws.CommitTrans ' 三步全成功才提交 cmdInStock.Enabled = True Exit Sub Rollback: ws.Rollback ' 任一步失败,全部回滚 MsgBox "入库失败:" & Err.Description, vbCritical cmdInStock.Enabled = True End Sub

BeginTrans/CommitTrans/Rollback是 Workspace 对象上的事务方法,语义是要么全做要么全不做。放进事务的三步是写入库主表、写入库明细、回写货品表余量,这三步少任何一步,账本都是歪的。On Error GoTo Rollback是 VB6 里的错误跳转,错误发生时直接落到回滚标签,比在每一行后面判断返回值可靠得多。

3.2 数据觉察控件:DataSource 与 DataField 的绑定链

Data 控件是 VB 里最基础的数据控件,它的特殊之处在于它自己什么都不显示。它做的事是持有一个 Recordset,并提供移动记录、增删改的方法。真正把数据显示出来的是「数据觉察控件」:内部的 TextBox、Label、CheckBox、Image、ListBox、ComboBox 都是,外加 DataGrid、DataList、DataCombo、MSHFlexGrid 这些批量控件。

绑定的方式很朴素:把控件的DataSource属性指向一个 Data 控件,把DataField属性指向记录集里的一个字段名。绑好之后,Data 控件当前记录一变,所有绑在同一字段上的控件内容自动跟着刷新,一行代码都不用写。

' 窗体加载:把 Data 控件接到货品表,再把三个文本框绑上去 Private Sub Form_Load() Data1.DatabaseName = App.Path & "\stock.mdb" Data1.RecordSource = "SELECT ProdCode, ProdName, Stock FROM Product" Data1.Refresh ' 触发 Recordset 重建 txtCode.DataSource = Data1 ' 先指数据源 txtCode.DataField = "ProdCode" ' 再指字段名 txtName.DataSource = Data1 txtName.DataField = "ProdName" txtStock.DataSource = Data1 txtStock.DataField = "Stock" End Sub

DatabaseName指定 .mdb 文件路径,RecordSource可以是表名、已保存的查询名,也可以直接是一句 SELECT。Refresh会让 Data 控件丢掉旧的 Recordset 重新建一个,这一步很关键——RecordSource一改,原来的 Recordset 就被销毁,之前保存的书签和当前记录位置全部失效。所以顺序永远是先设RecordSource、再Refresh、最后才调用MoveFirst或Find。反过来写,程序不一定报错,但定位会莫名其妙地偏,这种问题排查起来相当费时间。

记录集的移动靠MoveFirst、MoveNext、MovePrevious、MoveLast和Find系列方法。Find是在当前记录集里按条件定位,不是重新查询数据库,所以它只能找到已经在记录集里的行——这一点和 SQL 的 WHERE 是两回事,混淆了会写出「明明数据库里有却查不到」的代码。

DataGrid的用法稍有不同,把它的DataSource指向 ADO Data 控件,整个结果集会以表格形式自动填充。它本质上是一个固定的列集合,行数随记录集变化,适合做列表页和批量浏览。DataList和DataCombo则适合做关联字段的录入:由一个数据源填充下拉项,选中后把值写回另一个数据源的关联字段,典型场景就是入库单上的供应商选择框。

3.3 ADO 与 Data 控件:两代记录集接口的差别

Data 控件属于 DAO 那一代,认的是DatabaseName加RecordSource,指向一个本地 .mdb 文件。ADO Data 控件是后来的一代,用ConnectionString建立连接,底层走 OLE DB,能连的东西更多。两者在界面上的用法几乎一样,都是给数据觉察控件当数据源,差别在连接方式和事务接口上。

维度Data 控件(DAO)ADO Data 控件
连接方式DatabaseName + RecordSourceConnectionString
记录集类型Recordset(表 / 动态集 / 快照)ADODB.Recordset(支持客户端游标)
事务接口Workspace.BeginTransConnection.BeginTrans
典型场景本地 .mdb 单机或小局域网需要切换数据源或离线操作

一个实际的选择建议:数据就在本地 .mdb 文件里,Data 控件够用而且简单;将来可能换数据源,或者需要把数据一次性拉到内存、断开连接再操作、最后批量提交,那就用 ADO——它的客户端游标正是为这种场景准备的,在网络不稳的环境里价值很高。

4. SQL 落到 VB 代码里:QueryDef 与 RecordSource 两条记录集构造路径

SQL 在 VB 程序里出现的位置只有两个:一是先写成一个命名的 QueryDef 存起来,二是拼成字符串直接赋给 Data 控件的RecordSource。这两条路径看起来只是写法不同,实际上决定了后面维护的难度。

4.1 非过程化的代价与红利

SQL 最容易被低估的特性是它一次处理一个集合,而不是一条记录。SELECT 的输入是一张表,输出也是一张表,中间没有循环。这个设计带来两个直接红利:写得少,而且同一条语句在数据量变化之后依然成立。集合特性还允许一条语句的输出当作另一条语句的输入,子查询和视图就是这么来的。

代价也很实在:用户不被允许指定数据怎么取。走不走索引、走哪个索引、先做连接还是先做过滤,全由 DBMS 里的查询优化器决定。方案里那句话说得挺准确——优化器知道存在什么索引、在哪儿用索引合适,而用户不需要知道表有没有索引。这句话的另一面是:当优化器选错执行计划时,SQL 层面能做的事很有限,只能回头去改索引或者改写查询。

SQL 语句按用途分三组:数据操作语言(SELECT / INSERT / UPDATE / DELETE)、数据定义语言(CREATE / DROP / ALTER)、数据控制语言(GRANT / REVOKE)。库存系统日常用到的九成是前三条加上 SELECT。另外还有一类域聚合函数值得记住,DSum、DCount、DLookup、DMax、DMin、DAvg这些,可以在 VB 代码里直接对整张表做统计而不必显式打开记录集。

' 统计指定货品的累计入库数量 ' domain 是表名或查询名,criteria 是 WHERE 子句的内容(不带 WHERE 关键字) Dim totalQty As Long totalQty = DSum("[Qty]", "InStockDetail", "ProdCode = 'P0001'")

DSum的第一个参数是要求和的表达式,第二个是数据域,第三个是筛选条件。这类函数写起来快,但每调用一次就发一次查询,放进循环里就是灾难——一万次循环等于一万次查询,正确但慢得离谱。

4.2 两条构造记录集的路径

路径一是把 SQL 存成命名查询,之后按名字复用。

Dim mydb As Database Dim myq As QueryDef Set mydb = OpenDatabase(App.Path & "\stock.mdb") Set myq = mydb.CreateQueryDef("qryInMonth") ' 建一个命名查询 myq.SQL = "SELECT * FROM InStock " & _ "WHERE Year(InDate)=2024 AND Month(InDate)=3" ' 之后按名字打开,不必重写 SQL Set rs = mydb.OpenRecordset("qryInMonth")

OpenDatabase打开数据库文件,CreateQueryDef创建一个持久化的查询对象,SQL属性用来写入语句。好处是复用和集中修改:查询条件变了只改一处,所有引用这个名字的地方自动跟着变。方案里给出的原例是用字符串截取年份月份来拼条件,这种写法很脆弱——日期格式一变,截出来的就不是年月。直接对日期字段用Year()和Month()函数稳得多。

路径二是拼一句带聚合的 SQL 直接赋给RecordSource,适合条件由用户当场输入的查询。

' 按货品汇总 2024 年 3 月的入库数量 Data1.RecordSource = "SELECT D.ProdCode, SUM(D.Qty) AS TotalQty " & _ "FROM InStockDetail D, InStock M " & _ "WHERE D.InNo = M.InNo " & _ "AND M.InDate BETWEEN #2024-03-01# AND #2024-03-31# " & _ "GROUP BY D.ProdCode" Data1.Refresh

AS TotalQty给聚合列起别名,不起名的话这一列在记录集里叫SumOfQty,取值时容易写错。GROUP BY按货品分组,配合 SUM 得到每件货的汇总。日期字面量在 Access SQL 里用#包裹,这是 Access 特有的写法,换成'2024-03-01'有时能跑有时不能,取决于区域设置——这就是玄学出现的地方。Refresh之后记录集重建,Move和Find才能正常使用。

两条路径的分工其实很清楚:需要反复用、条件固定的查询,做成 QueryDef;需要按用户输入动态变化的查询,拼字符串赋给RecordSource。前者稳定,后者灵活。

4.3 字符串拼 SQL 的坑:引号、注入和类型

把文本框内容直接拼进 SQL,第一个撞上的问题是引号不匹配。

' 反例:供应商名称里带一个单引号(比如某英文名带撇号),这句直接语法错误 sql = "SELECT * FROM Supplier WHERE SupName='" & txtName.Text & "'" ' 修正:单引号双写转义,再包上外层引号 Function Q(s As String) As String Q = "'" & Replace(s, "'", "''") & "'" End Function sql = "SELECT * FROM Supplier WHERE SupName=" & Q(txtName.Text)

Replace(s, "'", "''")把每一个单引号替换成两个,这样值里的单引号在 SQL 解析器看来就是普通字符而不是字符串结束符。数值型字段不能加引号包裹,日期型要用#包裹,这三种类型混着拼最容易出错。更彻底的做法是用 ADO 的 Command 对象加参数集合,把 SQL 和值彻底分开传。

Dim cmd As New ADODB.Command cmd.ActiveConnection = conn cmd.CommandText = "SELECT * FROM Supplier WHERE SupName = ?" ' ? 是占位符 cmd.Parameters.Append cmd.CreateParameter("name", adVarWChar, adParamInput, 60, txtName.Text) Set rs = cmd.Execute

CreateParameter的五个参数依次是名称、数据类型、参数方向、长度、值。参数化之后,值永远不会被当成 SQL 语法的一部分解析,引号问题自然消失。这套写法在当年不算主流,但它才是正确的方向。

5. 避坑与排查:库存系统运行中最容易翻车的五处

下面这五条都是从这类系统的实际运行里长出来的,现象看着像玄学,拆开看原因都很朴素。

5.1 入库出库都成功,库存余量却对不上

现象:流水表里入库 100、出库 30,货品表Stock显示 80,差 10。

原因:Stock是冗余字段,靠每次操作Stock = Stock + Qty回写。并发时两个用户读到同一个旧值再各自加,后写的覆盖先写的;或者事务中途失败只回滚了流水没回滚余量。

解决:入库、出库、回写三步必须放进同一个事务;再加一道「后悔药」——定期用流水表重算一遍余量做对账,发现不一致就修正并记录。更彻底的做法是不存冗余字段,查询时聚合,代价是列表页变慢。

5.2 第二个人一打开程序就报文件被占用

现象:单人用一切正常,第二台机器打开 .mdb 就报错或者变成只读。

原因:Access 是文件级共享,锁的粒度很粗,写操作会对整个数据页加锁;另外OpenDatabase的打开模式如果默认成独占,第二个人根本进不来。

解决:打开时显式指定共享模式并配上记录锁策略;数据文件放在共享目录且所有人有写权限;用户数超过十个就该考虑把窗体文件和数据文件拆开,前端留在本地、后端放服务器,减少锁冲突。

5.3 报表越跑越慢

现象:货品只有几百条时秒开,涨到几千条后按名称排序的报表要等十几秒。

原因:ProdName上没有索引,每次都是全表读进内存再排序;数据量上来后耗时线性增长,再往后还会撞上内存瓶颈。

解决:在排序列和过滤列上建索引;报表不要SELECT *,只取需要的列;尽量先用 WHERE 把范围缩小再排序,让排序作用在小结果集上。

5.4 权限只在前端做了

现象:登录框能拦住普通用户,但把 .mdb 文件拷走,用任何工具打开都能看到全部数据。

原因:权限判断写在窗体加载事件里,数据库文件本身没设密码、没做用户级安全。前端校验只防君子不防小人。

解决:给数据库文件加密;关键操作在写库前再校验一次操作员身份;加一张操作日志表,记录谁在什么时候改了什么,出了问题能追。这些措施不能做到绝对安全,但能把门槛从「零」抬到「需要一点手段」。

5.5 金额字段用了单精度浮点

现象:单价 0.1、数量 3,金额显示成 0.30000000000000004。

原因:Single 和 Double 是二进制浮点,无法精确表示十进制小数,累加之后误差还会放大。

解决:金额字段用定点类型(Access 里的Currency,小数位固定四位),运算前先转定点,显示时统一格式化到两位。这个坑当年坑过不少人,血泪经验就是一句话:凡是要对账的数字,别碰浮点。

6. 设计取舍:VB 加 Access 这套组合的边界在哪里

把整套方案拆完,会发现它的每一个设计点都是取舍,而不是「最优解」。

取舍点选了什么放弃了什么
存储单文件 Access并发写入能力
数据访问数据绑定控件分层与可测试性
业务逻辑事件过程里直接写复用与单元测试
一致性冗余库存字段额外的对账成本
SQL 构造字符串拼接类型安全与注入防护

这套组合的合理边界很清楚:单机或十人以内的小局域网、数据量在十万行以内、开发周期短、没有专职运维。在这个范围内,可视化拖拽加属性绑定能把开发效率拉到很高,一个熟悉 VB 的人几周就能交付一个能用的系统。一旦超出这个范围——并发用户上去了、数据量上去了、需要远程访问——文件级锁、单文件存储、字符串拼 SQL 这些当年的优点会同时变成瓶颈,而且是一起发作。

判断该不该换,我一般看三个信号:操作日志里出现同一条记录的并发修改冲突、报表响应时间开始随数据量线性恶化、有人开始问「能不能在手机上查」。三个里出现任意两个,就说明这套架构到顶了。

验证一致性有个便宜的办法,不需要写复杂的测试框架:写一个对账查询,把流水表按货品汇总,和货品表的Stock逐条比对,把不相等的行选出来。

-- 对账:找出余量与流水汇总不一致的货品 SELECT P.ProdCode, P.Stock AS StoredQty, Nz((SELECT SUM(D.Qty) FROM InStockDetail D WHERE D.ProdCode = P.ProdCode), 0) - Nz((SELECT SUM(O.Qty) FROM OutStockDetail O WHERE O.ProdCode = P.ProdCode), 0) AS FlowQty FROM Product P WHERE P.Stock <> Nz((SELECT SUM(D.Qty) FROM InStockDetail D WHERE D.ProdCode = P.ProdCode), 0) - Nz((SELECT SUM(O.Qty) FROM OutStockDetail O WHERE O.ProdCode = P.ProdCode), 0);

Nz把 NULL 转成 0,避免新货品没有任何流水时整行被 NULL 吞掉。这条查询跑出来是空集,说明账本平了;跑出来有行,那几行就是需要人工核对的差异。把它挂成每天定时任务,比事后拍脑袋找原因有效得多。

从那以后我每次设计库存类表结构,都强制先把「流水表能不能独立重算出库存表」这句话写进设计文档的第一行,再动手建表。写不出来,说明这个冗余字段不该存在。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询