Oracle考勤系统十二张表:建表顺序与外键约束实战

发布时间:2026/10/31 11:41:31
Oracle考勤系统十二张表:建表顺序与外键约束实战

简介:围绕学生考勤系统展开的Oracle数据库课程设计文档,面向学习数据库管理与开发、需要完成课程设计或实训报告的高校学生及IT初学者。内容从高校考勤管理背景与需求分析入手,划分学生、任课老师、班主任、院系领导、学校领导和系统管理员等角色,梳理请假、考勤记录、统计分析与后台管理等功能模块,并给出E-R模型、数据字典、数据库表逻辑结构、表空间与建表等设计环节,能够帮助读者理解从需求到Oracle实现的完整设计思路。压缩包内含1个doc文档,大小约227KB,便于直接查阅和参照报告结构。文档还包含表空间创建、建表及其他数据库对象的落地示例,并附课程设计心得体会与参考文献,便于读者快速把握报告组织方式、核对设计步骤,也可作为Oracle课程作业的参考模板。目前已有967人学习下载,适合用作课程设计选题参考、报告框架借鉴以及Oracle表结构与SQL实现的对照材料。

1. Oracle数据库课程设计:十二张表跑通之前,先解决三个顺序问题

把那份学生考勤系统的报告从头翻到尾,你会发现一个反直觉的事——最花时间的不是存储过程和触发器,而是建表顺序。报告里的 CREATE TABLE 是按 admin、student、faculty、major 排下来的,照抄执行,第二条就卡住:student 引用了 classes、major、faculty,这三张表当时还没建。Oracle数据库课程设计这类题目,考的从来不是你会不会写 CREATE TABLE,而是你能不能把六类用户的需求收敛成一套自洽的约束,再把约束翻译成能按顺序执行的脚本。下面按需求收敛、表结构定型、脚本跑通、踩坑排查、验收闭环走一遍,新手可以照着改参数直接用,熟手可以看我在哪几处做了取舍、为什么这么取舍。

2. 六类用户与三个模块:先把需求翻译成表和约束

课程设计报告里最常见的一段废话是"本系统涉及六类用户"。这句话本身没价值,有价值的是:六类用户其实对应六种不同的 WHERE 条件,而 WHERE 条件决定了你要建什么表、留什么字段、开什么视图。需求分析这一章写不好,后面 E-R 和建表全要返工。

2.1 六类用户的可见范围,本质是六个过滤条件

学生只看自己,任课老师看自己教的课,班主任看自己带的班,院系领导看本院系,学校领导看全校,管理员看全量。这六条落到 SQL 上就是六个不同的过滤维度,我把它们和对应的实现方式摆在一起看:

角色可见数据范围过滤依据实现方式
学生本人全部出勤与请假stu_no应用层绑定变量
任课老师本人所授课程的出勤teacher_no + course_no应用层绑定变量
班主任本班所有课程出勤class_no + classtea_no应用层绑定变量
院系领导本院系所有班级faculty_id视图或存储过程
学校领导全校无过滤全表统计
系统管理员全量增删改无过滤独立账号 + 角色

这张表定下来之后,一个关键判断就出来了:前三条是"行级过滤",靠应用层拼 WHERE 就够;后三条是"范围过滤",范围是稳定且有限的,才值得用视图封装。报告里给每个院系建一个视图(rjxy 这种)的做法,本质上是把范围写死在数据库对象里,院系一多就没人维护得动,这是典型的课程设计式偷懒,后面第 5 章会细说怎么改。

还有一个容易漏的点:班主任和任课老师是两套人还是同一套人。报告里 classteacher 和 teacher 是两张独立的表,各有自己的主键、姓名、性别、所属专业/学院。这意味同一个人既是某班班主任又教另一个班的课,就得在两张表里各存一条记录,改个电话号码要改两处。课程设计阶段这么分没问题,因为它换来了"班主任属于专业、教师属于院系"这种语义上的清晰;但你要知道这是个冗余,将来真做工程化,第一件事就是把这两张表合并成一张 person 表加角色关联表。

2.2 三个功能模块落到十二张表

请假、考勤、后台管理三个模块,最后收敛成十二张表。这个数量不夸张,但每张表存在的理由要说清楚,否则答辩时被问"这张表能不能删"就答不上来。

