门诊管理系统数据库设计:从E-R图到SQL Server与Oracle实现
2026/9/18 0:24:36 网站建设 项目流程

简介:医院门诊管理系统数据库设计课程设计文档,适合软件工程或数据库课程设计的学生参考,用于完成医院门诊业务从需求梳理到数据库落地的完整设计过程。文档基于结构化分析方法,围绕病人信息、医生信息、药品信息、诊断信息等核心数据,详细展开数据流程图、数据字典、E-R图设计,并覆盖概念设计、逻辑设计、物理设计以及SQL Server 2008环境下的数据库实施与测试,可直接借鉴其模式构建同类管理系统。压缩包内共有1个doc文件,大小731KB,以课程论文形式呈现,结构包含目录、需求分析、数据库结构设计、物理设计与实施测试等章节,便于按章节查阅和修改。已有2831人学习浏览,对于正在撰写数据库课程设计报告、需要规范图表和数据库建表思路的读者,是一份实用且具参考价值的范例。

1. 门诊系统的需求边界:挂号、诊断、收费三条主数据流

门诊管理系统是典型的OLTP场景,看起来只是“病人挂号、医生开单、药房发药”,但真正把数据模型拆开时要面对的坑不少:同一张收费单会被挂号、药品、检查三个环节引用;同一个病人可能先欠费后补缴;医生和科室之间还存在排班带来的动态关系。这份课程设计选的是最稳妥的结构化路线——先画数据流程图(DFD),再写数据字典,最后用E-R图收敛成关系模式。整个过程没有引入过于花哨的设计,反而更适合拿来当数据库课程的完整范本。对还在做课程设计、或者想快速搭一套门诊业务演示库的读者来说,这份文档的价值在于:它把数据流、数据字典、概念模型到物理实施的完整链路都走了一遍,SQL Server和Oracle两套建库脚本也齐备,直接照着改改就能用。

2. E-R图设计:8个实体如何收敛成8个关系模式

2.1 分E-R图与全局E-R图的合并流程

需求分析阶段产出的数据字典里定义了35个数据项、8个数据结构、11个数据流和6个处理逻辑。在此基础上画分E-R图时,论文按照挂号收费、诊断、取药三张第二层数据流图分别建立局部模型。挂号收费这组包含病人、医生、科室、挂号单、收费单五个实体;诊断这组增加了处方、诊断结果;取药这组则把药品实体与处方关联起来。

关键的动作是合并分E-R图时消除冗余。常见的冗余有两种:一种是属性冗余,比如挂号单里已经通过病人编号关联到病人姓名,那挂号单实体里就没必要再存一遍“病人姓名”;另一种是关系冗余,比如病人和医生之间既有“挂号”联系又存在“诊断”联系,如果分图里重复画了,合并时就要按实际业务保留一条路径。文档里的全局E-R图最终保留了病人-医生、病人-挂号单、科室-医生、医生-诊断结果、诊断结果-处方单、处方单-药品-收费单这六组主要联系,其中处方单、收费单和药品构成三元联系,这是后续逻辑设计里最值得留意的地方。

2.2 实体转关系模式的三种合并策略

逻辑设计阶段要把E-R图映射到关系模型。这里遵循的标准做法是:一个实体转一张表;1:1联系可以并入任意一端;1:n联系并入n端;m:n联系必须独立建表。本系统的八张表里,科室与医生是1:n,所以把科室号并入医生表;医生与病人是1:n,所以医生号放进病人表;病人与挂号单是1:1,挂号单并入病人端。最终形成下面这套关系模式:

关系模式主键外键说明
科室 DepartmentDp_no-科室基本信息
医生 DoctorDnoDp_no医生归属科室
病人 PatientPnoDno病人登记及主治医生
药品 MedicineMno-药品价格与库存
收费单 BillBno-收费记录,属性含金额和方式
处方单 PrescriptionPr_noMno, Bno处方关联药品与收费单
诊断结果 Diagnose(Dno, Pno)Pr_no复合主键,记录确诊信息
挂号单 RegisterRnoPno, Bno挂号记录

这里有个容易被课程设计毙掉的地方:诊断结果表把(Dno, Pno)作为复合主键,隐含了一个假设——同一个医生对一个病人只会确诊一次。真实门诊里复诊场景很常见,所以更稳妥的做法是引入独立的诊断流水号。但如果是教学演示,复合主键能体现“医生-病人-病名-处方”的完整关联,答辩时能说出取舍理由即可。

逻辑结构定义时,文档用表格列出了每个属性的类型、长度、是否主外键、约束条件,这一步在课程设计里属于“用户子模式建立”的范畴,用来证明你考虑了不同角色的数据访问视角。实际工程中这也对应数据库设计文档里的字段说明书。

