简介:这份PDF是面向高校数据库课程设计场景的机票预订系统设计报告,适合正在完成大作业或需要参考完整设计流程的计算机相关专业学生。报告围绕模拟真实机票预订业务展开,从需求分析、系统功能划分到数据流图、数据字典、概念结构E-R图与逻辑结构关系模型,完整呈现了数据库设计的标准链路。资源包共1个PDF文件,大小约1.11MB,内容涵盖管理员与旅客双端功能设计、航班与旅客的M:N订购关系、退票信息表与取票通知单等关系模式,以及数据依赖极小化和第三范式分解过程,并附有Access物理表结构与操作界面说明。目前已有2396人学习下载,可作为课程设计报告撰写、数据库建模练习与E-R图到关系模型转换的参考材料,帮助读者理清需求到实现的整体思路。
1. 机票预订系统数据库设计:从课程设计到能跑起来的完整方案
机票预订系统这个题目,几乎每年都会出现在数据库课程设计的选题清单里。它看起来简单——不就是航班、乘客、订单三张表吗?但真正动手做过的同学都知道,这个场景的数据库设计远比想象中复杂:航班有经停和中转,座位有舱位等级和库存限制,订单涉及支付状态流转和退票规则,还要考虑并发选座时的锁竞争。我带过几届课程设计的答辩,见过太多方案在"能跑"和"能扛"之间翻车。
这篇内容面向正在做数据库课程设计的同学,也面向想用这个场景练手数据库建模的开发者。我会从需求拆解讲到表结构设计,再到索引、事务、并发控制,最后给出一个可以直接复现的 MySQL 实现方案。整套方案基于 MySQL 8.x,用 InnoDB 引擎,覆盖建库建表、增删改查、存储过程、触发器和查询优化。如果你正在搜"数据库课程设计 mysql"或者"机票预订系统 数据库设计",这篇应该能帮你把方案立住。
2. 需求拆解与实体关系建模:先想清楚再画 ER 图
2.1 机票预订系统到底有哪些核心实体
很多同学一上来就打开 Navicat 或者 dbx 数据库工具开始建表,结果建到一半发现字段不够用,又回头改,改完发现外键冲突。血泪经验是:先把实体和关系想清楚,再动手写 DDL。
机票预订系统的核心实体可以拆成这几类:
- 航班(flight):一次具体的飞行任务,包含航班号、起飞时间、降落时间、出发机场、到达机场、机型、总座位数。
- 航段(flight_segment):如果航班有经停,一个航班会拆成多个航段。比如北京-上海-广州,就是两个航段。这个设计是区分"直飞"和"经停"的关键。
- 舱位库存(cabin_inventory):每个航班每个舱位等级(经济舱、商务舱、头等舱)的座位数和已售数量。
- 乘客(passenger):乘机人信息,包括姓名、证件号、联系方式。
- 订单(booking_order):一次预订行为,关联乘客、航班、舱位、价格、支付状态。
- 支付记录(payment):订单的支付流水,支持多次支付(比如定金+尾款)。
- 退改签记录(refund_change):订单的退票或改签操作记录。
这七个实体基本覆盖了课程设计的要求。如果你的选题还要求会员积分、优惠券、航班动态,可以在此基础上扩展。
2.2 ER 图怎么画才不会被答辩老师挑刺
ER 图不是画得好看就行,关键是基数关系要标清楚。我见过最常见的错误是把"乘客"和"订单"画成一对一,实际上一个乘客可以有多张订单,一张订单也可以有多个乘客(多人同行)。所以乘客和订单之间是多对多,需要一张中间表order_passenger来关联。
另一个容易出错的地方是航班和航段的关系。如果你把航段直接塞进航班表用逗号分隔的字符串存,答辩老师大概率会让你回去重做。正确做法是独立建表,用flight_id做外键关联。
画 ER 图时建议用 Chen 记法或者 Crow's Foot 记法,标注清楚主键、外键、基数(1:1、1:N、M:N)。如果课程设计报告要求提交 PDF,建议用 draw.io 或者 Visio 导出矢量图,别用截图,放大就糊。
2.3 从 ER 图到关系模式的转换规则
ER 图到关系模式的转换有固定套路:
- 每个强实体转一张表,主键用代理键(自增 ID)或自然键。
- 1:N 关系把"1"端的主键放到"N"端做外键。
- M:N 关系单独建中间表,主键用联合主键或自增 ID。
- 多值属性单独建表。
- 弱实体依赖强实体的主键。
以订单和乘客为例,转换后得到:
booking_order表:order_id (PK), order_no, total_amount, status, create_timepassenger表:passenger_id (PK), name, id_card, phoneorder_passenger表:order_id (FK), passenger_id (FK), 联合主键
这样设计的好处是,一个订单可以关联多个乘客,一个乘客也可以出现在多个订单里,完全符合实际业务。
3. 表结构设计与 MySQL 建表实操:字段、类型、约束一次到位
3.1 七张核心表的 DDL 与字段选型理由
下面是我一般会用的建表语句,基于 MySQL 8.x,InnoDB 引擎,字符集 utf8mb4。字段类型的选择都有讲究,后面会逐条说明。
-- 航班表 CREATE TABLE flight ( flight_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, flight_no VARCHAR(10) NOT NULL COMMENT '航班号,如 CA1234', depart_airport CHAR(3) NOT NULL COMMENT '出发机场三字码', arrive_airport CHAR(3) NOT NULL COMMENT '到达机场三字码', depart_time DATETIME NOT NULL COMMENT '起飞时间', arrive_time DATETIME NOT NULL COMMENT '到达时间', aircraft_type VARCHAR(20) DEFAULT NULL COMMENT '机型', status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 2延误 3取消', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_flight_no_time (flight_no, depart_time), KEY idx_depart_arrive_time (depart_airport, arrive_airport, depart_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='航班表'; -- 航段表(支持经停) CREATE TABLE flight_segment ( segment_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, flight_id BIGINT UNSIGNED NOT NULL, segment_no TINYINT NOT NULL COMMENT '航段序号,从1开始', depart_airport CHAR(3) NOT NULL, arrive_airport CHAR(3) NOT NULL, depart_time DATETIME NOT NULL, arrive_time DATETIME NOT NULL, CONSTRAINT fk_segment_flight FOREIGN KEY (flight_id) REFERENCES flight(flight_id), UNIQUE KEY uk_flight_segment (flight_id, segment_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='航段表'; -- 舱位库存表 CREATE TABLE cabin_inventory ( inventory_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, flight_id BIGINT UNSIGNED NOT NULL, cabin_class CHAR(1) NOT NULL COMMENT 'F头等 C商务 Y经济', total_seats SMALLINT UNSIGNED NOT NULL, sold_seats SMALLINT UNSIGNED NOT NULL DEFAULT 0, price DECIMAL(10,2) NOT NULL, version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', CONSTRAINT fk_inventory_flight FOREIGN KEY (flight_id) REFERENCES flight(flight_id), UNIQUE KEY uk_flight_cabin (flight_id, cabin_class), CONSTRAINT chk_sold CHECK (sold_seats <= total_seats) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='舱位库存表'; -- 乘客表 CREATE TABLE passenger ( passenger_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, id_card VARCHAR(18) NOT NULL COMMENT '身份证号', phone VARCHAR(20) DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_id_card (id_card) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='乘客表'; -- 订单表 CREATE TABLE booking_order ( order_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT '订单号,业务生成', flight_id BIGINT UNSIGNED NOT NULL, cabin_class CHAR(1) NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已出票 3已退票 4已取消', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_flight_status (flight_id, status), KEY idx_create_time (create_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表'; -- 订单乘客关联表 CREATE TABLE order_passenger ( order_id BIGINT UNSIGNED NOT NULL, passenger_id BIGINT UNSIGNED NOT NULL, PRIMARY KEY (order_id, passenger_id), CONSTRAINT fk_op_order FOREIGN KEY (order_id) REFERENCES booking_order(order_id), CONSTRAINT fk_op_passenger FOREIGN KEY (passenger_id) REFERENCES passenger(passenger_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单乘客关联表'; -- 支付记录表 CREATE TABLE payment ( payment_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_id BIGINT UNSIGNED NOT NULL, pay_amount DECIMAL(10,2) NOT NULL, pay_method TINYINT NOT NULL COMMENT '1支付宝 2微信 3银行卡', pay_status TINYINT NOT NULL DEFAULT 0 COMMENT '0处理中 1成功 2失败', pay_time DATETIME DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_order (order_id), CONSTRAINT fk_payment_order FOREIGN KEY (order_id) REFERENCES booking_order(order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付记录表';字段选型有几个关键点:金额用DECIMAL(10,2)而不是FLOAT,避免浮点精度问题;机场三字码用CHAR(3)定长;舱位等级用CHAR(1)而不是ENUM,方便扩展;库存表加了version字段做乐观锁,后面并发控制会用到。
3.2 索引怎么加才不让查询变成全表扫
索引不是越多越好,每个索引都会拖慢写入。机票预订系统最核心的查询是"查某天从 A 到 B 的航班",所以flight表上建了idx_depart_arrive_time联合索引,顺序是出发机场、到达机场、起飞时间。这个顺序不能反,因为查询条件通常三个都用,遵循最左前缀原则。
订单表上建了idx_flight_status和idx_create_time,分别服务于"查某航班的订单"和"按时间范围查订单"。注意order_no上的唯一索引是必须的,业务上订单号不能重复。
一个常见的翻车场景是:在passenger表的name字段上建索引,结果发现查询还是慢。原因是姓名重复率高,索引选择性差,MySQL 优化器可能直接放弃索引走全表扫。这种字段要么不建索引,要么建前缀索引,但前缀长度要算好。
3.3 外键约束到底要不要用
课程设计里答辩老师经常问:"你为什么用外键?"或者"你为什么不用外键?"这个问题没有标准答案,但你要能说出理由。
用外键的好处是数据一致性有保障,删除航班时如果有订单关联会直接报错,不会产生孤儿记录。坏处是高并发写入时外键检查会增加锁竞争,而且分库分表后外键基本没法用。
我的建议是:课程设计阶段用外键,因为数据量小,一致性优先。生产环境可以考虑在应用层做约束,数据库层去掉外键。如果你在报告里写清楚这个取舍,答辩老师一般不会为难你。
4. 增删改查与业务逻辑落地:从 SQL 到存储过程
4.1 核心查询:查航班、查订单、查库存
最常用的查询是"查某天从北京到上海的经济舱航班":
SELECT f.flight_no, f.depart_time, f.arrive_time, ci.price, ci.total_seats - ci.sold_seats AS remain_seats FROM flight f JOIN cabin_inventory ci ON f.flight_id = ci.flight_id WHERE f.depart_airport = 'PEK' AND f.arrive_airport = 'SHA' AND f.depart_time BETWEEN '2025-06-01 00:00:00' AND '2025-06-01 23:59:59' AND ci.cabin_class = 'Y' AND f.status = 1 ORDER BY f.depart_time;这条 SQL 会命中idx_depart_arrive_time索引,cabin_inventory表通过uk_flight_cabin唯一索引关联,整体走 Nested Loop Join,效率可以。
查订单的 SQL 稍微复杂一点,因为要关联乘客:
SELECT o.order_no, o.total_amount, o.status, GROUP_CONCAT(p.name) AS passengers FROM booking_order o JOIN order_passenger op ON o.order_id = op.order_id JOIN passenger p ON op.passenger_id = p.passenger_id WHERE o.order_no = 'ORD20250601001' GROUP BY o.order_id;这里用GROUP_CONCAT把同一订单的乘客姓名拼起来,方便展示。注意GROUP_CONCAT有长度限制,默认 1024 字节,如果乘客多可能要调group_concat_max_len。
4.2 下单流程:一个存储过程搞定库存扣减和订单创建
下单是最容易出并发问题的地方。两个用户同时买最后一张票,如果处理不当就会超卖。我一般用存储过程把扣库存和建订单放在一个事务里:
DELIMITER // CREATE PROCEDURE create_booking( IN p_flight_id BIGINT UNSIGNED, IN p_cabin_class CHAR(1), IN p_passenger_ids VARCHAR(500), IN p_order_no VARCHAR(32), OUT p_result_code INT, OUT p_result_msg VARCHAR(200) ) BEGIN DECLARE v_remain INT DEFAULT 0; DECLARE v_price DECIMAL(10,2) DEFAULT 0; DECLARE v_order_id BIGINT UNSIGNED; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SET p_result_code = -1; SET p_result_msg = '系统异常,下单失败'; END; START TRANSACTION; -- 锁定库存行,防止并发超卖 SELECT total_seats - sold_seats, price INTO v_remain, v_price FROM cabin_inventory WHERE flight_id = p_flight_id AND cabin_class = p_cabin_class FOR UPDATE; IF v_remain <= 0 THEN ROLLBACK; SET p_result_code = 1; SET p_result_msg = '库存不足'; ELSE -- 扣减库存 UPDATE cabin_inventory SET sold_seats = sold_seats + 1 WHERE flight_id = p_flight_id AND cabin_class = p_cabin_class; -- 创建订单 INSERT INTO booking_order(order_no, flight_id, cabin_class, total_amount, status) VALUES(p_order_no, p_flight_id, p_cabin_class, v_price, 0); SET v_order_id = LAST_INSERT_ID(); -- 关联乘客(简化处理,实际应拆分字符串) INSERT INTO order_passenger(order_id, passenger_id) SELECT v_order_id, passenger_id FROM passenger WHERE FIND_IN_SET(passenger_id, p_passenger_ids); COMMIT; SET p_result_code = 0; SET p_result_msg = '下单成功'; END IF; END // DELIMITER ;这个存储过程的关键是SELECT ... FOR UPDATE,它会对库存行加排他锁,保证同一时刻只有一个事务能读到最新的库存数。后面的UPDATE和INSERT在同一个事务里,要么全成功,要么全回滚。
参数说明:p_passenger_ids是逗号分隔的乘客 ID 字符串,实际项目中建议用 JSON 数组或临时表传递。p_result_code返回 0 表示成功,1 表示库存不足,-1 表示系统异常。
4.3 触发器与定时任务:自动更新订单状态和清理过期订单
课程设计里加一两个触发器能加分不少。比如支付成功后自动更新订单状态:
DELIMITER // CREATE TRIGGER trg_payment_success AFTER UPDATE ON payment FOR EACH ROW BEGIN IF NEW.pay_status = 1 AND OLD.pay_status != 1 THEN UPDATE booking_order SET status = 1 WHERE order_id = NEW.order_id AND status = 0; END IF; END // DELIMITER ;这个触发器在支付记录状态变为"成功"时,把对应订单从"待支付"改为"已支付"。注意判断OLD.pay_status != 1,避免重复更新。
过期订单清理可以用 MySQL 的 EVENT 定时执行:
SET GLOBAL event_scheduler = ON; CREATE EVENT ev_clean_expired_order ON SCHEDULE EVERY 1 HOUR DO BEGIN UPDATE booking_order SET status = 4 WHERE status = 0 AND create_time < DATE_SUB(NOW(), INTERVAL 30 MINUTE); END;这个事件每小时跑一次,把创建超过 30 分钟还没支付的订单标记为"已取消"。实际项目中还要把扣减的库存加回去,这里简化了。
5. 并发、锁与性能排查:课程设计里最容易翻车的几个点
5.1 超卖问题:乐观锁和悲观锁怎么选
超卖是机票预订系统最经典的并发问题。上面存储过程用的是悲观锁(FOR UPDATE),适合冲突激烈的场景。如果并发量不大,可以用乐观锁:
UPDATE cabin_inventory SET sold_seats = sold_seats + 1, version = version + 1 WHERE flight_id = ? AND cabin_class = ? AND version = ? AND sold_seats < total_seats;然后检查affected_rows,如果为 0 说明版本号被改过,需要重试。乐观锁的好处是不加锁,吞吐量高;坏处是冲突多时重试次数多,反而更慢。
我的建议是:课程设计里两种都实现,在报告里对比 QPS 和冲突率,答辩时能讲清楚取舍。
5.2 死锁排查:看懂 SHOW ENGINE INNODB STATUS
死锁在批量下单或者退票时容易出现。比如事务 A 锁了航班 1 的库存,等航班 2 的库存;事务 B 锁了航班 2,等航班 1,就死锁了。
排查死锁的第一步是看SHOW ENGINE INNODB STATUS输出里的LATEST DETECTED DEADLOCK段。它会告诉你哪个事务持有什么锁、在等什么锁、最后 MySQL 回滚了哪个事务。
避免死锁的常见做法是:按固定顺序访问资源。比如批量扣库存时,先把航班 ID 排序,再依次加锁。这样所有事务的加锁顺序一致,就不会形成循环等待。
5.3 慢查询日志与 EXPLAIN 实战
MySQL 慢查询日志是排查性能问题的黑匣子。开启方式:
SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1; SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';然后对慢查询用EXPLAIN分析执行计划。重点看type列(最好到range或ref,避免ALL)、key列(实际用的索引)、rows列(扫描行数)。如果rows很大但结果很少,说明索引选择性差或者没走对索引。
一个常见问题是ORDER BY和WHERE用不同索引,导致 MySQL 先过滤再排序,产生Using filesort。解决办法是建联合索引,把WHERE和ORDER BY的字段都包进去,顺序是WHERE在前ORDER BY在后。
6. 避坑与常见问题排查
6.1 坑一:订单号用自增 ID 导致业务泄露
现象:订单号是 1001、1002、1003,用户能猜到总订单量,甚至能遍历别人的订单。
原因:直接用AUTO_INCREMENT主键当订单号。
解决:订单号用业务规则生成,比如"ORD + 日期 + 随机数 + 校验位",或者用雪花算法。数据库主键仍然用自增 ID,但对外只暴露订单号。
6.2 坑二:库存扣了但订单没建成功
现象:用户下单失败,但库存显示少了一张票。
原因:扣库存和建订单不在同一个事务里,或者事务中途异常没回滚。
解决:把两个操作放在同一个事务里,用存储过程或应用层事务控制。确保START TRANSACTION和COMMIT/ROLLBACK成对出现。
6.3 坑三:中文乘客姓名乱码
现象:乘客姓名存进去变成问号或者乱码。
原因:数据库、表、连接字符集不一致。常见的是数据库用utf8(实际是 utf8mb3),存不下某些生僻字。
解决:建库建表统一用utf8mb4,连接串加characterEncoding=utf8mb4。MySQL 8.x 默认就是 utf8mb4,但老版本要手动设。
6.4 坑四:外键导致删除航班失败
现象:想删一个测试航班,报错"Cannot delete or update a parent row"。
原因:有订单关联了这个航班,外键约束阻止删除。
解决:要么先删关联订单,要么用ON DELETE CASCADE级联删除(慎用),要么在应用层做软删除,给航班加is_deleted字段。
6.5 坑五:GROUP_CONCAT 截断导致乘客列表不全
现象:订单详情里乘客姓名只显示了一部分。
原因:GROUP_CONCAT默认最大长度 1024 字节,超出部分被截断。
解决:会话级别调大SET SESSION group_concat_max_len = 10240;,或者改用应用层拼接。
7. 进阶技巧:用窗口函数和 CTE 写出更优雅的查询
课程设计做到最后,如果想拿高分,可以在查询上加点进阶用法。MySQL 8.0 支持窗口函数和 CTE,能写出比子查询更清晰的 SQL。
比如查每个航班最贵的舱位和对应价格:
WITH ranked_cabin AS ( SELECT flight_id, cabin_class, price, ROW_NUMBER() OVER (PARTITION BY flight_id ORDER BY price DESC) AS rn FROM cabin_inventory ) SELECT f.flight_no, rc.cabin_class, rc.price FROM ranked_cabin rc JOIN flight f ON rc.flight_id = f.flight_id WHERE rc.rn = 1;这个查询用 CTE 先把每个航班的舱位按价格降序排名,再取第一名。比用子查询WHERE price = (SELECT MAX(price) ...)更直观,而且性能通常更好。
再比如查每个机场的航班吞吐量排名:
SELECT airport, COUNT(*) AS flight_count, RANK() OVER (ORDER BY COUNT(*) DESC) AS rank_no FROM ( SELECT depart_airport AS airport FROM flight UNION ALL SELECT arrive_airport FROM flight ) t GROUP BY airport;这个查询用UNION ALL把出发和到达合并,再用窗口函数排名。答辩时如果老师问"怎么查吞吐量最大的机场",这个答案比简单的GROUP BY加分。
验证方法上,我一般会造一批测试数据,用INSERT INTO ... SELECT生成几千条航班和订单,然后跑一遍核心查询,看执行时间是否在可接受范围。如果超过 100ms,就用EXPLAIN看执行计划,调整索引。
最后说个习惯:每次改完表结构或索引,我都会把建表语句导出成.sql文件,用 Git 管理起来。课程设计报告里附上这个文件,老师能直接复现你的环境。别等到答辩前一天才发现本地库和报告里的表结构对不上,那种后悔药没地方买。
希望帮到你。
本文还有配套的精品资源,点击获取