表名归属模块核心职责关键约束
admin后台管理管理员账号主键 admin_no
faculty后台管理院系字典主键 faculty_id
major后台管理专业字典,挂院系外键 faculty_id
classes后台管理班级,挂班主任外键 classtea_no
student后台管理学生,挂班级/专业/院系三个外键
teacher考勤任课教师,挂院系外键 faculty_id
classteacher请假班主任,挂专业/院系两个外键
collegeleader请假院系领导,挂院系外键 faculty_id
schoolleader考勤学校领导,无外键纯查询角色
course考勤课程字典主键 course_no
kaoqin_record考勤每节课每人的出勤结果三个外键
qingjia请假请假单 + 两级审批状态四个外键

系统管理员表看起来最虚,但它是唯一一张不参与业务关联的表,故意做成孤岛是有道理的——管理员不该被业务外键绑住,否则删一个院系还得先想管理员会不会受影响。字典类的 faculty、major、course 三张表加上 classes,构成了整个系统的骨架,前面说的"后台管理模块负责数据同步更新"实际上就是维护这四张表。四张字典表一动,student 和 kaoqin_record 的解释结果全变,所以外键约束在这里不是装饰,是防止数据变成孤儿的手段。

考勤记录表 kaoqin_record 是最容易建歪的一张。它同时挂着学生、教师、课程三个外键,看起来像个三向关联表,实际上它还带了 sk_time 和 stu_status 两个属性,等于把一个多对多联系提升成了带属性的实体。这么设计的好处是每节课都能独立追溯,坏处是数据量随"人数 × 课程数 × 周数"膨胀,一个班四门课一学期就是几万行。课程设计阶段这点数据量无所谓,但你要知道这是将来分表或加分区的原因。

2.3 从 E-R 到关系表的两处取舍

E-R 图那一章画得漂亮不难,难的是转换时的取舍。这份报告里有两处值得停下来想一想。

第一处:请假的两级审批状态,塞进一张表还是拆成一张审批流水表。报告的选择是塞进 qingjia,用 class_tea_sp_status 和 coll_leader_sp_status 两个字段分别记录班主任和院系领导的审批状态,各自配一个审批时间和审批人。这个做法的优点是查询简单,一条 SQL 就能看到请假单的全部状态;缺点是留不下审批历史——如果院系领导先驳回、学生修改后重新提交、班主任再审,整个过程只有最终状态,中间谁在什么时候改了什么,全丢了。课程设计级别这么写完全够用,但如果你的题目要求写"审批轨迹"或者"操作日志",就必须拆出 qingjia_approve_log 表,用 qingjia_id + 审批人 + 审批时间 + 动作 四列记流水,主表只保留当前状态做冗余。

第二处:请假天数 day_number 是存还是算。报告里存了,而且在数据字典里明确定义成"请假天数"。存的好处是查询快,统计时不用每次算日期差;坏处是它和 start_time、end_time 会产生不一致——学生先请三天,班主任批的时候改成两天,如果更新脚本只改了 end_time 忘了改 day_number,报表就错了。我一般倾向不存这个字段,需要时用end_time - start_time现算,Oracle 里 DATE 相减得到的是天数,天然就够用。真要存,就必须用触发器或生成列把它和起止时间绑死,让它不可能被单独改动。

顺带说一句 E-R 图里那堆 1:1、1:n、m:n 的标注。报告里学生和班级是 n:1、班级和班主任是 n:1、学生和课程通过考勤是 m:n,这些都没问题。真正容易画错的是请假条和学生——一个学生一个学期可以请很多次假,所以是 1:n,但如果你的设计里允许一个请假单覆盖多个学生(比如整班外出实习),那就变成了 m:n,得加中间表。报告选的是 1:n,字段上就体现为 qingjia 表里直接放 stu_no,这个决定要在需求分析阶段就锁定,别到建表时才改。

3. 十二张表的逻辑结构:主键、外键与类型怎么定

逻辑结构设计那一章的表格,大部分人只关心"字段名写对没有",其实真正决定这张表好不好用的是三件事:主键谁来生成、外键列和被引用列类型长度是否一致、状态字段用什么类型。这三件事定错了,后面写多少存储过程都救不回来。