2.3 三范式检查与常见误用

关系模式到了三范式,核心是消除传递依赖。以病人表为例:Pno决定Dno,Dno决定Dname,那么如果把医生姓名放进病人表,就产生了传递依赖,病人表就不再满足3NF。本系统的做法是把医生姓名留在医生表,病人表只保留Dno作为外键,查询时通过JOIN取名字,代价是多一次关联,换来的是一致性——医生改名后只需更新一处。

非主属性对码的部分依赖同样要清理。例如挂号单表如果同时放了病人姓名和病人编号,而病人姓名可由病人编号推导,就会造成冗余。检查时可以按规范化的定义逐表过一遍,也可以直接写SQL查依赖,我个人会在PowerDesigner这类工具里画好模型后,用它的检查功能跑一遍,比人工核对快得多。

另外一个常见误用是过度规范化——为了消除冗余把1:1关系拆成两张表,比如把挂号单拆成“挂号基本信息表”和“挂号扩展信息表”。门诊这类场景查询频率远高于写入频率,表拆得太碎反而让每次挂号查询都要做三次JOIN。这套设计的分寸掌握得比较合适:挂号单保持单表,收费单也保持单表,只在必要的地方用外键关联。

3. SQL Server 2008落地:建表、视图、索引与存储过程

3.1 物理设计:字段类型与约束的选择

进入物理设计阶段,数据库名为Hospital,八张基本表的建表语句在课程设计文档里完整给出。类型选择上全部用varchar(20)做主键,这个粒度对课程设计演示够用,但生产环境里我一般会改用自增int或bigint,或者用业务编码如“P20260601-0001”这类定长字符串。varchar(20)做主键的问题在于:一旦门诊量上来,索引页分裂和存储开销都会被放大,而且外键引用时每次关联都要做字符串比较。

建表时值得注意的约束有三个。一是病人表的Page字段带check(Page>=0 and Page<=150),把年龄约束落到数据库层,防止应用程序端漏校验;二是医生表的Dp_no通过references Department(Dp_no)实现外键,确保医生不会挂到不存在的科室;三是处方单表里同时引用了Medicine和Bill两张表,表达“一次处方对应一种药、一次缴费”的业务逻辑。外键要不要建,课程设计里一般都会建,但实际生产里很多团队会故意去掉外键,把一致性交给应用层通过事务保证,理由是避免锁竞争和死锁。站在教学角度,保留外键是对的,能体现参照完整性概念。

3.2 视图层的查询路径设计

六个视图对应六类高频查询场景:

-- 收费细则视图:通过多表关联把诊断、处方、收费单据联成一条明细 create view BillDetail as select distinct Diagnose.pno, Bill.Bno, Bdate, Bmoney, Bway from Prescription, Bill, Diagnose, Register where Register.pno = Diagnose.pno and (Diagnose.Pr_no = Prescription.Pr_no and Prescription.bno = Bill.bno or Register.bno = Bill.bno)

这段SQL拼接了四条表的笛卡尔积,distinct用来去重。逻辑上是把一条挂号链路里可能出现的两条收费路径——挂号费和药费——都合并进同一个视图。注意到or条件的存在:一条挂号单可能先产生挂号收费,后续诊断才产生药品收费,两个都满足时就会查出两条记录,所以必须distinct。这里的隐式JOIN写法在SQL Server 2008里没问题,但不推荐在更长查询里继续用,可读性差且容易漏条件产生笛卡尔积。换成显式JOIN写法会更清晰:

create view BillDetail as select distinct r.pno, b.Bno, b.Bdate, b.Bmoney, b.Bway from Register r join Bill b on r.bno = b.bno left join Diagnose d on d.pno = r.pno left join Prescription p on p.Pr_no = d.Pr_no and p.bno = b.bno

视图层的价值在于把复杂关联封装成“虚拟表”,应用层只需select * from BillDetail就能拿到收费明细。剩下的病人-药品视图、诊断结果视图、医生病人视图、科室医生视图、病人挂号视图,本质上都是两张表按外键关联后投影部分列,代码模式一致。课程设计里用视图主要是为了体现数据库编程能力,答辩时能说清“为什么用视图”——通常是复用查询、权限控制(只暴露指定列)、逻辑屏蔽(应用层不直接感知表结构变更)。

3.3 索引的设计粒度

索引部分文档给出三例:Medicine(Mname)上的unique索引、Patient(pname)上的unique索引,以及Bill(bno)上的普通升序索引。unique索引在这里承担的是唯一性约束职责,防止药品名和病人名重复入库。

