药品管理系统数据库设计:从数据字典到触发器实践
2026/9/18 19:48:26 网站建设 项目流程

简介:这是一份数据库课程设计报告,以药品管理系统为背景,基于SQL Server平台,面向计算机、数据库相关专业学生及需要完成课程设计的开发者。资源为单个doc文档,约1.07MB,内容完整覆盖需求分析、概念设计、逻辑设计、物理设计到数据库实施等关键环节,包含业务流程图、数据字典、分E-R图与全局E-R图等核心内容,可直接作为课程设计报告撰写与数据库建模的参考范例。文档详细展示了从业务梳理到数据库表结构设计、索引策略及实施操作的完整流程,帮助读者理解如何将数据库理论应用于实际管理系统开发。已有1037人学习使用,适合正在筹备数据库课程设计或希望掌握药品信息管理项目设计思路的学习者。

1. 从手工台账到 SQL Server:药品管理系统为什么值得拆开看

药品管理系统是数据库课程设计里的常客,但大部分交上来的作业只讲建表,很少讲清楚“为什么这么建”。这份报告的特殊之处在于它把整个数据库设计流程走了一遍:从需求分析阶段的业务流程图和数据字典,到概念设计阶段的 E-R 图,再到逻辑设计、物理设计和实施阶段,最后落在一组视图、触发器、存储过程上。业务本身不复杂——药品、制药商、买药人、柜台四类主数据,加上订退、售退、存储三类流水,核心就一句话:所有进出货操作都要让库存表跟着变。适合第一次做完整数据库项目的学生对照流程,也适合准备计算机三级数据库或数据库面试的人拿它当案例比对。如果你手头正好在做类似的进销存系统,这份报告的 25 个数据项和 8 张表可以直接拿来做底稿。

2. 需求分析与概念设计:数据字典、业务流程图与 E-R 图建模

2.1 业务流程图决定系统边界

报告在需求分析阶段做了实地调查,结论是当时药店还在用手工台账:进货记录、售货记录、库存清点分开写在几个本子里,药卖出去以后库存能不能对上全凭经验。这里第一个值得学习的地方是它没有直接开始画表,而是先画业务流程图,把系统边界定下来。

三个核心业务分别是药品购进、药品出售、药品存储。购进业务里购药人员按需求单从制药商进货,合格药品入库,不合格的退回;存储业务里库存管理员处理出库入库并修改库存信息,缺货时给购药人员递缺货单;售药业务里买药人拿取药单到售药处,确认后售出或退回,取药单再流转到库存管理员。这三条线下去了,系统的处理对象就清楚了:药品、制药商、买药人、柜台,以及围绕它们发生的订退、售退、存储三类流水。

2.2 数据字典:25 个数据项如何收敛成 8 个数据结构

数据字典是需求分析阶段的输出物,报告里一共定义了 25 个数据项。初学者最容易在这里失控,把字典写成一张超大字段表。这份报告的处理方式值得借鉴:先按实体归类,再给每个数据项标上存储结构、别名和取值约束。节选几个如下。

数据项编号数据项名存储结构取值约束
DI-1Dno 药品编号char(5)主键
DI-6price1 药品进价float大于零
DI-10Page 年龄int1-120
DI-11Psex 性别char(2)男,女
DI-20Quantity 药品数量int大于零
DI-22Supply 订退方式char(4)订购,退订

这个表的规律性很强:编号类字段就是天然的主键候选;数量、价格类字段要加 CHECK 约束;方式类字段用定长字符存枚举值。把 25 个数据项按语义归拢后,得到 8 个数据结构:Drug、Maker、Patient、Storage 四张主表,Order_Back(订退)、Buy_Back(售退)、Stored(存储)、User(用户)。主表记录“谁存在”,流水表记录“发生了什么”,这个划分直接决定了后面关系的粒度。

这里有一个在数据库原理课程里反复强调但很容易被跳过的点:数据结构不是把字段随便分堆,而是按主键粒度分的。Drug 的主键是 Dno,Patient 的主键是 Pno,那它们之间发生的“买药”行为就不能塞进任何一张主表,必须单独建一张以 (Pno, Dno) 为主键的中间表,这就是后面 DBuy、DOrder 的由来。