3.1 主键用 CHAR 还是 NUMBER,取决于谁生成

报告里的主键类型是混着来的:admin_no 是 char(5),stu_no 是 char(10),faculty_id 是 number,kaoqin_id 是 char(13),qingjia.id 是 number。看起来乱,其实有内在逻辑——有天然业务编号的用 CHAR,没有的用 NUMBER。

学号、教师工号、课程号、管理员编号这些在现实里本来就是一串字符,而且带前导零(比如 0820980113),用 CHAR 存最省事,还避免了 NUMBER 把前导零吃掉的问题。院系编号、专业编号、请假单号这些现实中不存在的编号,就用 NUMBER 加序列生成,省得手工拼字符串拼出重复。

这里有个坑要说清楚:CHAR 是定长,VARCHAR2 是变长。stu_no 定 char(10),存进去 8 位学号会在尾部补两个空格,查询时where stu_no = '08209801'因为 blank-padded 比较语义居然能匹配上,看起来很智能;但一旦用length(stu_no)做统计,得到的是 10 不是 8,做字符串拼接时会多出空格。我的习惯是业务编号一律 VARCHAR2,除非能百分百确定长度固定且永远不会变。报告里定 char(10) 不算错,因为它假设学号统一 10 位,但这个假设一旦破了就是隐患。

kaoqin_id 定 char(13) 更值得说。出勤记录的主键如果靠人工拼(比如"学号后 6 位 + 日期"),并发插入时极易撞主键;正确做法是用序列seq_kaoqin.nextval生成,或者干脆用 12c 之后的GENERATED BY DEFAULT AS IDENTITY。课程设计里数据量小、单人操作,撞主键的概率接近零,但这是典型的"能跑就行、上线就炸"式写法,答辩时被问一句"两个人同时打卡怎么办"就露馅了。我一般会在这里顺手加一句注释说明为什么用序列,成本几乎为零。

3.2 外键顺序与类型一致性,是两个独立的坑

先说顺序。外键要求被引用的表和列必须已经存在,所以建表顺序必须按依赖关系拓扑排序。这份设计里依赖关系是这样的:

faculty ← major ← classteacher ← classes ← student ↑ ↑ teacher ──────────────────────────────┘ ↑ course ← kaoqin_record → student qingjia → classes / student / classteacher / collegeleader

按这个图,安全的建表顺序是:faculty → major → classteacher → classes → student → teacher → course → collegeleader → schoolleader → admin → kaoqin_record → qingjia。报告里把 admin 和 student 排在最前面,纯粹是"从第一张表开始写"的习惯,执行必挂。这个顺序不是死记的,你只要记住一句话:先建字典表,再建主体表,最后建业务流水表。

再说类型一致性,这个坑比顺序更隐蔽。Oracle 允许外键列和被引用列类型相近但不完全相同(比如 char(13) 引用 char(10)),它不会在建表时报错,但会带来两个后果:一是字符集或长度不一致时可能触发隐式转换,让被引用列上的索引失效;二是后续插入超长值时才报 ORA-12899,报错位置离你写错的地方隔了十万八千里。报告里 student.stu_class 在数据字典里写 char(13),在逻辑结构表里也写 char(13),但在 CREATE TABLE 语句里写成了 char(5),而 classes.class_no 是 char(10)——三处对不上。这种前后不一致,跑起来不报错,等你要插一个 8 位班级号进去才发现装不下。

我的做法是:外键列一律%TYPE或直接照抄被引用列的定义,写完所有建表语句后跑一句校验:

-- 检查所有外键列与被引用列的类型长度是否完全一致 select c.table_name, cc.column_name, cc.data_type as fk_type, cc.data_length as fk_len, rc.table_name as ref_table, rc.column_name as ref_column, rcc.data_type as ref_type, rcc.data_length as ref_len from user_constraints c join user_cons_columns cc on cc.constraint_name = c.constraint_name join user_constraints rc on rc.constraint_name = c.r_constraint_name join user_cons_columns rcc on rcc.constraint_name = rc.constraint_name and rcc.position = cc.position where c.constraint_type = 'R' and (cc.data_type <> rcc.data_type or cc.data_length <> rcc.data_length);

