医院管理系统数据库设计:从患者主索引到费用库存闭环的建表指南
发布时间:2026/10/2 15:16:23来源:尧图网络
简介医院信息系统HIS数据库设计是医疗信息化建设的核心环节涉及门诊、住院、药房、财务等多部门、多角色的复杂数据关系。此文档面向数据库设计人员、医疗信息化从业者及高校相关专业学生以医院真实业务流程为背景梳理各部门的功能划分与协作方式并重点讲解需求分析、ER图建模、关系表设计、外键关联、索引优化等内容同时对数据完整性、一致性、安全性、系统性能、可扩展性及法规遵从性等关键设计原则给出说明。资源包内为1个docx文档总大小398KB内容以文字方案和图表设计为主便于查阅和二次修改。目前已有173人学习下载。文档结合具体业务场景详述了病人挂号、缴费、取药、检查、住院、手术等环节的数据流转与存储需求并给出实体划分、约束设置、索引设计等实操建议可作为医院管理系统数据库课程设计、毕业设计或项目实现的参考模板。1. 医院管理系统数据库设计先想清楚这 20 张表是给谁用的医院管理系统数据库设计听起来是张表结构清单实际上一开始就要回答一个总被跳过的问题这些表到底要撑起谁的业务课程作业里用户信息表、挂号表、药品表画出来能跑增删改查就算交差真实 HIS 项目里患者、挂号、收费、药库、结算必须串成一条丢不了数据、重复计费就会出事的闭环。这中间差的不是表数量是对状态、金额、并发这三个词的理解。这篇笔记按后者来写先讲表为什么这么拆再给建表脚本、字段参数和踩坑记录最后是一份可以直接拿去评审的检查清单。适合正在做课设、毕业设计或者要接手医院类系统数据库设计的人照着复现。2. 患者主索引与挂号模型这 5 张表决定系统能不能撑住门诊高峰门诊系统最大的压力点不在并发而在“同一个患者被抓出来三份档案”。患者挂号、医生写病历、药房发药、收费结算全都要先定位到人。如果患者数据乱后面所有表的关联都跟着出问题。这一章先把用户、患者、挂号、就诊这几张地基表讲透。2.1 用户信息表与患者表为什么要拆开两种完全不同的实体很多实训平台把医院管理系统数据库设计拆成“第 1 关用户信息表”起步这一步题目常常是让建一张包含用户名、密码、角色、科室的 user 表。没问题但注意这里说的“用户”指的是能登录系统的人——医生、护士、药师、管理员、收费员。而“患者”是不登录系统的他只是被服务的人两者的字段差异大到没法塞进一张表。医生有工号、职称、科室、排班患者有证件号、过敏史、医保类型、紧急联系人。如果你把患者也塞进 user 表就得给大部分行留空字段更严重的是权限模型会乱一个收费员理论上不该有权限开处方如果都在一张表里角色判断的边界就会被模糊掉。所以我的习惯是分两张表sys_user 管账号和权限patient 管就诊人档案中间通过创建关系、就诊关系关联而不是混在一起。还要说一个和博客系统用户表的差别。博客系统的 user 表加个 role 字段就够了因为博主和读者的字段几乎一样医院不行患者档案里的出生日期要精确到天、证件号要校验、过敏史是结构化字段这些在账号表里都属于彻底无关的冗余。拆表不是过度设计是两类数据生命周期不同账号可能因为离职被禁用但患者的就诊档案要保留几十年。2.2 患者主索引证件号有重号、有换号不能拿来做外键患者表建好后紧接着就是主键选什么。最常见的新手做法是直接拿身份证号当主键理由是身份证唯一且稳定。但真实医院场景它撑不住原因有三个第一身份证号要脱敏存储和处理直接当主键等于明文管理第二未上户口的婴儿、外籍人员没有身份证号第三同一患者可能用身份证挂一次号、用医保卡又挂一次如果两套卡号都关联到不同 patient_id这个人在系统里就是两个人。所以业界用患者主索引EMPI来解决。简单说就是系统维护一个全局唯一的 patient_id无论患者用身份证、医保卡还是自费卡建档都通过证件类型加证件号、姓名加出生日期等规则去匹配已有的 patient_id匹配上了就复用匹配不上才新建。所有业务表只引用 patient_id证件号只是普通字段加唯一索引但允许为空。设计文档里这张表至少要包含patient_id主键、id_card证件号、id_type证件类型、name、gender、birth_date、medical_no医保卡号、phone、address、allergy过敏史。其中 gender 建议用 tinyint 字典0 未知 1 男 2 女别直接用字符串后面写接口和统计都省事。birth_date 用 DATE不要用 VARCHAR不然年龄计算和按出生年统计会变成一场灾难。2.3 挂号与就诊退号不是删记录是状态流转挂号表 registration 是门诊的第一个流量入口。它记录的是“某位患者在某一天挂了某个科室某个医生的某个号别”。核心字段有 reg_id、patient_id、visit_date、time_slot时段上午/下午/晚间、dept_id、doctor_id、reg_type普通/专家/急诊、status待就诊/已就诊/已退号/爽约、fee。这里有个设计原则退号只能做状态流转不能物理 DELETE。原因很实际退号率是门诊管理的考核指标财务要对账医生排班要统计爽约一旦删了记录报表全是窟窿。我见过把退号做成 DELETE 的早期版本月底统计退号率时永远少算最后只能从操作日志里手工捞数据补。所以状态字段从建表第一天就定死0 待就诊、1 已就诊、2 已退号、3 爽约谁改的、什么时候改的另行记录在操作日志表里。就诊表 visit 则挂更高一层的概念一次“就诊”是指患者实际接受诊疗的过程。理论上一次挂号对应一次就诊但真实情况里专家让患者先去做检查检查完回来复诊有的系统会让同一次挂号内产生多条 visit 记录住院患者根本没有挂号直接办入院就产生 visit。所以 visit 表建议独立于 registration用 reg_id 可空外键关联。visit 表至少要有 visit_id、patient_id、reg_id可空、visit_type门诊/急诊/住院/体检、dept_id、doctor_id、visit_start_time、visit_end_time、status。这样一来处方、检验、检查、费用明细全部挂在 visit_id 下而不是挂在挂号下数据链路就清晰了。2.4 五张核心表的字段清单参考下面这张表可以直接抄进你的《医院管理系统数据库设计.docx》文档里。类型以 MySQL 8 为例主键统一用 BIGINTID 生成策略见第 4 章。表名核心字段类型约束与说明sys_useruser_id, username, password_hash, real_name, role, dept_id, phone, statusBIGINT, VARCHAR(50), VARCHAR(100), TINYINT, BIGINTusername 唯一role 1 管理员 2 医生 3 药师 4 收费员 5 护士patientpatient_id, id_card, id_type, name, gender, birth_date, medical_no, phone, allergyBIGINT, VARCHAR(18), TINYINT, VARCHAR(50), TINYINT, DATEid_card 加唯一索引但允许为空id_type 1 身份证 2 医保卡 3 护照 4 港澳台registrationreg_id, patient_id, dept_id, doctor_id, visit_date, time_slot, reg_type, status, feeBIGINT联合索引 (dept_id, visit_date, time_slot) 查剩余号源status 用 TINYINT 字典visitvisit_id, patient_id, reg_id, visit_type, dept_id, doctor_id, visit_start_time, visit_end_time, statusBIGINTreg_id 可空住院场景不经过挂号表op_loglog_id, operator_id, op_type, target_table, target_id, before_json, after_json, created_atBIGINT记录关键表的变更前/后内容做审计后悔药注意 id_card 加唯一索引但不做主键这个细节承上启下唯一索引保证不重复建档主键用语义无关的 ID 保证换号、改号不影响业务表关联。3. 费用与药库闭环收费项目、处方明细与库存流水怎么串成一条链门诊高峰时一个药师一小时发两百盒药收费员一上午对一百张处方。如果费用和库存的数据模型是断的轻则对账不平重则药房发错药、财务查不出钱去哪了。这一章把“医生开单—收费—药房发药—月底盘点”整个闭环串起来。3.1 收费项目字典价格是字典的字段不是业务表的字段医生开的处方上写的是“阿莫西林胶囊 0.5g×24 粒”收费时要知道它的单价药房要知道它的规格。如果每次开单都把价格和规格复制一份到处方明细表里短期看着方便一旦医保调价或者药品换包装历史账单就全部失真——结算单上显示的是旧价格财务新老对不上。所以要有 charge_item 收费项目字典表。凡是能收钱的东西都算“项目”药品、检查、检验、治疗、材料、挂号费统一进这一张表。字段至少包含 item_code项目编码、item_name项目名称、category项目分类、specification规格、unit单位、unit_price单价、pinyin_code拼音码用于医生开单时简拼检索、enabled是否在架、effective_date。effective_date 是容易被忽略但必须有的字段。调价不是把 unit_price 改了就行而是要支持“调价前开的单按老价结算调价后按新价结算”。常见做法是同一 item_code 的多条记录用 effective_date 区分版本计价引擎取就诊时间对应的版本。顺便说一句苍穹外卖那些课设项目的菜品表之所以不用管价格版本是因为外卖菜品调价不涉及医保对账和退款追溯而医院收费每一笔都可能在几天后退费重算没有版本寸步难行。3.2 处方明细与费用账单一单一明细金额计算放在收费引擎处方主表 prescription 挂在 visit_id 下记录一次就诊开了哪些处方。字段有 prescription_id、visit_id、prescription_type西药/中成药/检验/检查/治疗、doctor_id、status已开立/已收费/已发药/已作废、created_at。处方明细表 prescription_detail 则一行一条项目字段有 detail_id、prescription_id、item_code、quantity、dosage、frequency、days、unit_price。设计这笔金额时记住一条原则明细表里可以冗余“收费时的单价”但不要把“应收金额”和“实收金额”混进去。因为医院计费有三种来源自费、医保、公费医保还要分甲类乙类自付比例。最终应收金额由收费引擎根据项目类别、医保类型、折扣规则计算得出结果写进费用表 fee。fee 表的最小字段是 fee_id、visit_id、patient_id、fee_type自费/医保/公费、total_amount、discount_amount、actual_amount、status待结算/已结算/已退费、settle_time。为什么不在明细表里直接写死应收因为一张处方可能部分自费部分医保还可能在第 20 天来退费退费只退医保范围内的那一小项。如果每行明细都有自己的一套金额逻辑退费功能就是一场噩梦。反过来明细只保留“数量 × 收费单价”的原始数据金额计算全部集中在收费引擎退费时重算就行。3.3 药库两张表库存表和流水表先写流水再改库存药库的模型比一般进销存多一个约束同一药品不同批号、不同有效期要分开管理因为医院有近效期预警和批号追踪的硬要求。库存表 drug_stock 的主键建议是复合的 (drug_id, batch_no)字段有 drug_id、batch_no、expire_date、quantity、unitquantity 只表示当前结存。另一张表 stock_flow 记录每一次数量变化字段有 flow_id、drug_id、batch_no、change_type入库/出库/报损/退库/盘点调整/期初、change_quantity正数入负数出、before_quantity、after_quantity、operator_id、created_at、ref_id关联的处方或单据号。出库发药时的正确事务顺序是先写 stock_flow 流水再 UPDATE drug_stock 扣减结存两步在同一个数据库事务里。流水表的 before/after 快照就是日后盘点的后悔药——月底盘亏一盒药翻流水能倒查到是哪一笔处方、哪个窗口、哪个操作者。如果只改库存不写流水盘亏时数据表里干干净净你连从哪查起都不知道那种黑匣子状态做一次就知道有多难受。3.4 费用与库存相关表的字段清单参考费用与库存一共涉及六张表charge_item、prescription、prescription_detail、fee、drug_stock、stock_flow。核心字段见下表。表名核心字段类型约束与说明charge_itemitem_code, item_name, category, specification, unit, unit_price, pinyin_code, effective_date, enabledVARCHAR / DECIMAL(10,2)item_code 加历史版本唯一索引 (item_code, effective_date)prescriptionprescription_id, visit_id, prescription_type, doctor_id, statusBIGINT联合索引 (visit_id, status) 支撑“某次就诊有哪些未收费处方”prescription_detaildetail_id, prescription_id, item_code, quantity, unit_price, dosage, frequency, daysBIGINT / DECIMAL(10,2)不存应收金额只存原始数量和开单单价feefee_id, visit_id, patient_id, fee_type, total_amount, discount_amount, actual_amount, status, settle_timeBIGINT / DECIMAL(10,2)金额一律 DECIMAL不允许 FLOATdrug_stockdrug_id, batch_no, expire_date, quantity, unitDECIMAL(10,3)复合主键 (drug_id, batch_no)数量允许三位小数stock_flowflow_id, drug_id, batch_no, change_type, change_quantity, before_quantity, after_quantity, operator_id, ref_idBIGINT / DECIMAL(10,3)before/after 快照必须记录用于审计4. 把 ER 图落成可执行的建表脚本字符集、主键策略与三条索引规则设计文档画到 ER 图只是完成一半另一半是把图变成能跑、能扛并发、能导入历史数据的 DDL。这一章从建库语句开始把字符集、主键、外键、索引四个从文档到落地最容易返工的地方讲透。4.1 建库之前的三个决定字符集、引擎与 ID 生成第一个决定是字符集。MySQL 8 默认就是 utf8mb4它能存四字节表情和生僻字而医院患者姓名里的生僻字比你想的多得多“玥”“彧”这些都算常见了。排序规则用 utf8mb4_general_ci 还是 utf8mb4_unicode_ci对医院系统这种重查询轻排序的场景差别不大统一用一种就行但整库至少不要混用两种排序规则否则 JOIN 时索引失效。第二个决定是引擎生产必须用 InnoDB。理由不是“默认”而是 InnoDB 有行锁和事务。第 3 章说到的“先写流水再扣库存”依赖事务MyISAM 连事务都没有真做并发发药会直接翻车。第三个决定是主键 ID 怎么生成。单机课设用自增 BIGINT 最省事但如果你做的是多院区或者要合并历史库自增主键会撞车。医院系统我的习惯是业务主键用雪花 IDBIGINT 存储审计类流水表用自增。雪花 ID 本身带时间信息拆库、合并、分表都方便。设计文档里主键字段统一写成 BIGINT NOT NULL COMMENT 雪花ID比让读者自己选省一个坑。4.2 从设计文档到 DDL患者、挂号、就诊三表的最小建表脚本下面这段 SQL 是前面两张表的最小落地版本可以直接在 MySQL 8 里执行。为了突出“设计文档怎么翻译成 DDL”外键我先用普通索引代替原因在本章最后说明。-- 患者主索引表档案与账号分离 CREATE TABLE patient ( patient_id BIGINT NOT NULL COMMENT 雪花ID, id_card VARCHAR(18) NULL COMMENT 证件号允许为空, id_type TINYINT NOT NULL DEFAULT 1 COMMENT 证件类型1身份证 2医保卡 3护照, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT NOT NULL DEFAULT 0 COMMENT 0未知 1男 2女, birth_date DATE NOT NULL COMMENT 出生日期, medical_no VARCHAR(32) NULL COMMENT 医保卡号, phone VARCHAR(20) NULL COMMENT 联系电话, allergy VARCHAR(255) NULL COMMENT 过敏史自由文本, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (patient_id), UNIQUE KEY uk_patient_idcard (id_card), KEY idx_patient_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT患者档案表; -- 挂号表退号通过状态流转 CREATE TABLE registration ( reg_id BIGINT NOT NULL COMMENT 挂号流水ID, patient_id BIGINT NOT NULL COMMENT 患者ID, dept_id BIGINT NOT NULL COMMENT 科室ID, doctor_id BIGINT NOT NULL COMMENT 医生ID, visit_date DATE NOT NULL COMMENT 就诊日期, time_slot TINYINT NOT NULL COMMENT 1上午 2下午 3晚间, reg_type TINYINT NOT NULL DEFAULT 1 COMMENT 1普通 2专家 3急诊, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待就诊 1已就诊 2已退号 3爽约, fee DECIMAL(8,2) NOT NULL DEFAULT 0 COMMENT 挂号费, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (reg_id), KEY idx_reg_dept_date (dept_id, visit_date, time_slot), KEY idx_reg_patient_date (patient_id, visit_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT门诊挂号表;这段脚本里有两个参数值得解释。dept_id 和 doctor_id 没有建外键只建了普通索引一方面是因为它们关联的是科室表和医生表而医生表属于 sys_user 体系另一方面是医院数据库经常要拆库物理外键在迁移时是最先被删的东西。第二个关键是 idx_reg_dept_date 联合索引它正好给“某天某科室还剩多少号”这个最热门的查询用叶子节点已经按科室和时间排好序MySQL 走索引就能数出号源不用回表。如果你做课设可以把外键补上让 ER 图跟库一致但生产环境我强烈建议外键只存在于设计文档的“逻辑外键”说明里物理外键删掉只保留索引。逻辑外键保证数据一致性靠的是应用层事务物理外键在导入历史数据时是最大的性能杀手第 5 章有对应踩坑记录。提示外键决策按数据量来。10 万行以内的课设数据物理外键带来的便利大于开销百万级的历史导入物理外键是第一个被拆掉的。4.3 索引怎么加三个高频查询决定索引组合医院门诊库的高频查询不超过十个索引设计别贪多每张表三到五个就够。我一般会按查询条件反推挂号剩余号源WHERE dept_id ? AND visit_date ? AND time_slot ?对应 idx_reg_dept_date。患者历史挂号WHERE patient_id ? ORDER BY visit_date DESCpatient_id 单列索引就行或者联合 (patient_id, visit_date) 让覆盖索引直接返回。医生某天排班WHERE doctor_id ? AND visit_date ? AND visit_date ?建议 (doctor_id, visit_date)。一个高频误用是在 status 字段上建单列索引。status 的取值就 0 到 3区分度极低优化器大概率全表扫也不会走索引。真要按状态过滤把它放到联合索引的末尾比如 (dept_id, visit_date, status)还能省一次回表。索引字段的选择直接决定查询是毫秒级还是秒级这个在数据量小的课设里看不出来导入几十万条历史数据后立见分晓。5. 医院管理系统数据库设计避坑五个让我返工过的实际问题数据库设计的问题往往要上线后一个月才暴露而修复成本是设计阶段的几十倍。下面五条是医院类系统里很常见的坑按“现象—原因—解决”写清楚能帮你少走一半弯路。5.1 患者手机号被当主键一个人给全家人挂号就炸了现象患者表用手机号 mobile 做主键设计评审时看着挺合理——手机号唯一嘛。上线后第一个月没事第二个月有位用户用自己手机号给家里老人挂号系统提示手机号已存在建档失败。而且老人后来换手机号所有关联记录都跟着改。原因把业务上的“唯一值”当成了主键。手机号是业务属性可能换、可能被多个就诊人共用主键却要求语义无关且永久不变。解决patient_id 独立成雪花 ID 主键mobile 只是普通字段允许为空按业务需要建唯一索引。如果确实要支持“一个手机号绑多个就诊人”手机号就连唯一索引都不要建放到一个单独的账号绑定表里。5.2 金额字段用 FLOAT对账差两分钱查了一下午现象收费模块每天对账都差几分钱打印出来的小票金额出现 9.999999 这种值。原因FLOAT 是二进制浮点无法精确表示 0.1乘法加法累积出误差。解决金额统一 DECIMAL(10,2)数量和剂量用 DECIMAL(10,3)因为有的药品按克开。这个坑只要在设计文档里出现一个 FLOAT 金额后面所有表都要跟着改一遍所以我的习惯是文档里直接约定凡涉及钱和数量的列一律 DECIMAL并在字段说明里写死精度。5.3 药品库存直接 UPDATE超卖只会在月底盘点时暴露现象发药高峰期两位药师同时在窗口发同一盒药库存显示 1 盒两个处方都提示发药成功。月底盘点库里少了 1 盒翻流水怎么都对不上。原因发药逻辑写成了“先 SELECT 判断库存够不够再 UPDATE 扣减”。两条语句之间没有锁并发时两个请求都看到库存 1都判断“够”然后都执行扣减互相覆盖。解决把扣减写成一条原子语句UPDATE drug_stock SET quantity quantity - 1 WHERE drug_id ? AND batch_no ? AND quantity 1然后用受影响行数判断是否扣成。如果还要同时写流水就必须放在同一个事务里。这个改动成本极低但能直接把超卖概率降为零。5.4 时间字段存成字符串跨院区查询时区直接乱掉现象某院区导出的挂号记录时间比实际晚 8 小时按天分组的统计全部错位。原因设计文档里 visit_time 字段类型写的是 VARCHAR写入时程序本地拼接字符串不同院区服务器时区不一致存进去的就是错的时间。解决所有时间一律用 DATETIME 或 TIMESTAMP应用层统一时区数据库连接串指定 serverTimezone。另一个容易忽略的是“只存某一天”的业务字段比如挂号的 visit_date直接用 DATE不要用 DATETIME 再套 DATE() 函数去取否则索引直接失效。注意如果时间字段已经是 VARCHAR 的老库迁移时用 STR_TO_DATE 洗一遍别试图在查询层用函数兼容那样索引永远用不上。5.5 外键建太多导入十万条历史数据卡到怀疑人生现象把老系统数据导入新库10 万条挂号记录导了 4 个小时中途还因为一条孤儿记录外键报错中断。原因设计文档里给每张子表都写了物理 FOREIGN KEY。导入时逐条校验外键、还要按主表到子表的顺序十万条数据放大十倍代价。老系统里本来就有“患者档案已删、挂号记录还在”的孤儿数据物理外键直接拦截。解决生产库用逻辑外键只建索引不建约束数据清洗阶段把孤儿记录单独标记归档。设计文档里外键照画但要在“约束说明”列写明物理外键仅适用于开发环境生产环境删除。课设数据量小物理外键没问题但养成这个习惯以后接真实项目能省很多事。6. 交付前的验证 SQL 与清单十分钟查出设计文档里的隐藏问题设计文档写完不等于设计完成我会在上线前跑三条验证 SQL 和一份十项检查清单十分钟内把文档里的隐藏问题暴露出来。6.1 三条验证 SQL-- 1. 查出绑定了就诊记录却找不到患者档案的孤儿数据 SELECT r.reg_id, r.patient_id FROM registration r LEFT JOIN patient p ON p.patient_id r.patient_id WHERE p.patient_id IS NULL; -- 2. 对账挂号明细金额汇总不等于收费汇总账就平不了 SELECT r.patient_id, SUM(r.fee) AS reg_fee FROM registration r WHERE r.status IN (0, 1) GROUP BY r.patient_id; -- 3. 库存不能为负出现负数说明发药逻辑没做原子扣减 SELECT drug_id, batch_no, quantity FROM drug_stock WHERE quantity 0;这三条 SQL 执行的顺序就是设计的验收顺序先验数据缝合得紧不紧再验钱平不平最后验库存逻辑有没有并发漏洞。任何一条查出异常都说明设计文档对应章节要返工而不是修数据。6.2 十项检查清单检查项判定标准主键是否语义无关所有业务表主键是 ID不是证件号/手机号金额与数量字段全部 DECIMAL不允许 FLOAT时间字段类型时间用 DATETIME/TIMESTAMP日期用 DATE业务记录能否 DELETE挂号、处方、费用记录只允许状态流转外键策略文档描述逻辑外键生产脚本不带物理外键高频查询有无对应索引挂号号源、患者历史、医生排班都有联合索引状态字段是否字典化gender/status 用 TINYINT 并注释枚举值收费是否有流水费用表有结算状态退费走状态不删记录患者能否合并档案patient 独立主索引重复建档有合并机制权限边界sys_user 角色与业务表操作权限一一对应我的习惯是交文档前把这份清单打印出来逐项打勾。以前我总跳过“业务记录能否 DELETE”这一项觉得退号删记录多干净直到被报表数据打脸才改过来。现在任何一张业务表我都会先问一句这一行要是删了月底的报表还准吗不准就只改状态、不删行。这个习惯救了我很多次希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网