2.3 E-R 图到关系模式:三组多对多关系的拆解

概念设计阶段先从各子系统的分 E-R 图入手,再合并成全局 E-R 图。报告给出了三个分 E-R 图:药品存储、药品售出、药品退订。合并时要做属性冲突检测,比如“处理时间 Time_SD”在售出、退回、订购、退订四个图里都出现,但语义不同,不能简单合并成一个属性,而要落到各自的业务表里。

药品和制药商是多对多,一个制药商生产多种药品,一种药品也可能由多个制药商供货;药品和买药人是多对多;药品和柜台也是多对多,一个药品可以放在多个柜台,一个柜台放多种药品。多对多关系在关系模式里必须拆成两个一对多,所以 DOrder、OBack、DBuy、BBack、Stored 这些中间表是必然出现的。拆表的代价是查询多一次 JOIN,换来的是订单、售退、库存各自独立成行,不会出现“一条药品记录里塞多个制药商”这种反范式设计。

3. 逻辑设计与物理设计:主外键、CHECK 约束与复合索引的取舍

3.1 建表 SQL:先看三张代表性表

逻辑设计阶段要把 E-R 图转成具体的表结构。报告原文里写的是 SQL Server 2000 语法,部分细节直接抄会有点问题,这里给出调整后的版本:

create database DrugStore; go use DrugStore; go create table Drug ( Dno char(5) primary key, Dname char(20) not null, Dclass char(8), Dguige char(10), Dbrand char(10), price1 decimal(10, 2) check (price1 > 0), price2 decimal(10, 2) check (price2 > 0) ); create table Maker ( Mno char(5) primary key, Mname char(30) not null, Mplace char(10) not null, Mphone char(20) not null ); create table DOrder ( Mno char(5) not null, Dno char(5) not null, Quantity int not null, Time_SD datetime, Supply char(4) not null, primary key (Mno, Dno), foreign key (Mno) references Maker(Mno), foreign key (Dno) references Drug(Dno), check (Quantity > 0), check (Supply = '订购') );

代码说明:原文把价格定义成 float,这里换成 decimal(10,2)。float 是浮点存储,0.1+0.2 这种运算在二进制下本身就是不精确的,做进价售价这种货币计算一定要用定点数。另外原文用户表里 ID 用了 number(4),这是 Oracle 的写法,SQL Server 里不认,统一改成 int。char(5) 在 SQL Server 中是定长字符,存 'D001' 实际会补成 'D001 ',比较时数据库会忽略尾部空格,但应用层取出字符串时可能带空格,这个坑在对接 Java、C# 时经常出现,作业里可以用,真实项目建议直接用 varchar 或 nchar。

3.2 CHECK 约束:为什么订购和退订要拆成两张表

原设计里 DOrder 和 OBack 结构几乎一样,都是 (Mno, Dno, Quantity, Time_SD, Supply),但 DOrder 的 CHECK 约束是 Supply='订购',OBack 的 CHECK 是 Supply='退订',DBuy 和 BBack 也是这样拆的。这个设计是刻意的:把业务方向用 CHECK 钉死在表上,插入时如果传错值,数据库直接拒绝,不靠应用层判断。坏处是查“某个药品全年的订购加退订总量”时要 UNION 两张表,查询语句变长,但换来的是数据语义的强约束。

这个取舍在课程设计评分时是加分项。很多人会把 Supply 设计成一张表里可取任意值,再用 where 条件区分,那样也不是不行,但 CHECK 约束一拆,数据字典里的“订退方式”就和表边界对齐了,可读性更强。我一般会保留这种拆分,同时在视图层把两张表 UNION 起来暴露给上层,兼顾约束和查询便利。

3.3 复合索引的顺序:按药品查库存还是按柜台查库存

物理设计阶段选择了索引存取方法,关键看索引列顺序。Stored 表主键是 (Lno, Dno),这意味着默认聚簇索引按柜台编号排列,同一个柜台的药品在物理上相邻,适合“按柜台清点库存”的场景。而业务里更常见的是“查某个药品还剩多少”,也就是按 Dno 走,这时候主键索引帮不上忙。所以报告给 Stored 额外建了一个 (Dno, Lno) 的唯一索引,这是对的。