这段查的是当前用户下所有外键约束,用user_cons_columns按 position 对齐外键列和被引用列,只要类型或长度有一个不同就列出来。返回空集才算过。写完建表脚本先跑它,比等业务跑歪了再回头找强得多。

3.3 审批状态字段:别看只有三个值,类型选错一样返工

请假单上有两个状态:班主任审批状态、院系领导审批状态。报告里定成 char(10),值域是 0/1/2,含义是 0 待审批、1 同意、2 不同意。定 char(10) 存单个数字,等于用十个字节存一个字节的信息,而且没有任何约束兜底——插入一个 '9' 或者 '待审批' 都能进。

我的写法是number(1)加 CHECK:

class_tea_sp_status number(1) default 0, constraint ck_qj_tea_status check (class_tea_sp_status in (0, 1, 2)), coll_leader_sp_status number(1) default 0, constraint ck_qj_lead_status check (coll_leader_sp_status in (0, 1, 2))

用 NUMBER(1) 而不是 CHAR(1),是因为数值型在比较时不存在空格填充的玄学问题,where status = 0和where status = '0'在 CHAR 下行为不同,在 NUMBER 下没有歧义。加 DEFAULT 0 更关键——新插入的请假单必须默认处于待审批状态,如果靠应用层记得传这个值,早晚会漏。

还有一个三值问题要提前想清楚:状态字段能不能为 NULL。如果允许 NULL,那 NULL 到底算"未提交"还是"待审批",代码里每次判断都要写status is null or status = 0,非常容易漏。我一般把状态字段设成 NOT NULL + DEFAULT 0,把"未提交"这种状态放在业务层用别的字段区分,数据库里永远只有一个明确的 0/1/2。

至于 day_number 这个冗余字段,前面说过倾向不存。真要存,就在建表时加一条 CHECK 把它和起止时间绑住:

constraint ck_qj_day check (day_number = end_time - start_time)

这条约束会让"改时间不改天数"的更新直接失败,比事后对账强。注意 DATE 相减得到的是带小数的天数,如果请假起止是整点,结果刚好是整数;一旦带时分秒,这条 CHECK 会因为小数位不匹配而误伤,所以要么把日期截断到天,要么把 day_number 定义成 number 而不是 number(2)。

4. 表空间到建表脚本:把报告里的 SQL 真正跑通

报告最后一章给了完整的建表脚本和三个数据库对象,这是全文最有价值也最容易翻车的部分。我照着手敲过一遍,前前后后改了七处才跑通,这里按执行顺序说。

4.1 表空间与存储参数:哪些写了有用,哪些是摆设

报告里建表空间的语句是这样的:建一个名为 linpeng_data 的表空间,数据文件放在 /u01/oracle/oradata/tab01.dbf,初始 100M,然后跟一串 DEFAULT STORAGE(initial 512K next 128K minextents 2 maxextents 999 pctincrease 0)。

这串存储参数在**本地管理表空间(LMT)**下基本是被忽略的。Oracle 10g 之后 LMT 是默认选项,区(extent)的分配由数据文件头部的位图统一管理,不再依赖数据字典,所以 initial、next、minextents、maxextents、pctincrease 这些字典管理时代的参数写上去也只是语法兼容。真正影响分配行为的是这几项:

-- 建一个专供考勤模块使用的表空间,路径和大小按实际环境改 create tablespace ts_kaoqin datafile '/u01/oracle/oradata/kaoqin01.dbf' size 100M autoextend on next 50M maxsize 2G -- 用满 100M 后每次扩 50M,上限 2G extent management local autoallocate -- 区大小由 Oracle 自动决定,避免手工估算 segment space management auto; -- 段空间自动管理,避免 pctfree/pctused 手工调

autoextend是这里最该加的一句。课程设计阶段 100M 够用,但触发器一跑、批量灌数据,临时段和回滚段一起涨,很快就会遇到 ORA-01653 表空间无法扩展。加上自动扩展能省掉一次半夜排查。autoallocate让 Oracle 自己决定每次分配多大的区,比uniform size 128K灵活,代价是区大小不规整,对超大规模数据仓库不友好,但考勤系统这种体量完全够。

建完之后建议顺手确认一下:

-- 确认表空间类型与自动扩展状态 select tablespace_name, extent_management, allocation_type, status from dba_tablespaces where tablespace_name = 'TS_KAOQIN'; select file_name, bytes/1024/1024 as mb, autoextensible, maxbytes/1024/1024 as max_mb from dba_data_files where tablespace_name = 'TS_KAOQIN';

第一句看的是 LMT 有没有生效(extent_management 应为 LOCAL),第二句看自动扩展开没开。如果第一句返回 DICTIONARY,说明你用的版本很老或者语句被改过,那 DEFAULT STORAGE 才真的有用。

4.2 建表语句里必须改掉的几处语法

报告里的 CREATE TABLE 有六类问题,逐个说,因为这些都是答辩时被追问"你这脚本跑过吗"的致命点。

第一类:datetime 不是 Oracle 的类型。报告里 kaoqin_record.sk_time、qingjia.start_time 等全写成 datetime,Oracle 里根本没有这个类型,会直接报 ORA-00902: invalid datatype。正确写法是 DATE(精度到秒)或 TIMESTAMP(精度到小数秒)。考勤打卡只需要到秒,用 DATE 就够,别为了显得高级上 TIMESTAMP,它会多占 5 个字节。

第二类:列级外键不能写 FOREIGN KEY。报告里写的是stu_class char(5) foreign key references classes(class_no),这行的 FOREIGN KEY 三个字是多余的。列级约束只写REFERENCES 表(列),表级约束才写CONSTRAINT 名 FOREIGN KEY (列) REFERENCES 表(列)。多了那三个词,报 ORA-00907: missing right parenthesis,而且报错位置在语句末尾,很容易误判成括号问题。

第三类:拼写错误。day_number 定成nubmer,参数定成outnumber(应为out number),这类错在建表阶段报 ORA-00902,在存储过程里报 PLS-00103,都是低级但高频。

第四类:varchar 能用但不该用。Oracle 里 VARCHAR 目前是 VARCHAR2 的同义词,能跑,但官方文档明确说这是为了兼容保留的,将来行为可能变。统一写 VARCHAR2,别留把柄。

第五类:中文字段长度。admin_sex char(2)想在里存"男"或"女"。如果数据库字符集是 AL32UTF8 且长度语义是默认的 BYTE,一个汉字占 3 字节,CHAR(2) 只能装 2 字节,插"男"直接报 ORA-12899: value too large for column。解决办法有两个:把长度改成 char(3) 以上,或者建库时把NLS_LENGTH_SEMANTICS设成 CHAR。我一般直接把性别定成varchar2(6),够宽,也不在乎那点空间。

第六类:表名和字段名大小写与保留字。classes、course、admin 这些词本身不冲突,但title、dept、status在部分版本里要小心。稳妥做法是建表时全小写不用引号,Oracle 会自动转成大写存储,查询时不加引号也能命中。

改完之后的建表语句长这样,以 user 表和 attendance 表为例:

-- 学生表:三个外键分别指向班级、专业、院系,删除行为设为 restrict 防止误删 create table student ( stu_no varchar2(10) not null, stu_name varchar2(30) not null, stu_sex varchar2(6) not null, stu_class varchar2(10), stu_major number(4), stu_faculty number(4), constraint pk_student primary key (stu_no), constraint ck_stu_sex check (stu_sex in ('男','女')), constraint fk_stu_class foreign key (stu_class) references classes(class_no), constraint fk_stu_major foreign key (stu_major) references major(major_id), constraint fk_stu_faculty foreign key (stu_faculty) references faculty(faculty_id) ) tablespace ts_kaoqin; -- 出勤记录表:主键由序列生成,三个外键构成"谁在哪节课什么状态" create table kaoqin_record ( kaoqin_id number(12) not null, sk_time date not null, stu_number varchar2(10) not null, stu_status varchar2(10) not null, teacher_no varchar2(10), course_no varchar2(13), constraint pk_kaoqin primary key (kaoqin_id), constraint ck_stu_status check (stu_status in ('出勤','迟到','早退','请假','旷课')), constraint fk_kq_stu foreign key (stu_number) references student(stu_no), constraint fk_kq_tea foreign key (teacher_no) references teacher(tea_no), constraint fk_kq_course foreign key (course_no) references course(course_no) ) tablespace ts_kaoqin;

