简介:这是一份数据库课程设计文档,主题为机票预订系统,适合高校计算机、软件工程专业学生完成大型数据库课程设计或Oracle综合实验时参考。文档围绕航空客运订票业务,从需求分析入手,确定航班、机票、乘客、业务员等核心实体及联系,绘制E-R模型并转换为关系模型,再在Oracle环境下完成表空间创建、数据库表设计、参数化视图、存储过程、函数与触发器编写,同时规划系统角色、用户权限及数据备份与恢复方案,完整展现了从业务建模到数据库落地实现的过程。资源仅含1个doc格式文件,大小约1.2MB,文档中包含详细的设计步骤、E-R图说明及可参考的SQL语句,能够帮助读者快速理解数据库设计规范。已有84人学习使用,适合需要完成同类课程设计、撰写实验报告或复习Oracle数据库开发的同学查阅。
1. 从一份课程设计文档拆出的机票预订系统:交付物不止是建表SQL
你下载的这份《数据库课程设计机票预订系统.doc》,说白了是一整套能在 Oracle 里直接跑起来的数据库方案,不是那种只堆概念凑字数的论文。最反直觉的一点:Oracle 原生不支持带参数的视图,但这份设计偏偏用全局临时表把参数传了进去,实现了按航班号、按出发地和目的地动态查询航班信息的效果。整套方案包含 E-R 图、八张业务表的完整建表 SQL、四个业务视图、批量生成机票的存储过程、触发器,以及角色权限和备份方案的规划。它解决的核心问题是:数据库课程设计验收时,评委想看的不只是建了几张表,而是从需求分析到备份恢复的完整链路。适合三类人——正在做 Oracle 课程设计的学生、要把这份文档复现成实际库的人、以及想学 PL/SQL 视图传参和存储过程批量生成模板的开发者。
2. 从E-R模型到表空间:实体关系先理清,库表才不散
2.1 实体、属性与关系:一张图看清业务骨架
这份设计的第一步是梳理业务实体。系统里一共有七个实体:航空公司、飞机、航班、机舱、机票、乘客、业务员,外加一个“售票”联系。实体和属性的划分直接影响后面的表设计,先看清楚再动手建库。
| 实体 | 核心属性 | 与哪些实体发生关系 |
|---|---|---|
| 航空公司 company | 企业编号、企业名、企业电话、企业地址 | 拥有一架飞机、多名业务员 |
| 飞机 airplane | 飞机编号、飞机名称 | 隶属于航空公司、执飞多个航班 |
| 航班 flight | 航班号、出发地、目的地、起飞时刻、飞行时间 | 由某飞机执飞、下挂多个机舱等级 |
| 机舱 cabin | 机舱等级、座位数、定价、折扣 | 属于某航班、容纳多张机票 |
| 机票 ticket | 机票编号、登机日期、预定状态、座位号 | 属于某机舱等级、被业务员售出 |
| 乘客 passenger | 身份证号、姓名、联系电话、住址 | 购买机票 |
| 业务员 salesman | 业务员编号、姓名、身份证号、联系电话、住址 | 属于某航空公司、办理售票 |
这里有一个容易被忽略的设计点:机票和乘客之间不是直接外键,而是通过 ticketsale 表来桥接。ticketsale 有三个外键(机票号、乘客身份证、业务员号),还带一个售票日期属性。这种中间表设计能支持“一个乘客买多张票”“一个业务员卖多张票”的多对多关系,也方便后面做销售业绩统计。如果你在课程设计答辩时被问“为什么多一张 ticketsale 表”,答案就是:E-R 图里的多对多联系必须转换成独立的关系表。
2.2 表空间分配:数据量决定分布策略
文档里的表空间分配逻辑很直白:乘客表、机票表和售票表数据量大,单独分配表空间;其他表数据量小,共用一个。这个判断在真实业务里是合理的——机票表会随着日期和座位数快速增长,乘客表会持续积累,把它们和字典表混在同一个数据文件里会加剧碎片化和 I/O 竞争。
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; CREATE SMALLFILE TABLESPACE "TICKET" DATAFILE 'F:\APP\ORACLE\ORADATA\ORCL\TICKETSALE\ticket.dbf' SIZE 100M AUTOEXTEND ON NEXT 5M MAXSIZE UNLIMITED LOGGING EXTENT MANAGEMENT LOCAL SEGMENT SPACE MANAGEMENT AUTO; CREATE SMALLFILE TABLESPACE "TICKETSALE" DATAFILE 'F:\APP\ORACLE\ORADATA\ORCL\TICKETSALE\ticketsale.dbf' SIZE 100M AUTOEXTEND ON NEXT 5M MAXSIZE UNLIMITED LOGGING EXTENT MANAGEMENT LOCAL SEGMENT SPACE MANAGEMENT AUTO; CREATE SMALLFILE TABLESPACE "OTHERS" DATAFILE 'F:\APP\ORACLE\ORADATA\ORCL\TICKETSALE\others.dbf' SIZE 100M AUTOEXTEND ON NEXT 5M MAXSIZE UNLIMITED LOGGING EXTENT MANAGEMENT LOCAL SEGMENT SPACE MANAGEMENT AUTO;参数说明:SMALLFILE表示传统小文件表空间,适合课程设计这种单数据文件的场景;AUTOEXTEND ON NEXT 5M表示文件满了自动扩展,每次扩 5M,MAXSIZE UNLIMITED表示不设上限;EXTENT MANAGEMENT LOCAL是本地化管理区,现代 Oracle 默认值,避免数据字典表的争用;SEGMENT SPACE MANAGEMENT AUTO让段空间用位图自动管理,比较省心。
常见的坑是数据文件路径。文档里的F:\APP\ORACLE\ORADATA\ORCL\TICKETSALE\是原作者机器上的路径,你换一台电脑复现时,如果那个目录不存在,Oracle 会直接报 ORA-01119。我一般会把路径改成自己本机 Oracle 的ORADATA路径,先建好目录再执行。这里提一句容量估算:单表空间初始 100M 对课程设计完全够用,但 title 表按日期和座位数增长后,一个月的机票记录可能达到上万行,AUTOEXTEND这时候就是后悔药,不会因为忘记手动扩文件导致插入失败。
2.3 建表与主外键:从关系模型到 Oracle 表
关系模型在文档里已经推导得很完整,建表就是照着关系模型逐张落地。注意原设计把表建在了SYSTEM用户下,这在实际工程里不推荐,但课程设计为了答辩方便能跑就行,这里保留原样。
CREATE TABLE "SYSTEM"."COMPANY" ( "CNO" VARCHAR2(10) NOT NULL, "CNAME" VARCHAR2(20) NOT NULL, "CTEL" VARCHAR2(20), "CADDRESS" VARCHAR2(50), PRIMARY KEY ("CNO") VALIDATE ) TABLESPACE "OTHERS"; CREATE TABLE "SYSTEM"."PASSENGER" ( "PID" VARCHAR2(20) NOT NULL, "PNAME" VARCHAR2(20) NOT NULL, "PTEL" VARCHAR2(20), "PADDRESS" VARCHAR2(50), PRIMARY KEY ("PID") VALIDATE ) TABLESPACE "PASSENGER"; CREATE TABLE "SYSTEM"."SALESMAN" ( "SNO" VARCHAR2(10) NOT NULL, "SID" VARCHAR2(20) NOT NULL, "SNAME" VARCHAR2(20) NOT NULL, "STEL" VARCHAR2(20), "SADDRESS" VARCHAR2(50), "CNO" VARCHAR2(10) NOT NULL, PRIMARY KEY ("SNO") VALIDATE, FOREIGN KEY ("CNO") REFERENCES "SYSTEM"."COMPANY" ("CNO") VALIDATE ) TABLESPACE "OTHERS";字段类型的选择有几个细节值得在答辩时说清楚。CNO、SNO这类编号用VARCHAR2而不是NUMBER,因为业务编号通常带前缀且不会参与数值计算;PID用 20 位VARCHAR2是考虑到身份证号有 X 结尾;PRICE用NUMBER(5)能存 99999 以内的价格,DISCOUNT用NUMBER(3,2)表示 0.00 到 9.99 之间的折扣系数,精确到小数点后两位。这些类型看着琐碎,但答辩时老师最喜欢追问“为什么不用 NUMBER”。
CREATE TABLE "SYSTEM"."FLIGHT" ( "FNO" VARCHAR2(10) NOT NULL, "DEPARTURE" VARCHAR2(20) NOT NULL, "ARRIVAL" VARCHAR2(20) NOT NULL, "TIME" DATE NOT NULL, "FLYTIME" INTERVAL DAY TO SECOND NOT NULL, "ANO" VARCHAR2(10) NOT NULL, PRIMARY KEY ("FNO") VALIDATE, FOREIGN KEY ("ANO") REFERENCES "SYSTEM"."AIRPLANE" ("ANO") VALIDATE ) TABLESPACE "OTHERS";FLYTIME用INTERVAL DAY TO SECOND是这份设计里比较讲究的地方。飞行时长不是某个时间点,而是一段时长,用INTERVAL类型表达语义最准确,还能直接参与时间运算——视图里那句time + flytime就是靠它算出抵达时间的。如果当初用NUMBER存小时数,每次算抵达时间都得手动换算,视图的写法会丑很多。TIME字段起名要注意,它不是 Oracle 保留字,能直接建,但如果用别的数据库工具连接,可能触发关键字校验,稳妥的做法是叫DEPARTURE_TIME。
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";STATUS NUMBER(1) DEFAULT 1这里的 1 表示未售、0 表示已售,用数值而不是字符串,是为了让存储过程里判断更轻量。复合外键("FNO","CBLEVEL")对应 cabin 的复合主键,保证机票一定挂在某个有效航班的有效舱位上。ticketsale 的主键是(TNO, PID, SNO)三列联合,也是从 E-R 图多对多联系直接映射来的。整张表建完后,建议自查一遍各表是否都放到了规划的表空间里,常见错误是忘了在 CREATE TABLE 末尾写TABLESPACE,结果全部落进默认 USERS 表空间,答辩时被问“你设计的表空间分配在哪”就露馅了。
3. 参数化视图:用全局临时表绕过 Oracle 的视图参数限制
3.1 为什么 Oracle 视图不能带参数,而业务又需要参数化
先讲一个反直觉的事实:Oracle 的普通视图不支持参数。MySQL 里可以定义带输入参数的视图,但 Oracle 里视图就是一个被固化的 SELECT 语句,每次查询只能通过 WHERE 条件来限定,无法像调用存储过程那样传入“航班号”“出发地”这样的运行时参数。而这份机票系统的业务恰恰是参数化的——用户查航班,得输出发地和目的地;查余票,得输航班号和日期。
常见做法有两种:一种是用包(PACKAGE)里的游标函数返回结果集,另一种就是这份设计采用的全局临时表传参。临时表方案对课程设计更友好,直观而且好讲,步骤就是“先往临时表插参数,再查视图”,视图内部去临时表取参数拼接条件。这种设计在真实业务里也有应用,但要注意它有个天然限制:临时表的数据是会话级的,会话之间互相隔离,且必须在同一会话里先插参数再查视图,跨会话就会翻车。
3.2 传参通道:INPUT_TO_FLIGHT 临时表的设计
这份设计的传参载体是一张全局临时表,专门用来接收应用端传入的查询条件。它的四个字段对应四个查询维度:航班号、出发地、目的地、航班日期。
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;参数说明:GLOBAL TEMPORARY TABLE表示全局临时表,数据只在当前会话可见;ON COMMIT PRESERVE ROWS表示事务提交后数据仍然保留到会话结束,选这个模式是因为应用端需要先插参数、再执行视图查询,如果提交后数据被清空(ON COMMIT DELETE ROWS),两步操作之间就断了。这种做法也带来一个使用约定:每次查询前必须先写临时表,查询结束后最好清掉这几行,否则下一次查询的参数会被旧数据污染。
我在复现时踩过这个坑:查完“广州到长沙”后没清表,直接插了一个航班号参数去查余票,结果 flight 视图因为同时匹配到了旧参数和新参数,返回结果要么为空要么串了数据。解决办法很简单,应用端每次查询前先DELETE FROM input_to_flight,再插新参数,养成习惯。
3.3 三个核心视图:航班查询、余票视图、机票打印
第一个视图是航班信息查询,支持按航班号精确查,也支持按出发地和目的地组合查。视图里 JOIN 了 flight、airplane、company 三张表,把航班号对应的公司名、飞机名带出来,同时用time + flytime动态计算抵达时间。
CREATE OR REPLACE VIEW "SYSTEM"."FLIGHT_VIEW_BYFNO" ( "FNO","CNAME","ANAME","TIME","ARRIVAL_TIME","DEPARTURE","ARRIVAL" ) AS SELECT fno, cname, aname, time, time + flytime, departure, arrival FROM flight, company, airplane, input_to_flight WHERE flight.ano = airplane.ano AND airplane.cno = company.cno AND fno = input_to_flight.T_fno;这个视图的巧妙之处在最后的关联条件:fno = input_to_flight.T_fno让视图结果完全由临时表里的参数决定。你插 F0001,视图就只返回 F0001 的信息;你插别的号,结果跟着变。这样普通视图就实现了参数化查询的效果。同理,按出发地和目的地查询的版本叫 FLIGHT_VIEW_BYSITE,条件改为departure = input_to_flight.T_departure AND arrival = input_to_flight.T_arrival,其余 JOIN 完全一样。
第二个视图是余票查询,文档里叫 REMAIN_SEATS_VIEW,它需要调用后面会讲的 count_ticket 函数来算某航班某日期某舱位的剩余座位数。SQL 里有一个细节:视图用了SELECT DISTINCT,因为 ticket 表对同一航班同一日期同一舱位会有多行记录,每行都会触发一次函数调用,不用 DISTINCT 就会出现一排重复的余票数。
第三个视图是售票后打印机票用的 TICKET_INFO_VIEW。乘客买完票,票面上要印出公司名、飞机名、出发地、目的地、日期、起飞抵达时间、舱位、座位、原价、折扣、实付金额、乘客姓名身份证、业务员姓名。这个视图 JOIN 了 ticket、flight、airplane、company、passenger、salesman、ticketsale、cabin 一共八张表,核心计算是price * discount得出实付价。这属于典型的多表关联查询,答辩时能把这八张表的连接关系讲清楚,基本就是加分项。查这个视图时同样要先往临时表里插参数,视图内部靠ticket.fno = flight.fno链条和临时表参数联动。
3.4 查询方式与调用边界:同一会话里完成两步
参数化视图的调用方式很固定,先插参数再查视图。假设要查“茂名到长沙”的航班:
INSERT INTO input_to_flight VALUES ('', '茂名', '长沙', ''); SELECT * FROM flight_view_bysite;这时视图返回 F0007、F0008 这类满足条件的航班。注意插入参数时,不需要的条件要传空字符串或 NULL,视图里的 WHERE 条件会忽略掉空值对应的列。这里有个边界一定要记住:临时表是会话级的,SQL*Plus 里连的是同一个会话,两步能跑通;但如果用连接池的应用连接数据库,两次请求可能落在不同会话里,第二步查视图时临时表是空的,结果就是空表。解决方法是把两步包在同一个事务短连接里,或者干脆把插参数和查询封装成一个存储过程对外提供。
文档里余票查询那段 SQL 有个笔误,SELECT * FROM remain_seats_view ORER BY cblevel里的ORER是ORDER的拼写错误,复现时直接改成ORDER BY cblevel就行。这种 OCR 类文档里的小错不少,照着抄 SQL 前最好过一遍语法。
4. 存储过程、函数与触发器:把重复劳动交给数据库
4.1 create_ticket:按航班舱位批量生成机票
机票表的数据量是最大的,一个航班多个舱位,每个舱位几十上百个座位,一天一个航班就要生成几百行,手工 INSERT 不现实。文档用存储过程 CREATE_TICKET 解决了批量生成问题。它的核心是一个双循环:外层循环舱位等级,内层循环该舱位的座位数,逐行插入 ticket 表。为了让票号有序不重复,还专门建了一张 T_NUMBER 表保存当前票号。
CREATE TABLE "SYSTEM"."T_NUMBER" ( "TNO" NUMBER(10) ); 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;参数说明:p_fno是航班号,p_flydate是航班日期,p_discount是本次统一折扣率。过程先查该航班有几个舱位等级,再读 T_NUMBER 里当前票号作为起始编号,外层循环每个舱位,内层循环根据 seats 字段生成对应数量的票,票号逐张累加,最后把新票号写回 T_NUMBER。内层 INSERT 里第 5 个参数固定是 1,表示新票状态都是“未售”;第 6 个参数v_j就是座位号,从 1 排到 seats,保证一张票一个座。
调用示例:
CALL create_ticket('F0003', to_date('2020-06-10', 'yyyy-mm-dd'), 0.7);注意我这里的日期写成了'2020-06-10',文档里的写法是to_date('-6-10','yyyy-mm-dd'),年份缺失,Oracle 会报 ORA-01861,这在第 5 章展开说。执行成功后,ticket 表会新增该航班所有舱位、所有座位的票,每张票的状态都是未售、折扣统一 0.7。这个存储过程有几个可以优化的点:T_NUMBER 表只有一行,两个会话同时调用时可能取到同一个票号;还有删除的票号不会回收,票号会一直涨。真实工程里一般用序列或者SELECT ... FOR UPDATE来控制,课程设计做到这个程度已经够用了。
4.2 count_ticket 函数:余票计算与视图联动
余票视图调用了 count_ticket 函数,它的逻辑很直接:统计 ticket 表里某航班某日期某舱位且状态为未售的票数。文档只给了调用它的视图,函数体没有展开,但按视图使用方式,函数签名和内部逻辑是可以明确补全的。
CREATE OR REPLACE FUNCTION count_ticket ( p_fno varchar2, p_flydate date, p_cblevel number ) RETURN number IS v_count number; BEGIN SELECT count(1) INTO v_count FROM ticket WHERE fno = p_fno AND flydate = p_flydate AND cblevel = p_cblevel AND status = 1; RETURN v_count; END;参数说明:三个入参分别对应用户要查的航班号、日期、舱位等级,返回值是该条件下未售票的数量。视图里对每行调用函数,配合SELECT DISTINCT避免重复输出。这种“函数包在视图里”的写法在课程设计层面很讨巧,但性能上有隐患——视图每返回一行都会执行一次函数,ticket 表一大了查询会很慢,实际项目一般会把聚合逻辑写成 JOIN 或 GROUP BY,而不是对每行调函数。答辩时如果被问到性能,能答出这一点会显得你真的跑过数据。
4.3 触发器:售票数据完整性的一道保险
需求里要求至少有 1 个触发器,文档 3.5 节只列了“触发器设计”这个标题,没有给出具体 SQL。按这套表结构,最值得做的触发器是售票前校验票状态。ticketsale 表插入一条售票记录时,触发器检查对应票是否未售,已售则报错,否则插入成功后把票状态改为已售。这样即使应用端忘了更新状态,数据库层也能兜底。
CREATE OR REPLACE TRIGGER trg_ticketsale_insert BEFORE INSERT ON ticketsale FOR EACH ROW DECLARE v_status NUMBER; BEGIN SELECT status INTO v_status FROM ticket WHERE tno = :NEW.tno; IF v_status != 1 THEN RAISE_APPLICATION_ERROR(-20001, '该机票已售出,无法重复销售'); END IF; UPDATE ticket SET status = 0 WHERE tno = :NEW.tno; END;触发器的关键是FOR EACH ROW行级触发和:NEW伪记录。:NEW.tno代表正在插入的这行 ticketsale 的机票号,先查 ticket 的当前状态,不是 1 就抛异常中断插入,是 1 就把票状态改成 0。这里有个隐患:两张表之间没有显式事务包裹的话,如果 UPDATE 成功但 INSERT 因为其他原因回滚,票状态会被错误改成已售。稳妥做法是在触发器里同时处理,或者干脆把两步写成一个存储过程,由应用端统一调用。课程设计场景里,触发器能拦住“同一张票卖两次”的核心翻车点,就够了。
4.4 角色、用户与权限:不要三个业务员共用一个账号
文档 3.6 节规划了角色、用户、权限,但没有给出落地 SQL。按这套系统的业务,至少分三类角色:管理员负责录航班、生成机票;业务员负责售票、查自己的业绩;普通游客只能查航班和余票。Oracle 里的落地方式是先建角色,再把角色授权给具体的用户。
CREATE ROLE ticket_admin; CREATE ROLE ticket_salesman; CREATE ROLE ticket_guest; GRANT SELECT ON flight_view_bysite TO ticket_guest; GRANT SELECT ON remain_seats_view TO ticket_guest; GRANT INSERT, UPDATE ON ticketsale TO ticket_salesman; GRANT SELECT ON salerecord_view TO ticket_salesman; GRANT SELECT ON sale_grade_view TO ticket_salesman; GRANT EXECUTE ON create_ticket TO ticket_admin; CREATE USER sales_zhang IDENTIFIED BY sales_zhang_pwd; GRANT ticket_salesman TO sales_zhang;权限规划的原则是最小权限。业务员只需要售票相关的插入和查询,不需要碰 ticket 表本身的批量生成;批量生成票是管理员的事,所以EXECUTE ON create_ticket只给了 admin 角色。文档把表都建在 SYSTEM 名下,实际是拿超级用户当业务库用,这不安全,但要改会动到整篇 SQL 的所有者前缀,课程设计验收一般不看这么深,复现时能跑通就行。真要按照规范做,我会单独建一个 TICKET_USER 用户,把这些表全部归到这个用户下,再按角色授权。
5. 避坑:复现这个课程设计最容易翻车的五个地方
5.1 日期传参翻车:to_date 少年份直接报错
现象:按照文档里to_date('-6-10','yyyy-mm-dd')执行 create_ticket 或查询余票,Oracle 直接报ORA-01861: literal does not match format string。原因:格式串要求四位年份,但传入的字符串只有月和日,格式对不上。解决:改成完整的四位年份,如to_date('2020-06-10','yyyy-mm-dd')。文档里因为排版省略了年份,复现时尤其要注意,所有涉及日期的 INSERT 和 CALL 都要补全年份。
5.2 临时表参数残留:上一次查询污染下一次结果
现象:先查“广州到长沙”的航班,再查“F0003 余票”,结果返回空或者数据对不上。原因:input_to_flight 表里有上一次查询留下的出发地和目的地,余票视图的fno = input_to_flight.t_fno AND flydate = input_to_flight.T_FLYDATE虽然只用了航班号和日期,但如果旧参数里有别的值,视图关联时语义会混乱。解决:每次查询前先DELETE FROM input_to_flight,再插入本次参数,查询顺序保持“清空→插入→SELECT”。这个操作要固化到应用端的查询方法里,别指望人工每次记得。
5.3 T_NUMBER 并发取号:两个会话拿到同一个票号
现象:两个终端同时执行 create_ticket,生成的 ticket 表里出现重复 TNO,主键冲突或数据错乱。原因:T_NUMBER 表存的是当前票号,两个会话同时执行SELECT tno INTO v_tno FROM t_number,拿到同一个起始值。解决:把读票号的语句改成SELECT tno INTO v_tno FROM t_number FOR UPDATE,锁住这行直到事务结束;或者干脆换成 Oracle 序列CREATE SEQUENCE seq_ticket,循环里直接用seq_ticket.NEXTVAL取号。课程设计单人单机跑一般碰不到,但我做过演示时现场被老师开两个 SQL*Plus 窗口测试,当场就重现了。
5.4 count_ticket 逐行调用函数:数据一大查询就慢
现象:余票视图查一个航班 30 天的余票,SQL 跑了十几秒才出结果。原因:视图对返回的每一行都执行一次 count_ticket 函数,等于在结果集上隐式做循环,机票数据越多函数调用次数越多。解决:把函数改写成 GROUP BY 聚合直接关联,或者限制查询范围只查单日单航班。原设计方案为了演示“函数和视图联动”,牺牲了性能。实际开发时我会把余票查询落成一条原生 SQL:SELECT fno, cblevel, count(1) FROM ticket WHERE fno=? AND flydate=? AND status=1 GROUP BY fno, cblevel。
5.5 表空间路径写死:换机器复现必报 ORA-01119
现象:执行 CREATE TABLESPACE 时报ORA-01119: error in creating database file。原因:文档里的F:\APP\ORACLE\ORADATA\ORCL\TICKETSALE\是原作者电脑的路径,你的 Oracle 安装路径大概率不一样,目录也不存在。解决:先看自己 Oracle 的ORACLE_BASE和ORACLE_HOME路径,手动创建TICKETSALE目录,把四段 CREATE TABLESPACE 里的路径全部替换掉。顺便说一句,路径里别带中文,Oracle 对中文路径的支持一直很玄学,装上能跑是运气,跑不起来才是常态。
6. 验证方法:一条链路走完从建库到出票
整个系统的验证不需要搞复杂的测试框架,按业务顺序走一遍 SQL 就能确认每个模块是通的。我每次拿到这类课程设计资源,都会强制自己把全链路脚本按顺序执行一遍,不跳过任何一步。
第一段验证库结构。表空间、八张表、临时表建好后,执行SELECT table_name, tablespace_name FROM user_tables ORDER BY table_name,核对每张表是否落在规划的表空间里,重点看 ticket 是否在 TICKET 表空间、passenger 是否在 PASSENGER 表空间。
第二段验证参数化视图。这是整套设计最出彩的部分:
DELETE FROM input_to_flight; INSERT INTO input_to_flight VALUES ('', '广州', '长沙', ''); SELECT * FROM flight_view_bysite; DELETE FROM input_to_flight; INSERT INTO input_to_flight VALUES ('F0003', '', '', to_date('2020-06-10', 'yyyy-mm-dd')); SELECT * FROM remain_seats_view ORDER BY cblevel;查完航班再查余票,两次查询之间必须重写临时表,把第一步的旧参数清掉。
第三段验证存储过程生成机票。调用 create_ticket 后,查SELECT count(1) FROM ticket WHERE fno='F0003' AND flydate=to_date('2020-06-10','yyyy-mm-dd'),数量应该等于该航班所有舱位座位数之和。然后模拟一次完整售票:
INSERT INTO ticketsale VALUES (1, '440902199001011234', 'S0001', sysdate); SELECT * FROM ticket_info_view WHERE tno = 1;能查出印有乘客姓名、业务员姓名、折扣后实付价的完整记录,说明八表关联视图的 JOIN 条件全部正确。再查一次SELECT * FROM sale_grade_view,确认销售总额统计是按price*discount正确聚合的。
第四段验证权限和备份。用一个业务员账号登录后,它应该能查 salerecord_view 和 sale_grade_view,但不能执行 create_ticket,这能确认 GRANT 授权生效。备份命令我一般是这么验证:
-- 数据泵导出,验证备份方案可执行 expdp system/oracle schemas=system directory=DATA_PUMP_DIR dumpfile=ticket_backup.dmp logfile=ticket_backup.logEXPDP 能正常完成就没有大问题。容量估算可以用一个简单公式:ticket 表的行数约等于“航班数 × 每个航班的座位总数 × 运营天数”。按文档示例数据,8 个航班、每个航班两个舱位共 130 座、运营 30 天,就是8 * 130 * 30 = 31200行,这个量级对 100M 初始表空间是安全的。从那以后,我每次复现数据库课程设计资源,都强制自己把“建库→建表→查视图→跑存储过程→模拟售票→导出备份”完整走一遍,中途所有报错全部记下来,恰恰是这些报错让这个资源真正变成自己的。希望这份拆解帮到你,少踩几个我已经替你踩过的坑。
本文还有配套的精品资源,点击获取