索引键列顺序覆盖的查询
PK_Stored(Lno, Dno)按柜台查药品、柜台盘点
DLno(Dno, Lno)按药品查库存、缺货统计

这里要注意复合索引列顺序不能随便换。(Dno, Lno) 和 (Lno, Dno) 是两棵完全不同的 B+ 树,前者先按药品编号排,后者先按柜台编号排。数据库只能利用最左前缀,所以建索引之前先想清楚查询条件里哪个列最常出现。另外一个被忽略的点是 Time_SD 没有建立索引,报告里的 DBuy_Time_select 存储过程按处理时间查售退记录,数据量大时就是全表扫描,这是典型的“索引没跟上查询”,补一个 (Time_SD) 或 (Time_SD, Dno) 索引就能解决。高频写入的场景下,如果主键是随机 UUID,插入时会造成索引页频繁分裂,进而引发锁等待甚至死锁,这份报告的复合主键是业务有序编号,反而不容易出现这个问题。

4. 数据库实施:建库建表、视图隔离、触发器联动与存储过程封装

4.1 建库建表:注意版本差异

报告附录第一句写的是 create datebase DrugStore,明显是拼写错误,正确是 create database。这种笔误在手写的报告里非常常见,直接照着敲会报语法错误。建库之后就是建表,前面已经给出了 Drug、Maker、DOrder 的建表 SQL,这里补上库存表和售出表:

create table Stored ( Dno char(5) not null, Lno char(5) not null, Quantity int not null, primary key (Lno, Dno), foreign key (Lno) references Storage(Lno), foreign key (Dno) references Drug(Dno), check (Quantity > 0) ); create table DBuy ( Pno char(5) not null, Dno char(5) not null, Time_SD datetime, Quantity int not null, Deal char(4) not null, primary key (Pno, Dno), foreign key (Pno) references Patient(Pno), foreign key (Dno) references Drug(Dno), check (Quantity > 0), check (Deal = '售出') );

两张表的复合主键分别对应“哪个柜台的哪种药”和“哪个病人买了哪种药”。外键 Dno 都指向 Drug(Dno),保证流水表里不能出现药品主表中不存在的编号,这就是参照完整性在实施阶段的落地。另外原报告里用户表直接命名为 user,user 是 SQL Server 的保留字,建表会直接报错,要么加方括号写成 [user],要么改成 sys_user 这类名字;字段 Postword 也是明显的笔误,应为 Password。保留字避让这类问题在作业里很容易被忽略,但在实际 DDL 脚本里第一次执行就会暴露。

4.2 视图:用 with check option 隔离数据权限

安全性设计用了视图加用户授权两层。给买药人看的视图只暴露药品名、规格、品牌、制药商和联系电话,不给进价售价;给管理员看的视图里才有价格和库存。两个视图的 SQL 如下:

-- 买药人视角:只看到在售药品的公开信息 create view DM_P as select Dname as 药品名字, Dguige as 规格, Dbrand as 品牌, Mname as 制药商名称, Mplace as 产地, Mphone as 联系电话 from Drug, Maker, DOrder where Drug.Dno = DOrder.Dno and Maker.Mno = DOrder.Mno with check option; -- 管理员视角:按柜台查库存,带进价售价 create view DS_M as select Drug.Dno, Drug.Dname, price1, price2, Storage.Lname, Stored.Quantity from Drug, Stored, Storage where Drug.Dno = Stored.Dno and Storage.Lno = Stored.Lno with check option;

这里要特别指出报告原稿的一个笔误:原 DS_M 视图的连接条件写成了 Drug.Dno = Stored.Lno,把药品编号和柜台编号当成同一维度比较,结果可想而知。正确应该是 Drug.Dno = Stored.Dno 且 Stored.Lno = Storage.Lno。如果你照着原报告敲代码发现查出来全是空或者列对不上,第一个要查的就是连接条件里的字段归属。

with check option 的作用是:通过视图插入或更新的数据必须满足视图本身的 where 条件。比如基于 DM_P 视图做插入时,如果插入的数据在 DOrder 里找不到对应关系,操作会被拒绝,这层保护在视图权限下放后尤其重要。

4.3 触发器:四张流水表与库存的联动