两处说明。第一,所有主键都显式命名成pk_表名、外键命名成fk_缩写_缩写,这是为了后面改约束时不用去查系统自动生成的名字(SYS_C0012345 这种)。第二,stu_status 用 VARCHAR2 存中文状态而不是存 0/1/2 编码,好处是可读性强、直接查表就能看懂,坏处是占空间且容易打错字。课程设计里可读性优先,真上生产我可能会换成 number 编码加一张状态字典表,但那是另一个话题。

4.3 存储过程、视图、触发器三件套的落地写法

报告给了三个数据库对象:统计某学生某课程缺勤次数的存储过程、给院系领导看本院系数据的视图、缺勤超三次提示的触发器。三个都有问题,改法如下。

存储过程原版是select count(*) in to absence_times from kaoqin_record where stu_number=stu_no and course_no=course_no,问题有三个:in to应为into;total_times=absence_times少了冒号,应为:=;最要命的是 count(*) 没有过滤 stu_status,统计的是"上了多少节课"而不是"缺勤多少次"。改完:

-- 统计某学生某课程的缺勤次数,只统计状态为"旷课"的记录 create or replace procedure get_absence_times ( p_stu_no in varchar2, p_course_no in varchar2, p_times out number ) as begin select count(*) into p_times from kaoqin_record where stu_number = p_stu_no and course_no = p_course_no and stu_status = '旷课'; -- 关键:没有这一行,统计的是总课次 exception when no_data_found then p_times := 0; -- 一次都没缺勤时也要给出 0,不能让调用方拿到 NULL end; /

参数加 p_ 前缀是为了和列名区分,避免where stu_number = stu_number这种自比较(Oracle 会优先解析成列,条件恒真,结果全表返回,非常隐蔽)。加上when no_data_found兜底,是因为 count(*) 其实不会触发 no_data_found,但如果将来改成 select 单个字段就会,留着不亏。

视图原版是给每个院系建一个视图,把student.stu_faculty = 5写死在 WHERE 里。问题很直接:加一个院系就要建一个视图,20 个院系就是 20 个几乎一样的对象。更好的做法是用带参数的存储过程返回游标:

-- 按院系返回该院系学生的出勤明细,院系 ID 由调用方传入 create or replace procedure get_faculty_kaoqin ( p_faculty_id in number, p_cursor out sys_refcursor ) as begin open p_cursor for select k.kaoqin_id, k.sk_time, k.stu_number, k.stu_status, k.course_no from kaoqin_record k join student s on s.stu_no = k.stu_number where s.stu_faculty = p_faculty_id order by k.sk_time desc; end; /

sys_refcursor是 Oracle 提供的弱类型游标,应用层拿到后当结果集遍历即可。这么改的好处是院系增删完全不用动数据库对象,坏处是权限控制跑到了应用层——如果应用层忘了传过滤条件,就会泄露全校数据。所以真要严格隔离,正确方案是 Oracle 的虚拟私有数据库(VPD)或者行级安全策略,但那是 DBA 级别的操作,课程设计里用存储过程加应用层校验已经足够,答辩时说清这个权衡反而加分。

触发器原版是after insert on kaoqin_record里统计次数,超过 3 次就dbms_output.put_line提示。三个问题:统计时没过滤 stu_status;>= 3的判断发生在插入之后,第一次到 3 就提示,但实际业务里通常是"缺勤满 3 次取消考试资格",边界要看学校规定;最重要的是 dbms_output 只在 SQL*Plus 里set serveroutput on之后才看得到,JDBC 连接下这段输出直接进了虚空,等于没写。改法有两种,看你的目标:

-- 方案A:只记录,不阻断。把超限提醒写进日志表,应用层轮询或人工查 create or replace trigger trg_kq_alert after insert on kaoqin_record for each row declare v_times number; begin if :new.stu_status = '旷课' then select count(*) into v_times from kaoqin_record where stu_number = :new.stu_number and course_no = :new.course_no and stu_status = '旷课'; if v_times >= 3 then insert into kaoqin_alert(stu_no, course_no, alert_time, alert_msg) values (:new.stu_number, :new.course_no, sysdate, '缺勤已达' || v_times || '次'); end if; end if; end; /

