Oracle机票预订系统数据库课程设计:从E-R图到PL/SQL完整实现
简介这份数据库课程设计文档面向高校计算机相关专业学生围绕机票预订系统这一典型业务场景提供从需求分析到数据库落地的完整设计思路。内容涵盖航班、机票、客户等核心数据的管理并系统讲解E-R模型构建、关系模型转换、表空间与表设计、视图、存储过程、函数、触发器以及角色权限与备份方案等关键环节适合正在完成数据库课程设计或希望提升Oracle与PL/SQL实战能力的学习者参考。资源包内仅含1个doc文档约1.2MB以文字与SQL示例为主结构按序言、需求分析、分析与设计、总结等章节展开便于按模块查阅。目前已有84人学习可帮助读者理解如何将现实业务需求转化为数据模型并掌握数据库安全性、性能优化与可扩展性设计的基本方法。1. 从一份课程设计文档说起Oracle 机票预订系统数据库到底能跑出什么如果你手头正躺着一份数据库课程设计任务书要求用 Oracle 把机票预订业务从 E-R 图一路做到表空间、视图、存储过程、触发器和备份方案那这份文档就是一份可以直接照着复现的完整参考。它不是那种只给几张截图、代码缺胳膊少腿的“水课设”而是把需求分析、关系模型、物理设计、PL/SQL 编程和安全策略串成了一条线。适合谁正在做数据库课设的学生、想拿一个完整业务场景练 Oracle 的初级 DBA、以及需要一套“能跑通”的数据库设计模板来改造成自己项目的开发者。它解决的核心问题是让你不用从零构思业务实体直接站在一个已经梳理清楚的机票预订模型上把 Oracle 的关键技术点逐个落地。2. 需求到 E-R七个实体怎么拆才不打架2.1 先锁定业务边界再谈实体属性机票预订这个场景业务边界其实比想象中窄。文档里明确列出了七个实体航空公司、飞机、航班、机舱、机票、乘客、业务员。很多人一上来就急着画图结果实体之间关系混乱后面建表时外键到处打架。我的习惯是先把每个实体的“唯一标识”和“生命周期”写清楚再决定它是不是独立实体。航空公司有公司编号、名称、电话、地址飞机有飞机编号、名称并且归属于某家航空公司航班有航班号、出发地、目的地、起飞时刻、飞行时间并且使用某架飞机机舱是航班下的一个等级划分有等级、座位数、定价、折扣机票是具体某一天某个航班某个舱位的一张票有票号、登机日期、预定状态、座位号乘客有身份证号、姓名、电话、住址业务员有编号、身份证号、姓名、电话、住址并且隶属于某家航空公司。这里有一个容易翻车的地方机舱和机票的关系。机舱不是独立存在的它必须依附于航班所以主键是航班号舱位等级的复合主键。机票又依附于机舱所以外键指向这个复合主键。如果你把机舱设计成独立实体后面插入机票时就会发现找不到一个稳定的父记录。2.2 关系模型转换从 E-R 到八张表的映射规则E-R 图里有一对多、多对多和一对一的联系。转换成关系模型时规则很固定一对多把“一”端的主键放到“多”端做外键多对多必须拆成独立的关系表一对一可以合并也可以独立建表。这份文档最终给出了八张表表名主要字段主键外键companycno, cname, ctel, caddresscno无passengerpID, pname, ptel, paddresspID无salesmansno, sID, sname, stel, saddress, cnosnocno → companyairplaneano, aname, cnoanocno → companyflightfno, departure, arrival, time, flytime, anofnoano → airplanecabinfno, cblevel, seats, price(fno, cblevel)fno → flighttickettno, fno, cblevel, flydate, status, seat, discounttno(fno, cblevel) → cabinticketsaletno, pID, sno, saledate(tno, pID, sno)tno → ticket, pID → passenger, sno → salesman注意 ticketsale 的主键是三个字段的组合。这意味着同一个票号、同一个乘客、同一个业务员只能出现一次销售记录。如果你允许一个乘客帮别人买同一张票这个主键就会冲突。实际业务里更合理的做法是加一个销售流水号但课程设计阶段按文档的复合主键走也能跑通。2.3 建表时的字段类型选择与约束文档里用的字段类型很典型编号类用 VARCHAR2(10) 或 VARCHAR2(20)姓名和地址用 VARCHAR2(20) 到 VARCHAR2(50)价格用 NUMBER(5)折扣用 NUMBER(3,2)飞行时间用 INTERVAL DAY TO SECOND。这里有几个参数值得说清楚。NUMBER(3,2)表示总共 3 位数字其中 2 位是小数所以最大值是 9.99最小值是 -9.99。折扣一般不会超过 1所以这个范围够用。NUMBER(5)表示最多 5 位整数票价最大 99999对于国内航班也够。飞行时间用 INTERVAL DAY TO SECOND 是 Oracle 特有的可以直接做时间加减比如time flytime就能算出到达时间比用数字存分钟数要直观。建表时还有一个细节STATUS NUMBER(1) DEFAULT 1 NOT NULL。这个字段表示预定状态默认 1 代表“已预定”。如果你想让状态可读性更强可以用 CHAR(1) 存 Y/N但 NUMBER 在索引和比较时效率更高。CREATE TABLE SYSTEM.TICKET ( TNO NUMBER(10) NOT NULL, FNO VARCHAR2(10) NOT NULL, CBLEVEL NUMBER(1) NOT NULL, FLYDATE DATE NOT NULL, STATUS NUMBER(1) DEFAULT 1 NOT NULL, SEAT NUMBER(3) NOT NULL, DISCOUNT NUMBER(3,2) NOT NULL, PRIMARY KEY (TNO) VALIDATE, FOREIGN KEY (FNO, CBLEVEL) REFERENCES SYSTEM.CABIN (FNO, CBLEVEL) VALIDATE ) TABLESPACE TICKET;这段代码里VALIDATE表示约束对已有数据也生效。如果你先插数据再建约束不加 VALIDATE 可能会跳过检查。TABLESPACE TICKET把票表单独放到一个表空间这是后面备份策略的基础。3. 表空间与视图把大表拆开把查询封装3.1 四个表空间的划分逻辑文档把表空间分成了四个PASSENGER、TICKET、TICKETSALE、OTHERS。划分依据是数据量和增长趋势。乘客表、机票表、销售表数据量最大单独放公司、飞机、航班、机舱、业务员这些基础信息表数据量小放 OTHERS。创建表空间的语句里SIZE 100M是初始大小AUTOEXTEND ON NEXT 5M表示每次自动扩展 5MMAXSIZE UNLIMITED表示不限制最大大小。EXTENT MANAGEMENT LOCAL和SEGMENT SPACE MANAGEMENT AUTO是 Oracle 推荐的本地管理和自动段空间管理能减少碎片。CREATE SMALLFILE TABLESPACE PASSENGER DATAFILE F:\APP\ORACLE\ORADATA\ORCL\TICKETSALE\passenger.dbf SIZE 100M AUTOEXTEND ON NEXT 5M MAXSIZE UNLIMITED LOGGING EXTENT MANAGEMENT LOCAL SEGMENT SPACE MANAGEMENT AUTO;路径里的F:\APP\ORACLE\ORADATA\ORCL\TICKETSALE\是 Windows 下 Oracle 的典型数据文件目录。如果你在 Linux 上路径会变成/u01/app/oracle/oradata/orcl/这种形式。SMALLFILE表示使用小文件表空间这是 Oracle 10g 之后的默认选项除非你有特殊需求否则不用改成 BIGFILE。3.2 参数化视图用临时表绕过 Oracle 的限制Oracle 的普通视图不支持参数但文档用了一个很实用的技巧建一张全局临时表INPUT_TO_FLIGHT把查询条件先插进去视图再和这张临时表做连接。这样应用程序只需要先 INSERT 条件再 SELECT 视图就能实现参数化查询。CREATE GLOBAL TEMPORARY TABLE SYSTEM.INPUT_TO_FLIGHT ( T_FNO VARCHAR2(10), T_DEPARTURE VARCHAR2(20), T_ARRIVAL VARCHAR2(20), T_FLYDATE DATE ) ON COMMIT PRESERVE ROWS;ON COMMIT PRESERVE ROWS表示事务提交后临时表里的数据仍然保留。如果你用ON COMMIT DELETE ROWS每次提交后数据就没了视图查出来就是空。这个参数选错视图永远返回不了结果是新手最容易踩的坑之一。航班信息视图FLIGHT_VIEW_BYFNO和FLIGHT_VIEW_BYSITE分别按航班号和出发地/目的地查询。余票视图REMAIN_SEATS_VIEW调用了自定义函数count_ticket这个函数需要单独创建逻辑是统计某个航班某天某个舱位下状态为已预定的票数再用舱位总座位数减去它。CREATE OR REPLACE VIEW SYSTEM.REMAIN_SEATS_VIEW ( FNO, FLYDATE, CBLEVEL, COUNT ) AS SELECT DISTINCT fno, flydate, cblevel, count_ticket(fno, flydate, cblevel) FROM ticket, input_to_flight WHERE fno input_to_flight.t_fno AND flydate input_to_flight.T_FLYDATE;查询时先插入条件INSERT INTO input_to_flight VALUES(F0003, , , to_date(2025-06-01,yyyy-mm-dd)); SELECT * FROM remain_seats_view ORDER BY cblevel;这里to_date的格式必须和字符串匹配否则会报 ORA-01861。如果你用2025-6-1这种不补零的写法格式串要改成yyyy-mm-dd仍然能解析但建议统一补零避免在不同 NLS 设置下出问题。3.3 视图的嵌套与性能边界文档里还有TICKET_INFO_VIEW、SALERECORD_VIEW、SALE_GRADE_VIEW三个视图。其中TICKET_INFO_VIEW连接了八张表用来打印机票信息。这种多表连接视图在数据量小的时候没问题但一旦 ticket 表上百万行查询会非常慢。我的经验是视图适合封装业务逻辑但不适合做大数据量的实时查询。如果课设要求只是演示八表连接没问题如果要在生产环境用应该在应用层拆成多次查询或者用物化视图定期刷新。物化视图的语法是CREATE MATERIALIZED VIEW ... REFRESH FAST ON COMMIT但需要先建物化视图日志复杂度较高课设阶段可以先不碰。4. 存储过程与触发器让 PL/SQL 替你干重复活4.1 批量生成机票的存储过程机票数据量庞大手工插入不现实。文档设计了一个CREATE_TICKET存储过程根据航班号和日期自动为该航班的所有舱位生成对应数量的机票记录。逻辑是先查这个航班有几个舱位再对每个舱位查座位数然后循环插入 ticket 表票号从T_NUMBER表里取每插一条就加一最后更新T_NUMBER。CREATE OR REPLACE PROCEDURE SYSTEM.CREATE_TICKET ( p_fno varchar2, p_flydate date, p_discount number ) as v_cblevel_count number; v_ticket_count_by_cblevel number; v_tno number; begin SELECT count(1) INTO v_cblevel_count FROM cabin WHERE fno p_fno; SELECT tno INTO v_tno FROM t_number; FOR v_i in 1..v_cblevel_count loop SELECT seats INTO v_ticket_count_by_cblevel FROM cabin WHERE fno p_fno AND cblevel v_i; FOR v_j IN 1..v_ticket_count_by_cblevel loop INSERT INTO ticket VALUES(v_tno, p_fno, v_i, p_flydate, 1, v_j, p_discount); v_tno : v_tno 1; END LOOP; END LOOP; UPDATE t_number SET tno v_tno; END;调用方式CALL create_ticket(F0003, to_date(2025-06-10,yyyy-mm-dd), 0.7);这里有几个参数要留意。p_discount是折扣0.7 表示七折。v_tno从T_NUMBER表读取这个表只有一行一列用来维护全局票号。如果你并发调用这个存储过程两个会话可能读到同一个票号导致主键冲突。解决办法是在SELECT tno INTO v_tno FROM t_number后面加FOR UPDATE锁住这一行等更新完再提交。课设演示通常单会话不加也能跑但知道这个边界很重要。4.2 售票存储过程与打印临时表另一个存储过程负责售票往ticketsale表插入记录同时把要打印的票面信息写入PRINT_TICKET临时表。这个临时表的结构和TICKET_INFO_VIEW的字段几乎一致方便应用程序直接读取打印。CREATE GLOBAL TEMPORARY TABLE SYSTEM.PRINT_TICKET ( TNO VARCHAR2(10), FNO VARCHAR2(10), CNAME VARCHAR2(20), ANAME VARCHAR2(20), DEPARTURE VARCHAR2(20), ARRIVAL VARCHAR2(20), FLYDATE DATE, TIME DATE, ARRIVAL_TIME DATE, CBLEVEL NUMBER(1), SEAT NUMBER(3), PRICE NUMBER(5), DISCOUNT NUMBER(3,2), FINAL_PRICE NUMBER, PNAME VARCHAR2(20) ) ON COMMIT PRESERVE ROWS;售票存储过程的核心是先检查该票是否已被预定status 1表示可预定如果可预定就更新ticket表的状态插入ticketsale再把票面信息插入PRINT_TICKET。这里的事务控制很关键更新状态和插入销售记录必须在同一个事务里否则可能出现票状态改了但销售记录没写进去的情况。4.3 触发器数据检查的最后一道闸文档要求至少建一个触发器。常见做法是在ticketsale表上建一个BEFORE INSERT触发器检查对应的ticket记录是否存在且状态为可预定。如果票已经被卖了触发器直接抛异常阻止插入。CREATE OR REPLACE TRIGGER SYSTEM.CHECK_TICKET_STATUS BEFORE INSERT ON SYSTEM.TICKETSALE FOR EACH ROW DECLARE v_status NUMBER(1); BEGIN SELECT status INTO v_status FROM ticket WHERE tno :NEW.tno; IF v_status 0 THEN RAISE_APPLICATION_ERROR(-20001, 该机票已被预定不能重复销售); END IF; END;RAISE_APPLICATION_ERROR的第二个参数是自定义错误信息第一个参数必须在 -20000 到 -20999 之间。触发器里查ticket表时如果票号不存在SELECT INTO会抛NO_DATA_FOUND这个异常不会被上面的 IF 捕获会直接冒泡出去。如果你希望给出更友好的提示可以加一个EXCEPTION WHEN NO_DATA_FOUND THEN RAISE_APPLICATION_ERROR(-20002, 票号不存在)。5. 避坑与排查课设里最容易翻车的五个地方5.1 表空间路径不存在导致建表失败现象执行CREATE TABLESPACE时报 ORA-01119提示无法创建数据文件。原因DATAFILE后面的路径在操作系统里不存在或者 Oracle 服务账户没有写权限。解决先在文件系统里手动创建目录比如F:\APP\ORACLE\ORADATA\ORCL\TICKETSALE\然后确认 Oracle 服务账户对该目录有完全控制权限。Linux 下还要注意chown oracle:oinstall和chmod 750。5.2 临时表 ON COMMIT 选项选错视图永远为空现象往INPUT_TO_FLIGHT插入条件后查询视图返回 0 行。原因建临时表时用了ON COMMIT DELETE ROWS而应用程序在插入条件后执行了提交数据被清空。解决改成ON COMMIT PRESERVE ROWS。如果临时表已经建好需要先 DROP 再重建ALTER 改不了这个属性。5.3 存储过程里 SELECT INTO 没查到数据现象调用CREATE_TICKET时报 ORA-01403: no data found。原因SELECT tno INTO v_tno FROM t_number时t_number表是空的或者SELECT count(1) INTO v_cblevel_count FROM cabin WHERE fno p_fno查不到该航班的舱位记录。解决在调用存储过程前先确认t_number表里有一行初始票号比如INSERT INTO t_number VALUES(1);。同时确认cabin表里该航班号有数据。如果业务上允许航班没有舱位存储过程里要加异常处理不要让它直接抛错。5.4 外键约束导致插入顺序错误现象插入ticket表时报 ORA-02291提示违反外键约束。原因ticket表的外键指向cabin(fno, cblevel)但cabin表里还没有对应的航班号和舱位等级记录。解决严格按照依赖顺序插入数据先company再airplane和salesman然后flight接着cabin最后才是ticket和ticketsale。如果已经插乱了要么回滚事务重新来要么先禁用外键约束插完再启用并验证。5.5 日期格式与 NLS 设置不一致现象to_date(2025-06-01,yyyy-mm-dd)在某些环境下报 ORA-01861。原因会话的NLS_DATE_FORMAT和格式串不匹配或者字符串里用了中文破折号、全角字符。解决在 SQL 开头显式设置ALTER SESSION SET NLS_DATE_FORMAT YYYY-MM-DD HH24:MI:SS;并确保字符串里的连字符是半角。如果是从文档里复制的代码特别注意有没有混入全角空格或全角减号。6. 备份与权限课设最后一步怎么收得干净6.1 角色与权限的最小化分配文档要求规划角色、用户和权限。我的做法是建两个角色ROLE_QUERY只给 SELECT 权限ROLE_OPER给 SELECT、INSERT、UPDATE 和存储过程执行权限。然后建两个用户一个查询用户一个操作员用户分别授予对应角色。CREATE ROLE ROLE_QUERY; GRANT SELECT ON company, passenger, salesman, airplane, flight, cabin, ticket, ticketsale TO ROLE_QUERY; CREATE ROLE ROLE_OPER; GRANT SELECT, INSERT, UPDATE ON ticket, ticketsale TO ROLE_OPER; GRANT EXECUTE ON create_ticket TO ROLE_OPER; CREATE USER query_user IDENTIFIED BY Query#2025; GRANT ROLE_QUERY TO query_user; CREATE USER oper_user IDENTIFIED BY Oper#2025; GRANT ROLE_OPER TO oper_user;密码里带特殊字符时要用双引号包起来。GRANT SELECT ON table TO role这种对象权限不能直接授予角色再授予用户吗可以但要注意如果角色被禁用权限就失效。课设环境通常不会禁用角色所以没问题。6.2 备份方案导出命令与恢复验证文档要求估计表容量和增长速度指定备份方案。对于课设规模用 Oracle Data Pump 做逻辑备份就够了。导出整个 schema 的命令expdp system/passwordorcl schemasSYSTEM directoryDATA_PUMP_DIR dumpfileticket_system_%U.dmp logfileticket_system_exp.log parallel2directory是 Oracle 里的目录对象需要先创建并授权CREATE OR REPLACE DIRECTORY DATA_PUMP_DIR AS F:\app\oracle\admin\orcl\dpdump; GRANT READ, WRITE ON DIRECTORY DATA_PUMP_DIR TO SYSTEM;parallel2表示并行度 2能加快导出速度但需要企业版支持。标准版只能用单线程。%U是通配符当 dumpfile 超过 1G 时会自动分片。恢复时用impdpimpdp system/passwordorcl schemasSYSTEM directoryDATA_PUMP_DIR dumpfileticket_system_%U.dmp logfileticket_system_imp.log恢复前最好先DROP USER SYSTEM CASCADE再重建否则可能遇到对象已存在的错误。但注意SYSTEM是 Oracle 的系统用户实际课设里应该建一个独立用户比如TICKET_USER不要直接用SYSTEM。文档里用SYSTEM只是演示生产环境千万别这么干。6.3 一个验证备份是否可用的技巧备份做完不验证等于没备份。我的习惯是导出完成后立刻在另一个测试库上做一次导入然后跑几条关键查询比如SELECT COUNT(*) FROM ticket;和SELECT * FROM remain_seats_view WHERE rownum 5;。如果查询能返回结果说明备份文件完整、导入过程没有丢对象。从那以后我每次做完备份都强制走一遍“导出→导入到测试库→跑三条查询”的流程哪怕课设时间再紧也不跳过。希望帮到你。本文还有配套的精品资源点击获取