
简介这份文档面向计算机专业学生及数据库设计初学者围绕医院管理系统这一典型业务场景系统讲解数据库设计的完整思路帮助读者理解如何从医院实际业务流程出发完成需求分析与建模。内容涵盖门诊与住院两大部门的组织结构梳理挂号、收费、取药、检查、住院、手术等业务活动分析以及数据完整性、一致性、安全性和系统性能等设计要点并结合ER图、实体表与外键关系、索引优化等给出具体方案。资源包共1个docx文件约398KB以文字与图示形式呈现需求分析与设计文档便于直接查阅和参考。目前已有174人学习下载适合课程设计、毕业设计或数据库课程练习时对照借鉴读者可从中获取业务调研方法、实体关系划分思路与表结构设计参考快速搭建符合医院场景的数据库框架。1. 医院管理系统数据库设计为什么一张挂号单能牵动整个库表结构很多人第一次看医院管理系统的数据库设计文档会觉得它不过是把病人、医生、药品各建一张表再拿外键串起来。真正翻完一份完整的 HIS 需求分析才会发现反直觉的地方在于决定整个库表骨架的不是病人表也不是医生表而是那张看起来最不起眼的挂号单。挂号单一旦落库就同时牵出了科室、医生排班、收费项目、病历归属四条链路住院侧同理一张入院登记单背后是床位、病区、医嘱、押金四条线。医院信息系统被公认为企业级信息系统里最复杂的一类原因不在数据量大而在于信息种类庞杂、业务流程交叉、状态流转密集。搞懂这套数据库设计本质上是搞懂「业务单据如何驱动实体关系」这件事这也是计算机专业做课程设计时最容易翻车、也最值得深挖的一块。2. 需求分析到实体建模门诊与住院两条业务流怎么变成表2.1 从组织机构图里抽实体为什么科室是自关联的原始需求里给了一张医院组织机构图门诊部门和住院部门各下设若干科室口腔科、内科、外科、皮肤科分列其下而且两个部门下设的部分科室是交叉的。这句话是整份设计里信息密度最高的一句它直接决定了科室表不能设计成一张平铺的字典表。常见的实现思路是把科室建成带父级引用的自关联结构用parent_id表达「门诊部 → 内科」这种层级同时用独立的关联表表达「某科室同时属于门诊和住院两侧」的多对多关系。如果偷懒把科室名直接写进医生表、挂号表里后面一旦科室改名或者新增交叉科室就要全库改字符串这是血泪经验里最常见的一种反模式。-- 科室表自关联表达组织层级 CREATE TABLE department ( dept_id CHAR(8) PRIMARY KEY, -- 科室编号 dept_name VARCHAR(32) NOT NULL, -- 科室名称如内科、外科 parent_id CHAR(8), -- 上级部门门诊部/住院部为 NULL dept_type TINYINT NOT NULL, -- 1 门诊 2 住院 3 医技 4 行政 FOREIGN KEY (parent_id) REFERENCES department(dept_id) ); -- 科室与院区的归属关系解决门诊/住院科室交叉 CREATE TABLE dept_scope ( dept_id CHAR(8) NOT NULL, scope TINYINT NOT NULL, -- 1 门诊侧 2 住院侧 PRIMARY KEY (dept_id, scope) );dept_type这个字段值得单独说。需求里把药库、药房、设备科、财务科、血库、病案室、手术室都列进了组织机构它们不是临床科室但在业务流里承担关键节点。把它们统一收进 department 表、用类型区分好处是挂号、检查、领药这些单据的「目标科室」字段可以统一外键指向 department不用为每类科室建一套表。代价是 department 表会变宽查询时要带类型过滤。这是典型的「一张宽表换外键统一」的取舍。2.2 门诊流的单据链挂号单为什么是主实体把门诊业务活动拆开看是一条清晰的单据链初诊病人到挂号处登记基本信息并挂号复诊病人直接挂号缴费后到相应科室就诊医生开处方或检查申请单病人再缴费然后去药房取药或去检查科室检查检查结果回来复诊。这条链上产生了挂号单、门诊病历、门诊处方、检查项目、收费项目五类核心数据。关键在于挂号单承载了「一次就诊会话」的身份。病历、处方、检查单都挂在这次挂号之下而不是直接挂在病人身上。为什么这么设计因为同一个病人一年可能来十次每次的主治医师、科室、诊断结果都不同。如果病历直接挂病人就无法区分是哪一次就诊产生的记录统计医生工作量、核算科室收入都会失真。CREATE TABLE registration ( -- 挂号单 gh_no CHAR(8) PRIMARY KEY, -- 挂号号8 位唯一编码 patient_id CHAR(8) NOT NULL, -- 病人号 dept_id CHAR(8) NOT NULL, -- 挂号科室 doctor_id CHAR(8), -- 主治医师初诊可为空 gh_type TINYINT NOT NULL, -- 1 初诊 2 复诊 gh_date DATE NOT NULL, -- 挂号日期 fee_id CHAR(8) -- 关联收费项目 ); CREATE TABLE outpatient_record ( -- 门诊病历 bl_no CHAR(8) PRIMARY KEY, -- 病历号 gh_no CHAR(8) NOT NULL, -- 归属的挂号单 patient_id CHAR(8) NOT NULL, doctor_id CHAR(8) NOT NULL, diag_time DATETIME NOT NULL, content VARCHAR(100), FOREIGN KEY (gh_no) REFERENCES registration(gh_no) );这里有个容易被忽略的细节需求的数据字典里挂号单的组成是「挂号号、挂号类别、主治医师、挂号人姓名、挂号科室、挂号日期」并没有病人号。但病人表又单独存在。如果严格照抄挂号单和病人表之间就断了联系。合格的解读是挂号人姓名是冗余字段用于快速展示真正的关联必须靠病人号。这种「字典里写了姓名但实际要落 ID」的情况在需求文档里非常普遍建模时要以实体关系为准不能逐字照搬字段清单。2.3 住院流的床位与医嘱状态机藏在哪张表里住院侧比门诊复杂一个量级。病人办入院手续、交押金病区科室安排床位医生开医嘱护士按医嘱形成药品申领单向药库领药检查申请单转给检查科室需要手术的提交手术申请给手术室出院时先办出科再办出院转科时还要走转科手续。床位是这里最典型的状态资源。一张病床在时间轴上会经历「空闲 → 占用 → 空闲」的循环而病人可能转床、转科。如果把床位号直接写成病人表的一个字段转床历史就丢了床位占用冲突也无法检测。常见的实现思路是建一张床位分配记录表用起止时间表达占用区间。CREATE TABLE bed_assignment ( -- 床位分配记录 assign_id BIGINT PRIMARY KEY, bed_no CHAR(8) NOT NULL, -- 病床号 patient_id CHAR(8) NOT NULL, zy_no CHAR(8) NOT NULL, -- 住院号 start_time DATETIME NOT NULL, end_time DATETIME, -- NULL 表示当前占用 assign_type TINYINT NOT NULL -- 1 入科 2 转床 3 转科 4 出科 );医嘱表则要区分「开立」和「执行」两个时间点。需求里医嘱的组成是「诊断单序号、诊断类别、启动日期、处理日期、医嘱内容、主治医师、病人」启动日期和处理日期分开说明设计者已经意识到医嘱是有生命周期的医生开立是一个状态护士执行是另一个状态。这个双时间戳设计直接支撑了「按天统计某病人用药量」这类查询也是住院收入核算的基础。3. 数据字典到物理表char(8) 编码体系与主外键落地3.1 为什么所有主键都是 char(8)翻数据字典会发现一个高度统一的特征挂号号、病历号、处方号、收费项目号、检查序号、供应商号、药品编号、医生号、病床号、病人号、处理方案号、医嘱号、手术序号、住院号清一色char(8)。这不是随手写的而是一种编码体系设计。用定长字符做主键好处是编码规则可以内嵌业务含义比如前两位表示科室、中间四位表示日期序号、末两位表示当日流水人工核对单据时能直接读出信息。坏处也很明显定长 char 在存储上不省空间而且一旦编码规则要扩展比如科室超过 99 个8 位就不够用。现代实现更倾向于用自增 BIGINT 或雪花 ID 做主键把业务编码放到独立的业务编号字段并加唯一索引。但在这份文档的语境下char(8) 统一编码是合理的教学级选择它让外键关系一目了然。提示如果要在真实项目里落地建议保留 char(8) 作为业务编号并加 UNIQUE 约束另设代理主键。业务编号会变代理主键不变这是避免级联更新的关键。3.2 数据流图怎么翻译成外键方向需求里给了数据流清单处方信息从门诊医生、收费处流向药房病历从门诊医生流向门诊挂号处药品信息从供应商流向药库检查单从门诊医生流向检查部门药品请领单从药房流向药库医嘱从住院医生流向病人和病区科室手术通知单从病区科室流向手术室。数据流的方向不等于外键的方向这是建模时最容易搞混的一点。处方信息「流向药房」但处方表的外键应该指向医生和病人药房只是读取方不需要在处方表里存药房 ID。真正需要落外键的是「谁拥有这条记录」。判断标准是一条记录的生命周期由谁创建、由谁负责外键就指向谁。处方由医生开立所以处方表挂医生号和病人号药房发药是另一个动作应该单独建发药记录表挂处方号和药房号。CREATE TABLE prescription ( -- 门诊处方 cf_no CHAR(8) PRIMARY KEY, gh_no CHAR(8) NOT NULL, -- 归属挂号单 doctor_id CHAR(8) NOT NULL, patient_id CHAR(8) NOT NULL, content VARCHAR(100), status TINYINT DEFAULT 0 -- 0 待缴费 1 已缴费 2 已发药 ); CREATE TABLE dispense_record ( -- 药房发药记录 dispense_id BIGINT PRIMARY KEY, cf_no CHAR(8) NOT NULL, -- 关联处方 pharmacy_id CHAR(8) NOT NULL, -- 发药药房 dispense_time DATETIME NOT NULL, FOREIGN KEY (cf_no) REFERENCES prescription(cf_no) );把「开处方」和「发药」拆成两张表好处是处方状态可以独立流转发药失败、退药、部分发药都能表达。如果合成一张表处方表会变成状态字段堆砌的宽表查询和维护都会变难。3.3 库存一致性药库、药房、请领单的三层结构需求里明确写了药库负责药品贮存、发放和采购药房负责病人药品管理并根据处方生成领药单向药库领药还要求「当库存少到一定程度系统应提出警告」。这实际上是一个两层库存加一层流转的结构药库是总库存药房是前置库存请领单是两者之间的转移凭证。一致性要求在这里体现得最直接药房发药药房库存要减药房向药库领药药房库存加、药库库存减药库采购入库药库库存加。如果只在药品表上放一个quantity字段药库和药房就共用了同一个数字必然出错。正确做法是库存按「药品 × 库房」建联合主键。CREATE TABLE stock ( -- 分库房库存 drug_id CHAR(8) NOT NULL, warehouse_id CHAR(8) NOT NULL, -- 药库或药房编号 quantity INT NOT NULL DEFAULT 0, safety_line INT NOT NULL DEFAULT 0, -- 安全库存线低于则告警 PRIMARY KEY (drug_id, warehouse_id) ); CREATE TABLE stock_request ( -- 药品请领单 request_no CHAR(8) PRIMARY KEY, from_wh CHAR(8) NOT NULL, -- 请领方通常是药房 to_wh CHAR(8) NOT NULL, -- 发放方通常是药库 drug_id CHAR(8) NOT NULL, qty INT NOT NULL, status TINYINT DEFAULT 0, -- 0 待处理 1 已出库 create_time DATETIME NOT NULL );safety_line字段对应需求里的库存告警。告警逻辑不应该写死在应用层的一个 if 里而应该由库存行自身携带阈值这样不同药品可以设不同安全线高值药品和普通耗材的补货策略本来就不一样。4. 避坑与排查医院库设计里最容易翻车的几处4.1 现象病人转科后查不到历史床位原因床位号直接存在病人表或住院登记表的一个字段里转科时被覆盖历史占用区间丢失。解决改用床位分配记录表每次入科、转床、转科、出科都插一条带起止时间的记录当前床位用end_time IS NULL查询。4.2 现象同一病人多次就诊病历查询结果串了原因病历表只挂了病人号没挂挂号号一次就诊的多条病历和另一次混在一起。解决病历表必须带gh_no外键所有按次就诊产生的记录都以挂号单为归属主体查询时先定挂号单再取明细。4.3 现象药房发药后药库库存也变了原因药库和药房共用药品表上的同一个数量字段任何一方扣减都影响全局。解决库存按「药品 × 库房」拆行发药只动药房行请领单出库时同时动两行并用事务保证两边同增同减。4.4 现象医生工作量统计对不上原因挂号单上的主治医师字段在初诊时为空或者医生改派后直接覆盖了原值。解决挂号单保留开单医生另建接诊记录表记录实际接诊医生和接诊时间统计以接诊记录为准原始挂号信息不覆盖。4.5 现象收费项目和实际费用对不上原因收费项目表把挂号费、药品费、检查费塞进同一行一次就诊涉及多个项目时无法逐项核销。解决收费项目拆成项目字典加费用明细两张表明细表挂挂号号或住院号每笔费用一行支持退费和部分缴费。5. 从教学文档到可运行系统冗余、索引与一致性的取舍这份文档最值得琢磨的地方是它在多个地方主动引入了冗余。挂号单里存了挂号人姓名处方里存了病人姓名、性别、年龄医嘱里存了病人和主治医师。按第三范式这些都能通过外键查出来不该重复存。但医院场景的特殊性在于单据要打印、要归档、要作为凭证长期保存。病人改了名字、医生调了科室历史单据上的信息不能跟着变否则凭证就失真了。这就是「快照冗余」的合理性——冗余不是为了查询方便而是为了固化业务发生那一刻的事实。对应的代价是一致性维护成本。我的习惯是凡是作为凭证的单据表允许并鼓励存快照字段凡是作为当前状态载体的表病人、医生、药品、库存严格走外键不冗余。这条界线划清楚后面加字段时就不会纠结。索引设计上高频查询集中在几个方向按病人查就诊历史、按医生查工作量、按日期查收入、按药品查库存。对应的索引应该建在registration(patient_id, gh_date)、prescription(doctor_id, status)、stock(drug_id, warehouse_id)这类组合上。注意不要给每个外键都单独建索引组合索引的前缀列已经覆盖了单列查询多建只会拖慢写入。设计点教学文档做法生产环境建议取舍理由主键char(8) 业务编码代理主键 业务编码唯一索引编码规则会变代理键稳定库存单字段数量药品×库房分行支持多库房与告警阈值单据姓名冗余存储保留快照字段凭证不可随主数据变化科室平铺字典自关联 归属表支持层级与门诊住院交叉最后说一个验证方法拿到任何一份 HIS 库设计先画三条链——挂号到处方到发药、入院到医嘱到请领、检查申请到结果回执。每条链上问三个问题归属主体是谁、状态字段够不够表达流转、失败或退回时数据往哪走。三条链都能自洽这套设计基本就站得住。从那以后我每次评审库表都强制先把这三条链在纸上走一遍再去看字段清单很多隐藏的断链一眼就能看出来。希望帮到你。本文还有配套的精品资源点击获取