方案 A 适合"提醒"场景。如果要"直接阻止录入",就把 insert 换成raise_application_error(-20001, '该生缺勤已超限'),但要注意:触发器里抛异常会让整个 INSERT 回滚,如果业务上只是想让老师看到警告、记录还是要存,就不能这么干。这个区别在答辩时被问到的概率很高。

还有一点:after insert触发器里查同一张表是可以的,因为此时行已经写入,能看到包括新行在内的全部数据;但如果写成before insert,新行还没落地,count 会少算一条,边界就从"满 3 次"变成"满 4 次"。这个 off-by-one 极其隐蔽,我见过有人在生产上因为这个把规则执行错了半年才发现。

5. 避坑与排查:五个照着报告抄必然踩的坑

以下五条都是我实际敲脚本时踩过的,按出现顺序排,每条给现象、原因、解决。

5.1 建表阶段:报错位置和真实原因永远不在一个地方

现象:执行第三条 CREATE TABLE 时报 ORA-00942 表或视图不存在,但那张表明明前面建过。原因:外键引用的表还没建。Oracle 的报错说的是"被引用的那个对象不存在",而不是"你建表顺序错了",所以你会顺着报错去查被引用的表,发现它在脚本更后面。这类错误在十二张表的脚本里平均出现三次。解决:把建表语句按"字典表 → 主体表 → 流水表"重排,或者在脚本最前面统一加一批alter table ... add constraint ... foreign key ...,先建表后加约束,顺序问题一次性消失。后者更适合多人协作,因为表结构定义和约束定义分离,合并脚本时不容易冲突。

现象:ORA-00907 missing right parenthesis,括号数了三遍都是配对的。原因:列定义里多了foreign key三个字,或者varchar(20) not null后面跟了逗号又跟约束。解析器在遇到不认识的语法时会一路找到语句末尾才报错,所以报错行号指向最后一行。解决:列级外键只写references 表(列);养成"一个列定义一行、约束单独一行"的书写习惯,出问题时能一眼定位。

5.2 数据写入阶段:中文和日期是两个高发区

现象:插入一条管理员记录,报 ORA-12899: value too large for column "ADMIN"."ADMIN_SEX" (actual: 3, maximum: 2)。原因:CHAR(2) 在默认的 BYTE 长度语义下只有 2 个字节,UTF-8 里一个汉字占 3 字节,装不下。解决:性别字段改varchar2(6),或者建库时把NLS_LENGTH_SEMANTICS设成 CHAR 让长度按字符算。前者简单粗暴,后者影响全局要慎重。

现象:插入日期报 ORA-01861: literal does not match format string。原因:直接用insert ... values('2009-12-29')往 DATE 列塞字符串,依赖数据库的NLS_DATE_FORMAT,而这玩意儿每个客户端环境可能都不一样,你机器上能跑,换台机器就炸。解决:永远显式转换,to_date('2009-12-29','yyyy-mm-dd')或date '2009-12-29'。后者更短,而且不受任何会话参数影响,我现在基本只用这一种。

5.3 对象阶段:触发器不生效、视图改不动

现象:触发器写好了,插入超限数据,控制台一片安静,什么提示都没有。原因:dbms_output.put_line的输出进了会话缓冲区,SQL*Plus 要set serveroutput on才显示,JDBC/ODBC 连接下压根没人读这个缓冲。等于写了句没人听的广播。解决:要留痕就写日志表,要阻断就raise_application_error,别指望 dbms_output 在应用里起作用。这个坑我第一次踩的时候排查了两小时,最后发现是"输出被吞了"而不是"逻辑没执行"。

现象:视图建好了,院系领导登录看到的还是全校数据。原因:视图里写死了student.stu_faculty = 5,但领导登录的院系 ID 是 7,而应用层直接select * from 视图名没有任何附加条件,视图返回的是"5 号院系的全部数据",跟登录人无关。解决:视图方案必须配应用层的二次过滤,或者换成带参数的存储过程(4.3 那节的写法)。最忌讳的是把"视图名"当成"权限",视图只是预定义的查询,它不认识登录用户。

5.4 排查顺序:先看约束,再看数据,最后看代码