库存不会自动变化,要靠触发器把订购、退订、售出、退回四个动作翻译成 Stored 表的增减。报告里写了四个 after insert 触发器,逻辑是对称的。以订购和售出为例:

create trigger DOrder_insert on DOrder after insert as begin update Stored set Stored.Quantity = Stored.Quantity + inserted.Quantity from Stored, inserted where Stored.Dno = inserted.Dno; end; go create trigger DBuy_insert on DBuy after insert as begin update Stored set Stored.Quantity = Stored.Quantity - inserted.Quantity from Stored, inserted where Stored.Dno = inserted.Dno; end;

inserted 是 SQL Server 在 after insert 触发器里自动生成的虚拟表,里面保存本次插入的所有新行。触发器就是用 inserted 里的药品编号去匹配 Stored 表,把对应药品的库存加或减掉。退订触发器和退回触发器分别是减和加,方向与订购、售出相反,四段代码其实只有运算符不同。这里的隐藏问题是:update 语句并没有对 Quantity 做非负校验,如果库存不够,数量会变成负数。这个问题的处理放到最后一节。

4.4 存储过程:把增删改查封装成参数化接口

存储过程这部分体现了“数据库增删改查接口化”的思路。业务层不直接对表 insert,而是调用存储过程,权限上可以只给应用账号执行存储过程的权限,不给底层表的增删改权限;SQL 上参数化输入,避免拼接字符串带来的注入风险。

create procedure Drug_insert @drug_no char(5), @drug_name char(20), @drug_class char(8), @drug_guige char(10), @drug_brand char(10), @drug_price1 decimal(10,2), @drug_price2 decimal(10,2) as begin insert into Drug(Dno, Dname, Dclass, Dguige, Dbrand, price1, price2) values (@drug_no, @drug_name, @drug_class, @drug_guige, @drug_brand, @drug_price1, @drug_price2); end; go create procedure DBuy_Time_select @dbt_time datetime as begin select Pno, Dno, Quantity from DBuy where Time_SD = @dbt_time; end;

存储过程的参数名统一用 @ 开头,和表字段名区分开,不会有歧义。调用方式很简单:

exec Drug_insert 'D010', '感康', '感冒药', '10片', '仁和', 9.50, 15.00; exec DBuy_Time_select '2025-06-01';

5. 缺货检查的触发器改进:从 PRINT 提示到回滚事务

5.1 原触发器的两个问题

报告里 Stored_quohuo 触发器在库存低于 10 时只做了一件事:print('药品货不足')。print 的结果只出现在 SSMS 的消息选项卡里,应用层根本接收不到,库存照样一路扣成负数。触发器是数据一致性的最后一道闸门,正确的做法是在阈值被突破时直接回滚事务并抛出错误。

5.2 改进版触发器

create trigger TR_Stored_StockCheck on Stored after insert, update as begin if exists ( select 1 from Stored s join inserted i on s.Dno = i.Dno and s.Lno = i.Lno where s.Quantity < 10 ) begin rollback; raiserror('药品库存低于阈值,本次操作已回滚', 16, 1); end end;

和原版相比,改动有两点设计含义:一是用 inserted 关联,只检查本次更新涉及的药品库存,避免历史存量数据低于阈值时误伤所有后续操作;二是 rollback 会把同一批次里已经执行的库存修改全部撤销,DBuy 或 DOrder 的插入也一起回滚,保证数据一致。

5.3 验证方法

用两条语句验证即可:

select Dno, Lno, Quantity from Stored where Dno = 'D001'; go insert into DBuy(Pno, Dno, Time_SD, Quantity, Deal) values ('P001', 'D001', getdate(), 95, '售出'); go select Dno, Lno, Quantity from Stored where Dno = 'D001';

如果 D001 原库存是 100,卖出 95 后理论上剩 5,低于阈值,插入语句会直接报错并回滚,第二次查询库存仍然是 100,DBuy 表中也没有这条记录。这套触发器的思路换到 MySQL 上同样成立,只是语法上要用 BEFORE INSERT 触发器配合 SIGNAL SQLSTATE '45000' 抛异常来替代 rollback,校验逻辑本身没有区别。

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

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

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

立即咨询