简介:《医院管理系统数据库设计》文档面向计算机专业学生、课程设计与毕业设计者,以及需要梳理医院信息系统(HIS)业务流程的初学者,帮助其完成从需求分析到数据库建模的文档撰写。内容围绕门诊与住院两大部门展开,覆盖挂号、收费、处方、取药、检查、住院入科、转科、出院及药库药房管理等业务场景,并归纳数据完整性、一致性、安全性、性能、可扩展性与法规遵从等设计要点,同时给出ER图、实体表、外键关联、索引设计和适度冗余的建模思路。整包为1个docx文件,约398KB,篇幅集中,便于查阅与二次编辑。已有175人学习,适合作为课程作业参考、数据库课程设计模板或HIS需求梳理的入门材料,读者可据此快速搭建表结构、梳理实体关系并补充设计说明。
1. 一份医院管理系统数据库设计 docx:能直接抄的是骨架,不能抄的是字段
医院管理系统数据库设计这份 docx,真正值钱的部分从第 13 页才开始:挂号号、病历号、处方号、检查序号,每个都标了 char(8),后面跟着数据流、数据存储和处理过程。很多人拿到手先画 ER 图,画完直接建表,结果挂号高峰一压就露馅——不是文档写错了,是文档停在逻辑设计,没告诉你物理落地时该补哪些约束。
它适合课程设计收尾、毕设开题,也适合给小型门诊系统做库表底稿。实体划分、业务链、数据字典的口径都能直接用;字段类型、主键策略、事务边界得重做。下面按“先读结构、再建表、后验账”的顺序拆一遍,把这份 docx 变成能跑的库。
2. 从组织机构图到 ER 图:把 20 个业务实体压成 12 张核心表
文档前半部分是需求分析和组织机构图,后半部分是数据字典、数据流、数据存储和处理过程。读的时候别从第一页顺着翻,先看第 6 页到第 9 页的两张业务图,把门诊链和住院链画出来,再回头对数据字典。实体不是越多越好,核心表控制在 12 张左右,外围的字典表、日志表按需加,库才不会被课程设计的“全字段”拖垮。
2.1 门诊链与住院链:为什么不能合成一张就诊表
门诊链是:挂号处登记挂号 → 收费处缴挂号费 → 医生开处方或检查单 → 收费处缴费 → 药房发药或检查科室检查 → 复诊。住院链是:住院处办入院 → 病区排床 → 医生开医嘱 → 护士执行医嘱 → 药房或检查科室处理 → 手术(可选)→ 出院或转科。两条链在文档里画得很清楚,但落到表设计时,很多人会想“都是就诊,合成一张 visit 表加 type 字段不就行了”。这个想法在课程设计里能跑通,在真实系统里会翻车。
| 业务动作 | 触发单据 | 落表 | 事务边界 |
|---|---|---|---|
| 门诊挂号 | 挂号单 | registration | 挂号号生成 + 挂号费记账 |
| 门诊就诊 | 门诊病历 | medical_record | 病历写入 + 处方或检查单开立 |
| 门诊收费 | 收费项目 | charge_record | 收费状态 + 药品库存预占 |
| 门诊发药 | 处方 | prescription + stock_flow | 处方状态 + 库存扣减 |
| 住院登记 | 入院登记单 | inpatient_admission | 住院号生成 + 预交金记账 |
| 排床 | 床位安排 | bed_occupancy | 床位状态 + 住院登记关联 |
| 医嘱执行 | 医嘱单 | doctor_order + order_execution | 医嘱状态 + 执行记录 |
| 出院 | 出院通知 | inpatient_admission | 出院时间 + 费用结算 |
第一,状态机不同。门诊挂号当天闭环,状态从待就诊到已完成最多几天;住院从入院到出院可能几十天,中间还有转科、手术、出院召回。把两种状态机塞进一张表,状态字段会变成“门诊已收费但未发药”和“住院已出院但未结算”混在一起,统计口径永远对不齐。
第二,费用结算粒度不同。门诊按次收费,挂号费、药品费、检查费在收费处一次结清;住院按日或按项目记账,预交金、每日清单、出院结算三套账。文档里收费项目表只有挂号费、药品费、检查费三个字段,这是门诊视角,住院侧直接套会丢记账明细。
第三,责任主体不同。门诊挂号的归属是挂号处,住院排床的归属是病区护士站;门诊处方由门诊医生开,住院医嘱由住院医生开、护士执行。合成一张表后,外键只能指向“医生”这一层,护士执行记录、病区归属都没地方放。分开建 registration 和 inpatient_admission,后面加字段才不会互相打架。
2.2 char(8) 编码体系:代理键与业务键怎么分工
文档里所有业务号都是 char(8):挂号号、病历号、处方号、检查序号、供应商号、药品编号、医生号、病床号、病人号、处理方案号。订单号 char(20),病区号 char(10)。这些编码只给了长度,没给生成规则和校验位。直接拿来做主键,第一个问题是定长补空格,第二个问题是跨表关联时长度不一致,第三个问题是业务号可能被复用。
| 业务号 | 文档类型长度 | 建议物理类型 | 是否主键 | 生成方式 |
|---|---|---|---|---|
| 病人号 | char(8) | varchar(20) | 否,唯一索引 | 序列或日期+序列 |
| 挂号号 | char(8) | varchar(24) | 否,唯一索引 | 日期+科室+序列 |
| 病历号 | char(8) | varchar(24) | 否,唯一索引 | 病人号+就诊次数 |
| 处方号 | char(8) | varchar(24) | 否,唯一索引 | 日期+序列 |
| 住院号 | char(8) | varchar(24) | 否,唯一索引 | 入院日期+序列 |
| 药品编号 | char(8) | varchar(20) | 否,唯一索引 | 分类码+序列 |
| 订单号 | char(20) | varchar(32) | 否,唯一索引 | 采购日期+供应商码+序列 |
我一般会加一个 bigint 代理键做物理主键,业务号只做唯一索引和展示。代理键不参与业务含义,改编码规则时不用动外键;业务号带唯一索引,检索和对账仍然快。下面这张病人表是模板,后面的表都按这个套路来。
CREATE TABLE patient ( patient_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '代理主键', patient_no VARCHAR(20) NOT NULL COMMENT '病人号,业务唯一', name VARCHAR(50) NOT NULL COMMENT '病人姓名', sex TINYINT NOT NULL DEFAULT 0 COMMENT '0未知 1男 2女', birth_date DATE NULL COMMENT '出生日期', id_card VARCHAR(18) NULL COMMENT '身份证号,可空', phone VARCHAR(20) NULL COMMENT '联系方式', address VARCHAR(200) NULL COMMENT '住址', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_patient_no (patient_no), KEY idx_patient_name_phone (name, phone) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='病人主表';逻辑说明:patient_id 是物理主键,所有外键都指向它,病人号改了也不影响历史关联;patient_no 是业务唯一键,挂号、住院、病案三处都用它做检索入口;id_card 加了可空唯一索引时要注意,MySQL 的唯一索引允许多个 NULL,不会把没登记身份证的病人拦在门外。
参数说明:patient_no 用 varchar(20) 不用 char(8),因为文档里的 8 位只是示例长度,实际编码带日期和序列很容易超;sex 用 tinyint 不用 char(1),后面加“未知”“其他”不用改字段长度;出生日期用 date 不用 datetime,年龄由应用层算,避免每天更新年龄字段。
2.3 ER 覆盖度核对:实体、联系、基数逐项过一遍
文档的数据字典列了数据结构、数据流、数据存储和处理过程,但没有一张完整的 ER 图。落地前先列实体清单,再列联系,最后看每个联系有没有外键落点。下面这张表是从文档里抽出来的实体覆盖情况,标了“需补”的是文档没列、但建库必须有的。
| 实体 | 文档来源 | 主键候选 | 是否核心表 | 备注 |
|---|---|---|---|---|
| 病人 | 门诊病人/病人信息 | patient_id | 是 | 病人号唯一 |
| 医生 | 医生信息/门诊医师 | doctor_id | 是 | 医生号唯一 |
| 科室 | 组织机构 | dept_id | 是 | 门诊住院共用 |
| 挂号单 | 数据结构 | reg_id | 是 | 挂号号唯一 |
| 门诊病历 | 数据结构 | record_id | 是 | 病历号唯一 |
| 门诊处方 | 数据结构 | prescription_id | 是 | 处方号唯一 |
| 处方明细 | 文档未列,需补 | item_id | 是 | 药品+数量+用法 |
| 检查单 | 数据结构 | exam_id | 是 | 检查序号唯一 |
| 收费项目 | 数据结构 | fee_item_id | 是 | 挂号费/药品费/检查费拆维度 |
| 收费记录 | 文档未列,需补 | charge_id | 是 | 按次记账 |
| 药品 | 数据结构 | drug_id | 是 | 药品编号唯一 |
| 供应商 | 数据结构 | supplier_id | 是 | 供应商号唯一 |
| 采购订单 | 数据结构 | po_id | 是 | 订单号唯一 |
| 药库 | 数据结构 | warehouse_id | 是 | 药库号 |
| 药房 | 数据结构 | pharmacy_id | 是 | 药房号 |
| 库存流水 | 文档未列,需补 | flow_id | 是 | 出入库事实 |
| 住院登记 | 数据结构 | admission_id | 是 | 住院号唯一 |
| 床位 | 数据结构 | bed_id | 是 | 病床号 |
| 医嘱 | 数据结构 | order_id | 是 | 医嘱号 |
| 手术 | 数据结构 | operation_id | 是 | 手术序号 |
| 病案 | 数据结构 | archive_id | 是 | 病案号 |
联系和基数这样核:
| 联系 | 基数 | 外键落点 | 唯一约束 |
|---|---|---|---|
| 病人-挂号 | 1:N | registration.patient_id | 无 |
| 挂号-病历 | 1:1 | medical_record.reg_id | uk_reg |
| 病历-处方 | 1:N | prescription.record_id | 无 |
| 处方-处方明细 | 1:N | prescription_item.prescription_id | 无 |
| 病人-住院登记 | 1:N | inpatient_admission.patient_id | 无 |
| 住院登记-床位占用 | 1:N(时段) | bed_occupancy.admission_id | 同床同时段唯一 |
| 住院登记-医嘱 | 1:N | doctor_order.admission_id | 无 |
| 医嘱-执行 | 1:N | order_execution.order_id | 无 |
| 病人-病案 | 1:N | medical_archive.patient_id | 无 |
| 供应商-采购订单 | 1:N | purchase_order.supplier_id | 无 |
| 药品-库存流水 | 1:N | stock_flow.drug_id | 无 |
核对步骤不复杂,按顺序走:先列实体,一个业务名词一张表,别急着合并;再列联系,动词就是联系,比如“挂号”“开方”“执行”;然后给每个联系找外键落点,找不到落点说明还缺表;基数是一对一的加唯一约束,多对多拆中间表;最后看文档里只出现一次的单据,收费记录、库存流水、处方明细这三类最容易漏,漏了后面统计和扣库存都做不了。
3. 把数据字典翻成建表语句:三类表的字段、类型与约束怎么定
数据字典里每个字段都给了类型和长度,但不能照抄。char(8) 的编码要改成 varchar,Int 10 的金额要改成最小货币单位,中文描述字段要拆成编码加内容。这一章按门诊、药品库存、住院三块给建表语句,每块后面说清楚为什么这样定、参数怎么改。
3.1 挂号、病历、处方:三张表的事务边界与明细表补法
文档里门诊处方是一个数据结构,组成是处方号、主治医师、病人姓名、病人性别、病人年龄、处方内容。处方内容 char(100) 存一段文本,发药时没法按药品扣库存,也没法统计某个药开了多少。合格的做法是拆出处方主表和处方明细表,主表存处方头和状态,明细表存药品、数量、用法、频次。
CREATE TABLE doctor ( doctor_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, doctor_no VARCHAR(20) NOT NULL COMMENT '医生号', name VARCHAR(50) NOT NULL, dept_id BIGINT UNSIGNED NOT NULL COMMENT '所属科室', title VARCHAR(20) NULL COMMENT '职称', reg_fee_cents BIGINT NOT NULL DEFAULT 0 COMMENT '挂号费,分', status TINYINT NOT NULL DEFAULT 1 COMMENT '1在岗 0停诊', UNIQUE KEY uk_doctor_no (doctor_no), KEY idx_doctor_dept_status (dept_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='医生表'; CREATE TABLE registration ( reg_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, reg_no VARCHAR(24) NOT NULL COMMENT '挂号号', patient_id BIGINT UNSIGNED NOT NULL, dept_id BIGINT UNSIGNED NOT NULL, doctor_id BIGINT UNSIGNED NULL, reg_date DATE NOT NULL COMMENT '挂号日期', reg_type TINYINT NOT NULL DEFAULT 1 COMMENT '1初诊 2复诊', fee_cents BIGINT NOT NULL DEFAULT 0 COMMENT '挂号费,分', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待就诊 1就诊中 2已完成 3已取消', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_reg_no (reg_no), KEY idx_reg_dept_date_doctor (dept_id, reg_date, doctor_id), KEY idx_reg_patient_date (patient_id, reg_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='挂号单'; CREATE TABLE medical_record ( record_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, record_no VARCHAR(24) NOT NULL COMMENT '病历号', reg_id BIGINT UNSIGNED NOT NULL COMMENT '对应挂号单', patient_id BIGINT UNSIGNED NOT NULL, doctor_id BIGINT UNSIGNED NOT NULL, diagnosis VARCHAR(500) NULL COMMENT '诊断内容', record_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_record_no (record_no), UNIQUE KEY uk_record_reg (reg_id), KEY idx_record_patient_time (patient_id, record_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='门诊病历'; CREATE TABLE prescription ( prescription_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, prescription_no VARCHAR(24) NOT NULL COMMENT '处方号', record_id BIGINT UNSIGNED NOT NULL, patient_id BIGINT UNSIGNED NOT NULL, doctor_id BIGINT UNSIGNED NOT NULL, total_cents BIGINT NOT NULL DEFAULT 0 COMMENT '处方金额,分', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待收费 1已收费 2已发药 3已退药', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_prescription_no (prescription_no), KEY idx_prescription_record (record_id), KEY idx_prescription_status_time (status, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='处方主表'; CREATE TABLE prescription_item ( item_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, prescription_id BIGINT UNSIGNED NOT NULL, drug_id BIGINT UNSIGNED NOT NULL, quantity INT NOT NULL COMMENT '数量', unit_price_cents BIGINT NOT NULL COMMENT '单价,分', amount_cents BIGINT NOT NULL COMMENT '金额,分', usage_text VARCHAR(100) NULL COMMENT '用法用量', KEY idx_item_prescription (prescription_id), KEY idx_item_drug (drug_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='处方明细';逻辑说明:挂号、收费、开方三件事的事务边界要分清。挂号和挂号费记账在同一个事务里提交,挂完号没收费的情况用 status 字段兜底;开处方和处方明细在同一个事务里提交,主表的总金额由明细汇总后回写,不能由前端传;发药时锁处方主表,校验 status=1 再扣库存,避免重复发药。
参数说明:reg_date 用 date 不用 datetime,挂号高峰的索引按天分区更稳;fee_cents、total_cents、amount_cents 都带 _cents 后缀,单位是分,前端展示时除 100;prescription_item 的 quantity 用 int,退药时用负数还是反向流水要在业务层定死,别两边混用;medical_record 的 uk_record_reg 保证一次挂号只对应一份门诊病历。
3.2 药品、药库、药房:库存流水与余额表的双表方案
文档里药库和药房是两个数据结构,药库有药库号、负责人、类别、面积,药房在业务描述里负责领药、发药、库存管理。库存设计最容易犯的错是只建一张药品表,加一个库存量字段,入库加、出库减。单机跑没问题,一旦发药和领药并发,库存量就對不上,而且出了差异查不出原因。我一般用流水表加余额表:流水记事实,余额记快照。
CREATE TABLE drug ( drug_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, drug_no VARCHAR(20) NOT NULL COMMENT '药品编号', drug_name VARCHAR(100) NOT NULL, spec VARCHAR(50) NULL COMMENT '规格', unit VARCHAR(10) NULL COMMENT '单位', safety_stock INT NOT NULL DEFAULT 0 COMMENT '安全库存阈值', status TINYINT NOT NULL DEFAULT 1 COMMENT '1在用 0停用', UNIQUE KEY uk_drug_no (drug_no), KEY idx_drug_name (drug_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='药品字典'; CREATE TABLE stock_flow ( flow_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, drug_id BIGINT UNSIGNED NOT NULL, warehouse_type TINYINT NOT NULL COMMENT '1药库 2药房', warehouse_id BIGINT UNSIGNED NOT NULL, biz_type VARCHAR(20) NOT NULL COMMENT 'PURCHASE_IN/REQUISITION_OUT/DISPENSE_OUT/RETURN_IN', biz_no VARCHAR(32) NOT NULL COMMENT '来源单号', quantity INT NOT NULL COMMENT '正数入库,负数出库', unit_price_cents BIGINT NOT NULL DEFAULT 0, occurred_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_flow_drug_time (drug_id, occurred_at), KEY idx_flow_biz (biz_type, biz_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存流水'; CREATE TABLE stock_balance ( balance_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, drug_id BIGINT UNSIGNED NOT NULL, warehouse_type TINYINT NOT NULL, warehouse_id BIGINT UNSIGNED NOT NULL, quantity INT NOT NULL DEFAULT 0 COMMENT '当前结存', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_balance (drug_id, warehouse_type, warehouse_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存余额';逻辑说明:每次入库、出库、发药、退药都写一条 stock_flow,biz_no 存来源单号,后面按单号能追到具体业务;stock_balance 只存当前结存,发药时在同一事务里UPDATE stock_balance SET quantity = quantity - ? WHERE drug_id=? AND warehouse_type=? AND warehouse_id=? AND quantity >= ?,影响行数为 0 就回滚,防止扣成负数。
参数说明:quantity 在流水里用正负号区分出入库,比加一个 direction 字段少一层判断;unit_price_cents 存采购单价或发药单价,按业务定,但同一张流水表里单位要统一;warehouse_type 区分药库和药房,药房向药库领药就是药库出、药房入两条流水,别只写一条。
3.3 住院侧:入院登记、床位占用、医嘱执行的时序约束
住院侧文档给了入院登记、排床、转床、医嘱、手术、出院、转科。落地时最容易漏的是床位占用表。床位状态直接写在 bed 表里,只能表达“当前占用”,查不了历史,也处理不了转床。加一张 bed_occupancy,记录每次占床的开始和结束时间,bed 表只存当前状态做快速查询。
CREATE TABLE inpatient_admission ( admission_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, admission_no VARCHAR(24) NOT NULL COMMENT '住院号', patient_id BIGINT UNSIGNED NOT NULL, ward_id BIGINT UNSIGNED NOT NULL COMMENT '病区', dept_id BIGINT UNSIGNED NOT NULL COMMENT '科室', admit_time DATETIME NOT NULL COMMENT '入院时间', discharge_time DATETIME NULL COMMENT '出院时间', deposit_cents BIGINT NOT NULL DEFAULT 0 COMMENT '预交金,分', status TINYINT NOT NULL DEFAULT 1 COMMENT '1在院 2出院 3转科', UNIQUE KEY uk_admission_no (admission_no), KEY idx_admission_patient (patient_id, admit_time), CONSTRAINT chk_discharge CHECK (discharge_time IS NULL OR discharge_time > admit_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='住院登记'; CREATE TABLE bed_occupancy ( occupancy_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, bed_id BIGINT UNSIGNED NOT NULL, admission_id BIGINT UNSIGNED NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NULL, KEY idx_occ_bed_time (bed_id, start_time), KEY idx_occ_admission (admission_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='床位占用历史'; CREATE TABLE doctor_order ( order_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(24) NOT NULL COMMENT '医嘱号', admission_id BIGINT UNSIGNED NOT NULL, doctor_id BIGINT UNSIGNED NOT NULL, order_type TINYINT NOT NULL COMMENT '1用药 2检查 3护理 4手术', content VARCHAR(500) NOT NULL, start_time DATETIME NOT NULL, stop_time DATETIME NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0开立 1执行中 2已停止 3已作废', UNIQUE KEY uk_order_no (order_no), KEY idx_order_admission_status (admission_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='医嘱'; CREATE TABLE order_execution ( execution_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_id BIGINT UNSIGNED NOT NULL, executed_at DATETIME NOT NULL, executor_id BIGINT UNSIGNED NOT NULL COMMENT '执行护士', result_text VARCHAR(200) NULL, KEY idx_exec_order_time (order_id, executed_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='医嘱执行记录';逻辑说明:admit_time 和 discharge_time 的 CHECK 约束在 MySQL 8 里生效,5.7 会忽略,所以应用层还要再校验一次;bed_occupancy 不加强制唯一约束,因为同一张床不同时间段可以给不同病人,唯一性靠业务层加锁加时段判断,或者用定时巡检 SQL 找重叠;医嘱的 start_time 和 stop_time 不要求 stop_time 大于 start_time 的 CHECK,因为长期医嘱可能先开立后补停止时间,但执行记录的 executed_at 不能早于医嘱 start_time,这条在应用层拦。
参数说明:deposit_cents 存预交金,住院期间每次记账写 charge_record,出院结算时预交金合计减费用合计;status 字段用 tinyint,别用中文,后面加状态不用改表;order_type 用编码,展示层查字典表,统计时按编码分组。
本文还有配套的精品资源,点击获取