从查询路径看,真正高频的过滤条件是Rno(挂号单号)、Dno(医生号)、Pno(病人编号),这三者都是主键,默认已有聚集索引,不需要额外建索引。可能欠缺的是外键字段的索引:Prescription表的Mno经常作为查询条件关联Medicine,如果Mno上没有索引,每次关联都要全表扫描。课程设计里没有覆盖这一点,但实际演示时插入几千条数据后就能感觉到差异。建议在做性能测试前,对外键列统一补上非聚集索引。

3.4 存储过程:把业务流程封装进数据库

存储过程是这份设计最有含金量的部分。它把“添加病人-生成收费单-创建挂号记录”三步操作封装成一个原子流程,对应挂号业务的addpatient存储过程:

create proc addpatient @Rno varchar(20), @Rway varchar(20), @Pno varchar(20), @Bno varchar(20), @Pname varchar(20), @Psex varchar(20), @Page int, @Dno varchar(20), @Bmoney float as begin insert into Patient values(@Pno, @Pname, @Psex, @Page, @Dno) insert into Bill values(@Bno, GETDATE(), @Bmoney, '挂号收费') insert into Register values(@Rno, @Rway, GETDATE(), @Pno, @Bno) end

三个insert分表写入了病人、收费单、挂号单,参数的顺序与三张表的字段顺序一一对应。这里有个明显的隐患:如果第二个insert失败,第一个insert已经提交,会造成“有病人无挂号单”的脏数据。严格来说应该用begin tran/commit tran包一层事务,或者直接声明为with execute as调用方并在过程内部管理事务。课程设计正文没提事务处理,但答辩时几乎必问,能主动补上会加分。

addDiagnose存储过程逻辑同理,完成“确诊+开处方+录入药费”三连操作。问题比addpatient更复杂,因为它同时写了Bill、Prescription、Diagnose三张表,还涉及收费金额的累计。这里我会补充一个参数说明:Bmoney如果传的是药品总价,Bill表会新增一条独立收费记录;如果业务要求合并到挂号产生的Bill上,就要在前面用update而不是insert。这个细节看你定义的收费粒度,是“一次就诊一条收费单”还是“一个项目一张收费单”。

另有两个存储过程值得注意:change_tel用于更新科室电话,change_med用于更新药品剩余量,这类“单表更新”封装成存储过程意义不大,更像是凑功能点。实际工程里我更倾向于让应用层直接执行update,省一层数据库调用。但课程设计需要展示存储过程覆盖增改查多类操作,所以保留可以理解。

3.5 分组统计与条件查询

存储过程末尾还藏着两个查询型过程:Dept_Doc按科室统计医生人数,Diag_p按病名筛选病人。按科室统计的实现用group by Dp_name加count(dno),结果集是科室名加人数两列。这类聚合查询放存储过程里确实比视图灵活,可以带参数做动态筛选。但注意Diag_p统计感冒病人的过程是写死的条件where iname='感冒',这在演示时没问题,实际使用时应改成@iname参数,否则每增加一种病就要新建一个过程。

4. Oracle移植:同一套模型的差异点与数据入库

4.1 类型与语法的关键差异

同一个设计从SQL Server移植到Oracle,不是简单把脚本跑一遍就行,至少有四处要改。日期类型:SQL Server的date在Oracle里通常换成DATE,带时间部分;字符串长度语义不同,varchar2(20)的单位是字节还是字符取决于字符集设置,中文库下建议用varchar2(20 char)避免入库报超长。空字符串:Oracle把空字符串当成NULL,SQL Server允许'',这会导致插入空值时报约束错误。主键自增:SQL Server可以用identity列,Oracle 11g只能用序列加触发器或者直接在插入时用序列的nextval。语法兼容:create proc在Oracle里是create or replace procedure,参数前必须带方向修饰符,比如@pname改成p_name in varchar2。

对应下面的建表对比:

-- SQL Server: identity自增主键 create table Bill ( Bno int identity(1,1) primary key, Bdate date, Bmoney float, Bway varchar(20) ); -- Oracle 11g: 序列 + 插入时显式取值 create sequence seq_bill start with 1 increment by 1; create table Bill ( Bno number(10) primary key, Bdate DATE, Bmoney number(10,2), Bway varchar2(20 char) ); -- 插入时: values(seq_bill.nextval, sysdate, 50.00, '挂号收费')

这里int identity改成number(10)后,主键生成方式从数据库自动变为主键值需外部供给。float改number(10,2)顺便解决了浮点金额精度问题,这是Oracle方案里比原设计合理的地方,金额用float会有二进制误差,累计报表时容易对不上。

4.2 数据入库的三条路线

文档里的Oracle实施演示了数据库对象建立和数据入库两部分。实际生产或课程演示中,往里灌数据常见有三条路线,按效率从低到高排列:

