简介:这份资源是面向软件工程、计算机及相关专业学生的数据库课程设计参考文档,聚焦医院门诊管理系统的数据库设计,适合正在完成课程设计或需要数据库建模实战案例的学习者。压缩包内共1个doc文件,约731KB,内容为完整的课程设计论文,涵盖需求分析、数据流程图、数据字典、数据项与数据结构定义、数据流与处理逻辑描述,以及概念设计、E-R图、逻辑设计、物理设计和数据库实施与测试等章节,并给出SQL Server 2008环境下的建库建表与数据入库思路。读者可借此掌握从需求梳理到E-R建模、关系模式规范化、物理存储设计的完整流程,理解挂号、诊断、收费等门诊业务的数据组织方式,也可作为撰写课程设计论文的结构与内容参考。目前已有2832人学习下载,具备一定的参考热度。
1. 医院门诊管理系统数据库设计:从课程设计到能跑起来的 MySQL 库
医院门诊管理系统这个题目,几乎每年都会出现在数据库课程设计的选题清单里。很多人第一反应是去搜一份现成的.doc文档,把表结构抄下来交差。但真正做过一遍的人都知道,这个题目的价值不在文档本身,而在于你能不能把「挂号—就诊—开方—收费—发药」这条业务链在 MySQL 里完整地建模出来,并且让 SQL 跑得通、查得对。我见过太多同学的库建到一半就卡住:挂号表和就诊表的关系理不清,处方明细和药品库存对不上,收费记录重复插入。这篇笔记就按一线做项目的思路,把医院门诊管理系统的数据库设计从需求拆解、表结构落地、约束与索引、到常见翻车点讲透。适合正在做数据库课程设计、需要交一份能演示的 MySQL 库、或者想拿这个题目练手的同学。读完你应该能自己从零建出一个能插入数据、能联表查询、能解释每张表为什么这么设计的门诊库。
2. 门诊业务到底要落哪几张表:先画业务流再定实体
2.1 从挂号到发药,把业务流拆成实体
数据库设计最容易犯的错,是打开 Navicat 就开始建表。我一般会先在纸上把门诊一天的业务流走一遍:患者到院 → 挂号(选科室、选医生、付挂号费)→ 候诊 → 医生接诊(写病历、开处方、开检查)→ 患者缴费 → 药房发药 / 检查科室执行。这条链上每一个「动作」和「对象」都是潜在的实体。
拆实体有个笨办法但很管用:把业务流里的名词圈出来。患者、科室、医生、排班、挂号单、病历、处方、处方明细、药品、收费单、收费明细。这些名词里,有的是独立存在的对象(患者、医生、药品),有的是两个对象之间的关系(排班是医生和科室、时间的组合,挂号单是患者和排班的组合)。区分「实体」和「关系」直接决定了你后面是建一张表还是建两张表加外键。
以挂号为例。很多课程设计文档里把挂号做成患者表的一个字段,这是典型的翻车起点。一个患者今天挂内科、下周挂外科,挂号记录会有多条,塞进患者表就没法存。正确做法是单独建registration表,用patient_id和schedule_id两个外键指向患者和排班。同理,处方和处方明细必须拆开:处方是「一次开方行为」,明细是「这次开了哪几种药、各多少」。拆开之后,一张处方对应多条明细,查询和统计才有得做。
这里给一个我常用的实体清单,你可以对照自己的需求增删:
| 实体/关系 | 对应表名 | 说明 |
|---|---|---|
| 患者 | patient | 独立实体,存基本信息 |
| 科室 | department | 独立实体 |
| 医生 | doctor | 独立实体,关联科室 |
| 排班 | schedule | 关系,医生+科室+时段 |
| 挂号单 | registration | 关系,患者+排班 |
| 病历 | medical_record | 关系,挂号单+诊断 |
| 处方 | prescription | 关系,病历+药品集合 |
| 处方明细 | prescription_item | 关系,处方+药品+数量 |
| 药品 | drug | 独立实体,含库存 |
| 收费单 | payment | 关系,挂号单+金额 |
2.2 主键、外键、业务主键怎么选
实体定完,接下来是主键。课程设计里最常见的两种做法:用自增整数做主键,或者用业务编号(比如挂号单号REG20240501001)做主键。我的建议是自增整数做主键,业务编号单独加一个唯一索引字段。原因很实际:业务编号会变规则,今天REG开头,明天可能加个院区前缀;而且业务编号是字符串,做外键关联时索引体积大、联表慢。自增主键稳定、紧凑,业务编号只用来给用户看和做查询条件。
外键要不要加,是个有争议的点。教学场景我建议加,因为外键能帮你自动挡住脏数据,比如往挂号表插一个不存在的patient_id,数据库直接报错,省得你写代码去校验。生产环境很多团队会去掉外键改由应用层保证,那是为了分库分表和写入性能,课程设计阶段没必要。加外键时注意ON DELETE和ON UPDATE的行为:患者被删除时他的挂号记录怎么办?一般设RESTRICT,不允许删有挂号记录的患者,避免历史数据丢失。
业务主键的唯一约束别忘了。挂号单号、处方号、收费单号这些字段要加UNIQUE,否则并发插入时可能生成重复编号。虽然课程设计里并发场景少,但养成习惯没坏处。
2.3 用一张 ER 关系表把外键方向定死
外键方向搞反是新手高频错误。比如把doctor_id放在科室表里,意思是「一个科室只有一个医生」,这显然不对。判断方向的方法:看「多」的一方。一个科室有多个医生,所以doctor表里放department_id;一个医生有多个排班,所以schedule表里放doctor_id。
我把核心表的外键方向列成一张表,建表时照着对:
| 表 | 外键字段 | 指向 | 关系 |
|---|---|---|---|
| doctor | department_id | department.id | 多对一 |
| schedule | doctor_id | doctor.id | 多对一 |
| registration | patient_id | patient.id | 多对一 |
| registration | schedule_id | schedule.id | 多对一 |
| medical_record | registration_id | registration.id | 一对一 |
| prescription | record_id | medical_record.id | 多对一 |
| prescription_item | prescription_id | prescription.id | 多对一 |
| prescription_item | drug_id | drug.id | 多对一 |
| payment | registration_id | registration.id | 一对一 |
这张表定完,建表顺序也就出来了:先建没有外键的department、patient、drug,再建doctor,然后schedule,最后建带多个外键的registration、prescription_item。顺序错了会因为外键指向的表不存在而建表失败。
3. 建表 SQL 逐张写:字段类型和约束怎么定
3.1 基础表:患者、科室、医生、药品
先建四张不依赖别人的表。字段类型的选择原则:能用定长就别用变长,能用整数就别用字符串。手机号用CHAR(11)而不是VARCHAR,因为长度固定;金额用DECIMAL(10,2)而不是FLOAT,浮点数算钱会出现0.1+0.2=0.30000000000000004这种玄学问题。
-- 科室表:最基础,无外键 CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '科室主键', name VARCHAR(50) NOT NULL UNIQUE COMMENT '科室名称', location VARCHAR(100) COMMENT '科室位置', phone VARCHAR(20) COMMENT '科室电话' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科室表'; -- 患者表:身份证号做唯一约束,避免重复建档 CREATE TABLE patient ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '患者主键', name VARCHAR(50) NOT NULL COMMENT '姓名', gender CHAR(1) NOT NULL DEFAULT 'U' COMMENT '性别 M/F/U', birth_date DATE COMMENT '出生日期', id_card CHAR(18) NOT NULL UNIQUE COMMENT '身份证号', phone CHAR(11) COMMENT '手机号', address VARCHAR(200) COMMENT '联系地址', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '建档时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='患者表'; -- 医生表:关联科室 CREATE TABLE doctor ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '医生主键', name VARCHAR(50) NOT NULL COMMENT '姓名', department_id INT NOT NULL COMMENT '所属科室', title VARCHAR(20) COMMENT '职称', phone CHAR(11) COMMENT '手机号', CONSTRAINT fk_doctor_dept FOREIGN KEY (department_id) REFERENCES department(id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='医生表'; -- 药品表:库存字段单独放,方便扣减 CREATE TABLE drug ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '药品主键', name VARCHAR(100) NOT NULL COMMENT '药品名称', spec VARCHAR(50) COMMENT '规格', unit VARCHAR(10) COMMENT '单位', price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '单价', stock INT NOT NULL DEFAULT 0 COMMENT '库存数量', UNIQUE KEY uk_drug_name_spec (name, spec) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='药品表';逻辑说明:department.name加UNIQUE防止建出两个「内科」;patient.id_card加UNIQUE是门诊建档的核心约束,同一个人不能建两次档;drug用(name, spec)联合唯一,因为同名不同规格的药是两种药。参数上,utf8mb4是为了存生僻字姓名,InnoDB是为了支持外键和事务,这两个别改。
3.2 业务表:排班、挂号、病历、处方
这四张表是门诊系统的核心,外键多、约束多,建的时候要小心顺序。
-- 排班表:医生在某天某个时段出诊 CREATE TABLE schedule ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '排班主键', doctor_id INT NOT NULL COMMENT '医生', work_date DATE NOT NULL COMMENT '出诊日期', time_slot VARCHAR(20) NOT NULL COMMENT '时段 上午/下午', total_slots INT NOT NULL DEFAULT 0 COMMENT '总号源', left_slots INT NOT NULL DEFAULT 0 COMMENT '剩余号源', CONSTRAINT fk_schedule_doctor FOREIGN KEY (doctor_id) REFERENCES doctor(id) ON DELETE RESTRICT, UNIQUE KEY uk_doctor_date_slot (doctor_id, work_date, time_slot) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='排班表'; -- 挂号表:患者挂某个排班的号 CREATE TABLE registration ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '挂号主键', reg_no VARCHAR(30) NOT NULL UNIQUE COMMENT '挂号单号', patient_id INT NOT NULL COMMENT '患者', schedule_id INT NOT NULL COMMENT '排班', reg_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '挂号时间', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待诊 1已诊 2退号', fee DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '挂号费', CONSTRAINT fk_reg_patient FOREIGN KEY (patient_id) REFERENCES patient(id) ON DELETE RESTRICT, CONSTRAINT fk_reg_schedule FOREIGN KEY (schedule_id) REFERENCES schedule(id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='挂号表'; -- 病历表:一次挂号对应一份病历 CREATE TABLE medical_record ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '病历主键', registration_id INT NOT NULL UNIQUE COMMENT '挂号单', chief_complaint VARCHAR(500) COMMENT '主诉', diagnosis VARCHAR(500) COMMENT '诊断', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '就诊时间', CONSTRAINT fk_record_reg FOREIGN KEY (registration_id) REFERENCES registration(id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='病历表'; -- 处方表:一份病历可开多张处方 CREATE TABLE prescription ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '处方主键', presc_no VARCHAR(30) NOT NULL UNIQUE COMMENT '处方号', record_id INT NOT NULL COMMENT '病历', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '开方时间', CONSTRAINT fk_presc_record FOREIGN KEY (record_id) REFERENCES medical_record(id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='处方表';逻辑说明:schedule的uk_doctor_date_slot保证同一个医生同一天同一时段只有一条排班,避免重复放号;registration.reg_no唯一,防止单号重复;medical_record.registration_id加UNIQUE是因为一次挂号只对应一份病历,这是一对一关系,用唯一约束强制。status用TINYINT存状态码而不是字符串,查询快、占空间小,但要在注释里写清楚每个数字的含义,否则过两周自己都看不懂。
3.3 明细表:处方明细和收费明细
明细表是「一对多」里「多」的那一端,字段少但插入频繁。
-- 处方明细:一张处方多种药 CREATE TABLE prescription_item ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '明细主键', prescription_id INT NOT NULL COMMENT '处方', drug_id INT NOT NULL COMMENT '药品', quantity INT NOT NULL DEFAULT 1 COMMENT '数量', usage_desc VARCHAR(200) COMMENT '用法用量', CONSTRAINT fk_item_presc FOREIGN KEY (prescription_id) REFERENCES prescription(id) ON DELETE CASCADE, CONSTRAINT fk_item_drug FOREIGN KEY (drug_id) REFERENCES drug(id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='处方明细表'; -- 收费单:一次挂号对应一张收费单 CREATE TABLE payment ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '收费主键', pay_no VARCHAR(30) NOT NULL UNIQUE COMMENT '收费单号', registration_id INT NOT NULL UNIQUE COMMENT '挂号单', total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '总金额', pay_method VARCHAR(20) COMMENT '支付方式', pay_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '收费时间', CONSTRAINT fk_pay_reg FOREIGN KEY (registration_id) REFERENCES registration(id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收费单表';逻辑说明:prescription_item的外键用ON DELETE CASCADE,因为明细脱离处方没有意义,删处方时明细跟着删是合理的;而drug_id用RESTRICT,药品被引用时不允许删。payment.registration_id加UNIQUE保证一次挂号只收一次费,防止重复收费。金额字段统一DECIMAL(10,2),别用FLOAT。
提示:建表顺序必须是 department → patient → drug → doctor → schedule → registration → medical_record → prescription → prescription_item → payment。顺序错了会报
errno: 150外键错误。
4. 索引、约束和事务:让库不只是能建还要能用
4.1 该加索引的字段和不该加的字段
索引不是越多越好。每加一个索引,插入和更新都要多维护一棵 B 树。门诊系统里真正需要索引的是高频查询条件:患者按手机号查、挂号按日期查、处方按病历查。
-- 患者按手机号查询 CREATE INDEX idx_patient_phone ON patient(phone); -- 挂号按日期和状态查(比如查今天所有待诊) CREATE INDEX idx_reg_time_status ON registration(reg_time, status); -- 处方明细按药品统计用量 CREATE INDEX idx_item_drug ON prescription_item(drug_id);逻辑说明:idx_reg_time_status是联合索引,把reg_time放前面是因为日期范围查询更常见,status放后面用于过滤。联合索引有最左前缀原则,WHERE reg_time = ... AND status = ...能命中,单独WHERE status = ...命中不了。参数上,区分度低的字段(比如性别只有 M/F/U)不要单独建索引,建了也用不上。
不该加索引的:patient.address这种长文本、medical_record.chief_complaint这种大字段,建索引体积大且查询少。外键字段 MySQL 会自动建索引,不用手动再加。
4.2 用事务保证挂号扣号和收费一致
门诊系统里有两个操作必须用事务包起来,否则数据会不一致。第一个是挂号:插入挂号记录的同时要扣减排班的剩余号源。如果插入成功但扣号失败,号源就超卖了。
START TRANSACTION; -- 扣号源,同时检查是否还有号 UPDATE schedule SET left_slots = left_slots - 1 WHERE id = 1 AND left_slots > 0; -- 检查影响行数,如果为 0 说明没号了,回滚 -- 应用层判断 affected_rows == 1 才继续 INSERT INTO registration (reg_no, patient_id, schedule_id, fee) VALUES ('REG20240501001', 1, 1, 10.00); COMMIT;逻辑说明:UPDATE ... WHERE left_slots > 0这个条件很关键,它把「检查」和「扣减」合成一个原子操作,避免先查后扣之间的并发问题。应用层拿到affected_rows,如果是 0 就ROLLBACK并提示无号。第二个事务是收费:插入收费单的同时更新挂号状态为已收费,两个操作要么都成功要么都失败。
4.3 视图和存储过程:课程设计里的加分项
课程设计如果只建表,分数一般。加一两个视图和存储过程,能体现你理解「数据库不只是存数据」。视图适合把常用的联表查询封装起来,比如「今日待诊患者列表」:
CREATE VIEW v_today_waiting AS SELECT r.reg_no, p.name AS patient_name, d.name AS doctor_name, dep.name AS dept_name, r.reg_time FROM registration r JOIN patient p ON r.patient_id = p.id JOIN schedule s ON r.schedule_id = s.id JOIN doctor d ON s.doctor_id = d.id JOIN department dep ON d.department_id = dep.id WHERE DATE(r.reg_time) = CURDATE() AND r.status = 0;逻辑说明:视图把五张表的联表逻辑封装成一个「虚拟表」,查询时直接SELECT * FROM v_today_waiting就行。注意视图不存数据,每次查都实时算,所以底层表的索引要建好。存储过程可以封装「开处方并扣库存」这种多步操作,但课程设计里视图的性价比更高,先把这个做扎实。
5. 避坑与排查:课程设计里最容易翻车的 5 个点
5.1 外键报错 errno 150:建表顺序和字段类型不一致
现象:建registration表时报ERROR 1005 (HY000): Can't create table ... (errno: 150)。原因有两个,一是被引用的表还没建,二是外键字段和主表主键的类型不完全一致,比如主键是INT UNSIGNED而外键写成了INT。解决:先按第 3 章的建表顺序建表,再用SHOW CREATE TABLE department看主键的完整类型,外键字段照抄,包括UNSIGNED和字符集。
5.2 中文乱码:字符集没统一成 utf8mb4
现象:插入患者姓名「张伟」后查出来是问号或乱码。原因:建表时用了默认字符集latin1,或者连接串没指定字符集。解决:建表语句统一加DEFAULT CHARSET=utf8mb4,连接时用jdbc:mysql://localhost:3306/clinic?useUnicode=true&characterEncoding=utf8。已经建好的表用ALTER TABLE patient CONVERT TO CHARACTER SET utf8mb4;转换。
5.3 金额算错:用了 FLOAT 导致精度丢失
现象:挂号费 10.00 加药费 23.50,算出来是 33.499999。原因:FLOAT和DOUBLE是二进制浮点,存不了精确的十进制小数。解决:所有金额字段用DECIMAL(10,2),Java 里对应BigDecimal,Python 里用decimal.Decimal。已经建的表用ALTER TABLE payment MODIFY total_amount DECIMAL(10,2);改。
5.4 重复挂号:业务编号没加唯一约束
现象:同一患者同一时段出现两条挂号记录,单号还一样。原因:reg_no只设了NOT NULL没设UNIQUE,应用层生成单号时并发插入。解决:给reg_no加UNIQUE KEY,插入时捕获唯一键冲突异常并重试。更稳的做法是用数据库序列或UUID生成单号,但课程设计里加唯一约束就够了。
5.5 删数据删不掉:外键 RESTRICT 挡住了
现象:想删一个测试患者,报Cannot delete or update a parent row。原因:该患者有挂号记录,外键设了ON DELETE RESTRICT。解决:这是正常保护,不是 bug。测试时先删挂号记录再删患者,或者临时SET FOREIGN_KEY_CHECKS=0;关掉检查(仅限测试环境,别在生产用)。设计时想清楚哪些关系允许级联删除,明细表可以CASCADE,主数据表一律RESTRICT。
6. 用 SQL 验证设计是否站得住:三个必跑的查询
设计完不验证,等于没设计。我一般会跑三个查询来检验表结构和索引是否合理。第一个是「某患者完整就诊记录」,把患者、挂号、病历、处方、明细全串起来,能跑通说明外键方向没错:
SELECT p.name, r.reg_no, mr.diagnosis, d.name AS drug_name, pi.quantity FROM patient p JOIN registration r ON p.id = r.patient_id JOIN medical_record mr ON r.id = mr.registration_id JOIN prescription pr ON mr.id = pr.record_id JOIN prescription_item pi ON pr.id = pi.prescription_id JOIN drug d ON pi.drug_id = d.id WHERE p.id_card = '110101199001011234';第二个是「某天各科室挂号量统计」,检验联合索引和分组查询:
SELECT dep.name, COUNT(*) AS reg_count FROM registration r JOIN schedule s ON r.schedule_id = s.id JOIN doctor doc ON s.doctor_id = doc.id JOIN department dep ON doc.department_id = dep.id WHERE DATE(r.reg_time) = '2024-05-01' GROUP BY dep.name ORDER BY reg_count DESC;第三个是「药品用量排行」,检验明细表的聚合性能:
SELECT d.name, SUM(pi.quantity) AS total_used FROM prescription_item pi JOIN drug d ON pi.drug_id = d.id GROUP BY d.id, d.name ORDER BY total_used DESC LIMIT 10;这三个查询跑通,基本能证明你的库不是「只能建不能查」的花架子。跑的时候用EXPLAIN看执行计划,重点看type是不是ref或range,key有没有命中你建的索引。如果出现ALL全表扫描,回去检查索引。
最后说个我自己的习惯:每次改完表结构,我都会把建表 SQL 全部导出成一个schema.sql,再写一个seed.sql塞几条测试数据,然后在一个空库上从头跑一遍。这个动作救过我很多次——本地改着改着忘了同步,换台机器就建不起来。课程设计答辩前,老师大概率会让你当场演示建库,能一条命令跑通整个 schema,比什么都稳。希望帮到你。
本文还有配套的精品资源,点击获取