遇到"数据不对",我的固定顺序是:先查约束是否生效(user_constraints里 status 是不是 ENABLED),再查数据本身(有没有孤儿行、有没有 NULL 混进 NOT NULL 字段),最后才怀疑存储过程逻辑。因为前两类问题的排查成本是分钟级,第三类是小时级,而这个顺序能保证你在 80% 的情况下不用进第三类。这个习惯比任何具体技巧都值钱。

6. 从能跑到能验收:把考勤统计做成一个可验证的闭环

脚本跑通、数据能插,只是及格线。真正让这份课设拉开差距的,是能不能回答一个问题:请假和考勤对得上吗。

报告的需求里写得很清楚——"系统自动根据学生请假系统,决定学生上课出勤的最终结果"。这句话翻译成 SQL,就是"如果一个学生在某节课的时间段内处于已批准的请假状态,那他不应该被记为旷课"。这是整个系统里唯一一处跨模块的业务规则,也是答辩时最容易被追问的地方。写出来大概是这样:

-- 找出被误判为旷课、但实际有已批准请假的记录 select k.kaoqin_id, k.stu_number, k.course_no, k.sk_time, q.id as qingjia_id from kaoqin_record k join qingjia q on q.stu_no = k.stu_number where k.stu_status = '旷课' and q.class_tea_sp_status = 1 -- 班主任已同意 and q.coll_leader_sp_status = 1 -- 院系领导已同意 and k.sk_time between q.start_time and q.end_time; -- 上课时间落在请假区间内

这段查出来的每一行,都是一条"本该是请假、却被记成旷课"的错账。反过来,如果要生成最终的出勤报表,就用 NOT EXISTS 把请假区间排除掉:

-- 生成某学生某课程的最终出勤统计:请假优先于旷课 select s.stu_no, c.course_no, count(*) as 总课次, sum(case when k.stu_status = '出勤' then 1 else 0 end) as 出勤, sum(case when k.stu_status = '请假' then 1 else 0 end) as 请假, sum(case when k.stu_status = '旷课' and not exists ( select 1 from qingjia q where q.stu_no = k.stu_number and q.class_tea_sp_status = 1 and q.coll_leader_sp_status = 1 and k.sk_time between q.start_time and q.end_time ) then 1 else 0 end) as 有效旷课 from kaoqin_record k join student s on s.stu_no = k.stu_number join course c on c.course_no = k.course_no where s.stu_no = :stu_no and c.course_no = :course_no group by s.stu_no, c.course_no;

有效旷课这一列是关键:它把"状态写着旷课"和"实际算旷课"分开了。中间那个 NOT EXISTS 子查询逐行判断这条考勤记录的时间点是否落在某张已双审通过的请假单区间内,落在里面就不计入。between是闭区间,请假当天最后一节课和返校当天第一节课都会算进去,如果学校规定"请假结束当天必须到课",就得改成k.sk_time < q.end_time。这种边界规则不问清楚,写出来的统计永远和教务处对不上。

验收的时候我会准备一张小表,把边界情况列出来逐条核对:

场景请假状态考勤状态期望结果
请假三天,双审通过1/1旷课不计入有效旷课
请假三天,班主任同意院系未审1/0旷课计入有效旷课
请假一天,只请了上午1/1下午旷课计入有效旷课
未请假直接缺勤无记录旷课计入有效旷课
请假被驳回2/2旷课计入有效旷课

这五行跑通,考勤统计的逻辑才算立住。剩下的事就是把统计结果包装成学生能看的"本学期出勤概况"、班主任能看的"本班旷课排行",本质上都是同一套 SQL 换个 group by 维度。

回过头看,这份课设真正教人的不是 Oracle 的语法,而是一个顺序:需求先收敛成过滤条件,过滤条件决定表和约束,表和约束决定脚本能不能按序执行,脚本跑通之后再谈业务规则。我后来的习惯是——任何数据库设计,建表脚本写完之后,先跑一遍外键类型一致性检查,再手工造五条边界数据验证触发器,两件事加起来不超过二十分钟,但能挡掉后面百分之八十的返工。这套流程从当年那份考勤系统一直用到现在,中间救过我很多次。希望帮到你。

本文还有配套的精品资源,点击获取