第一条是逐条insert,适合验证触发器、外键约束,几十条数据手工造没问题。第二条是SQL*Loader,适合从文本文件批量导入,语法简单,映射关系写在控制文件里,几千条药品数据几秒就能入库。第三条是用存储过程循环造数,适合生成压测数据,比如往Register表插入10万条挂号记录。课程设计里一般数据量不大,其实第一条就够。要点是注意外键插入顺序:先科室,再医生,接着病人、药品、收费单,最后才是处方和诊断结果,否则外键约束会直接报ORA-02291。

4.3 移植后的验证清单

数据入库后要做的第一件事不是select *,而是按外键顺序逐表核对行数:

-- 核对各表记录数与关联完整性 select (select count(*) from Register) as reg_cnt, (select count(*) from Bill) as bill_cnt, (select count(*) from Patient) as pat_cnt; -- 找出孤儿记录:挂号单引用了不存在的病人 select r.Rno, r.Pno from Register r left join Patient p on r.Pno = p.Pno where p.Pno is null;

第二条SQL的left join写法能把孤儿记录揪出来。如果结果不为空,说明入库顺序有误或外键约束被延迟检查了。这一步在答辩时非常实用,能直观展示你对自己的数据心里有数。

另外要注意的是,Oracle的varchar2不能直接存储超过4000字节的字符串,如果扩展字段描述,应换成CLOB或拆表。这份设计里的字段全在20字符内,不存在这个问题,但如果有人想把检查结果、病史描述塞进病人表,就要提前规划大字段策略。

5. 答辩现场:视图依赖、防重复与存储过程的事务边界

课程设计做到能运行只是及格,答辩时考官更关心边界条件。三个必考点:视图更新问题、唯一索引防重复、存储过程事务边界。

视图更新是高频提问点。基于多表join的视图(如BillDetail)默认不可更新,因为SQL Server无法确定一行改动应该映射到哪张基表。考官问“这个视图能不能update”时,正确回答是:单表视图可以,多表视图要看是否满足键保留表条件——即视图中是否有一张表的主键能唯一定位视图中每行。BillDetail因为有distinct和or逻辑,肯定不可更新,应该直接说明这类视图只读,写操作走后端存储过程。

防重复方面,unique索引的唯一约束要防的是重复挂号。但不要把唯一性全部压在数据库层:例如,同一病人上午挂内科下午挂外科是合法业务,用(Pno, Rdate, Dno)做唯一索引会误伤正常流程。正确粒度是按业务维度区分:Rno本身是主键能防重复;Pname上的唯一索引反而会让同名病人无法建档——文档里给Pname建unique_pname索引,这个选择在真实环境里会踩坑。答辩时可以主动提起这个权衡,说明如果业务上允许同名,应改成非唯一索引或加身份证号字段。

存储过程的事务边界是最后一个深渊。addpatient的三个insert没有包事务,在快速连续插入时如果中途失败,会出现部分写入。补上事务只需要包一层:

create proc addpatient @Rno varchar(20), @Rway varchar(20), @Pno varchar(20), @Bno varchar(20), @Pname varchar(20), @Psex varchar(20), @Page int, @Dno varchar(20), @Bmoney float as begin begin try begin tran insert into Patient values(@Pno, @Pname, @Psex, @Page, @Dno) insert into Bill values(@Bno, GETDATE(), @Bmoney, '挂号收费') insert into Register values(@Rno, @Rway, GETDATE(), @Pno, @Bno) commit tran end try begin catch rollback tran raiserror('挂号流程写入失败,请检查参数', 16, 1) end catch end

begin try/begin catch在SQL Server 2008开始支持,原设计漏掉这个防御,是唯一需要改动才能过严苛评审的点。验证方法很直接:故意把@Pno传成一个已有主键,触发器或约束会报错,看前两张表是否留下脏数据。如果事务包装正确,三条insert会一并回滚,Register表行数保持不变。

另一个值得展示的小技巧是用系统视图查依赖关系,确认表的关联设计没有悬空引用:

select fk.name as 约束名, tp.name as 父表, ref.name as 引用表 from sys.foreign_keys fk join sys.tables tp on fk.parent_object_id = tp.object_id join sys.tables ref on fk.referenced_object_id = ref.object_id order by 父表;

这条查询把库内所有外键约束和对应主从表关系一次性列出来,可以用来核对逻辑设计文档里画的关联图和实际建库结果是否一致。课程设计里画了E-R图但建库时漏建外键的情况很常见,跑一遍这条SQL就能在答辩前发现问题。整个项目最有复用价值的就是这套“数据流分析-关系模式-建库脚本-验证SQL”的链路,下次换一个业务域,比如图书馆借阅或在线考试,直接把实体替换掉,工作流完全不需要变。

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

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

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

立即咨询