☰
数据库实验报告5实战:存储过程与触发器完整拆解指南
2026/10/13 21:17:37 网站建设 项目流程

简介:西北工业大学数据库实验报告第五部分围绕数据库核心操作展开,面向正在学习《数据库原理》的本校学生及需要动手实践存储过程与触发器的初学者。报告系统梳理了数据表的创建与管理、视图重命名等基础操作,并重点演示了带参数存储过程jsearch的编写与执行、加密存储过程jmsearch的创建与删除,以及利用系统存储过程sp_helptext查看文本信息的方法。触发器部分同样充实,覆盖针对SC表和S表的INSERT、DELETE、UPDATE触发器设计,如禁止插入无效记录、禁止删除S表、级联删除选课记录、禁止修改sdept字段,还包含触发器禁用、删除和动态更新CAvgGrade统计表等进阶场景。每个实验都配有可运行的SQL语句和执行结果,便于对照练习。压缩包包含1个doc文档,约231KB,完整展示11页实验报告。已有716人学习下载,适合作为课内实验的参考范本,也能帮助读者深入理解存储过程与触发器的创建流程及验证思路。

1. 拿到西北工业大学数据库实验报告5.doc,先分清要补的是SQL还是数据库对象

每学期期末,这类文件名总会以各种途径出现在专业课群和网盘里。我见过不少人把“西北工业大学数据库实验报告5.doc”当成一份现成答案,复制粘贴后跑出结果,却答不上来老师的追问。实验报告5和前四个实验有本质区别:前四个实验练的是数据库增删改查,考验你输入哪条SQL;实验5练的是存储过程、触发器和完整性约束,考验你把业务规则写进数据库本身,而不是往查询窗口里塞一条长SQL。下面把实验5从建表到触发器验证的完整路径拆开,帮你把报告写成一份能自圆其说的技术说明。这份路径适合正在补实验报告、准备数据库课程设计,或者兜底复习数据库SQL的读者。

2. 实验5到底要做什么:把数据库增删改查升级成数据库对象编程

2.1 为什么大学数据库实验把存储过程和触发器放在实验5

国内高校数据库实验课最常见的排法,前面是SQL基础、查询优化、视图和索引,到实验5开始进入“数据库对象编程”。存储过程和触发器不是新的SQL语法,而是把一段业务逻辑放进了数据库服务器内部,由服务器来保证它要么全部执行、要么全部回滚。实验5考察的其实是你是否理解数据库SQL的“执行边界”:一条SELECT在哪返回、一个事务从哪开始、一条触发器语句什么时候被悄悄触发。

很多人在实验5翻车,是因为还在用写查询的思路写过程。查询是“你给我条件,我把结果甩给你”,存储过程则是“你来调用我,我把一段完整操作做完,再告诉你成没成功”。差值就在这,报告里最值得写的也是这个认知转变。

2.2 环境与前置准备:字符集、登录参数、客户端工具

做实验5前先确认环境,别急着把代码贴进去。最常见的组合是MySQL 8.0以上版本加Navicat或DataGrip,个别学校用SQL Server,语法上有差异,但事务和触发器模型是相通的。先跑一次系统变量检查,把结果截图放进报告,这也是让实验报告5.doc看起来不那么空的第一步。

mysql -u root -p -e "SHOW VARIABLES LIKE 'character_set_%';"

这条命令把当前实例的字符集相关变量全部列出来,重点关注character_set_server和character_set_database。如果其中一个是latin1,后面插入中文姓名和备注一定会变成问号。建议在MySQL配置文件my.cnf的[mysqld]段落里固定下面的参数,否则只是当前会话有效,下次重启又回去了。

character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci

参数说明:utf8mb4是utf8的超集,能存emoji和生僻字,数据库课程设计里用它最保险;collation决定排序和比较规则,utf8mb4_unicode_ci按Unicode标准排序,做模糊查询时行为更直观。设置完重启MySQL服务再连一次,确认这两项不再是latin1。

2.3 建表脚本和初始化数据:把账户、流水两张表先立起来

实验5的核心业务模型不需要复杂,两张表就够:一张账户表accounts,一张流水表transactions。账户表负责存余额,流水表负责记录每一笔收付。实验报告里先贴建表脚本,再贴几条插入语句,这是整个实验的“地基”,触发器后面全部围绕这两张表转。

DROP DATABASE IF EXISTS db_exp5; CREATE DATABASE db_exp5 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE db_exp5; CREATE TABLE accounts ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(30) NOT NULL, balance DECIMAL(12,2) NOT NULL DEFAULT 0.00, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE transactions ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, from_account INT UNSIGNED NOT NULL, to_account INT UNSIGNED NOT NULL, amount DECIMAL(12,2) NOT NULL, tx_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_from FOREIGN KEY (from_account) REFERENCES accounts(id), CONSTRAINT fk_to FOREIGN KEY (to_account) REFERENCES accounts(id) ) ENGINE=InnoDB;

说明一下几个容易在报告里被问到的点。DECIMAL(12,2)是定点数,金额字段不要用FLOAT或DOUBLE,二进制浮点会在小数点后产生误差;TIMESTAMP配ON UPDATE CURRENT_TIMESTAMP,每次UPDATE都会自动刷新最后修改时间,这是实验报告里可以写一句“利用数据库内建机制维护审计字段”的细节。外键约束写在表定义里而不是应用层,目的是让数据库自己拒绝不存在的账户编号,这也正好呼应了“完整性约束”这个实验主题。

初始化数据用一条INSERT带VALUES列表完成:

INSERT INTO accounts(name, balance) VALUES ('张三', 1000.00), ('李四', 500.00);

这时候往transactions里插一条流水,余额不会自动变,因为触发器还没有建。先手动插一条,观察两个账户余额不变,这就是后面触发器的“前后对比图”素材。

INSERT INTO transactions(from_account, to_account, amount) VALUES (1, 2, 100.00); SELECT * FROM accounts;

跑完这条SELECT,你会发现张三还是1000、李四还是500。报告里把这句话写清楚:“流水记录成功,但余额未同步”,后面触发器演示的对比效果就出来了。别小看这一步,没有这个对照组,你很难向老师证明触发器真的在干活。

3. 存储过程怎么写进实验报告:参数、事务和死锁边界

3.1 设计一个带IN参数和OUT参数的转账存储过程

实验5必做项目里,转账是出现频率最高的业务,因为它天然需要事务:扣款、入账、写流水,三个动作必须同生共死。给存储过程加上IN参数控制输入,OUT参数返回执行码,这样调用方就能判断成功还是失败。下面这段代码可以直接跑,也建议原样写进实验报告。

DROP PROCEDURE IF EXISTS sp_transfer; DELIMITER $$ CREATE PROCEDURE sp_transfer( IN p_from INT UNSIGNED, IN p_to INT UNSIGNED, IN p_amount DECIMAL(12,2), OUT p_code TINYINT ) BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SET p_code = 1; END; START TRANSACTION; UPDATE accounts SET balance = balance - p_amount WHERE id = p_from AND balance >= p_amount; UPDATE accounts SET balance = balance + p_amount WHERE id = p_to; INSERT INTO transactions(from_account, to_account, amount) VALUES (p_from, p_to, p_amount); COMMIT; SET p_code = 0; END$$ DELIMITER ;

逻辑说明:EXIT HANDLER 是异常处理器,任何一步报错就直接ROLLBACK,并且把返回码置为1,相当于把“后悔药”写进了数据库里。第一个UPDATE的WHERE条件带了balance >= p_amount,余额不够时影响行数为0,不会扣成负数;第二个UPDATE给收款方加钱;最后插流水。所有操作包在START TRANSACTION和COMMIT之间,任一步失败,前面做的全部撤销。

参数说明:IN p_from和IN p_to是入参,类型和accounts.id严格一致用INT UNSIGNED,避免隐式类型转换导致索引失效;IN p_amount用DECIMAL(12,2),和表字段精度一致;OUT p_code是输出参数,用来告诉客户端“成功返回0,异常回滚返回1”。命名上统一加p_前缀,避免和列名混合产生歧义,这是写存储过程的一个实用习惯。

3.2 事务边界:COMMIT放过程内还是调用方

上面这份代码把COMMIT写在了存储过程内部。实际生产里更讲究的做法是把COMMIT放到调用方,存储过程只负责业务动作和ROLLBACK,因为存储过程有可能被嵌进一个更大的事务里。一旦过程内部COMMIT,外层事务就没法整体回滚了。实验报告里不需要两种都写,但设计说明里加一句“本实验为简化调用,将事务边界放在过程中”,老师就会知道你考虑过这个问题。

事务边界直接关联到另一个高频考点:数据库并发锁。开两个mysql会话,会话A先手动执行一条UPDATE但不COMMIT,会话B再去更新同一个账户,会发现B卡住不动。查询SHOW PROCESSLIST,能看到B的State是Waiting for store lock,这就是InnoDB行锁在起作用。实验报告5的加分项之一,就是把这个等待现象截图,配一句话说明“数据库通过行级锁避免并发转账的覆盖写”。

SHOW PROCESSLIST;

这个命令会列出所有连接和当前状态。看到“Waiting for store lock”后,在会话A执行COMMIT或ROLLBACK,B就会立刻返回。注意不要在生产环境乱试,本地实验环境随便折腾,这正是课程实验最有价值的地方。

3.3 调用过程和排错:CALL之后看什么

存储过程写好了必须调用验证。调用语句本身不难,难的是调用完以后怎么判断结果对不对。下面这段验证脚本会一次把返回码、账户余额、流水记录全部查出来。

CALL sp_transfer(1, 2, 50.00, @rc); SELECT @rc AS return_code; SELECT * FROM accounts ORDER BY id; SELECT * FROM transactions ORDER BY id;

这段脚本的执行结果应该有四个特征:@rc返回0;张三余额从1000变成950;李四从500变成550;transactions里多了一条金额50的流水。如果@rc返回1,说明事务里某个步骤抛了异常,优先检查外键指向的账户ID是否存在、p_amount是否超过余额。

再试一个故意出错的调用,比如把转出账户ID写成999:

CALL sp_transfer(999, 1, 10.00, @rc); SELECT @rc AS return_code;

这里第一个UPDATE找不到id=999,影响0行;第二个UPDATE给张三加10;INSERT流水时外键会把from_account=999拒绝,从而触发异常处理器ROLLBACK。最终结果是@rc=1,张三还是950,没有多出10块钱。这就是事务回滚的证据,报告里必写。

4. 触发器部分:NEW与OLD的引用规则和报告写法

4.1 一个AFTER INSERT触发器:让流水表自动更新账户余额

存储过程解决的是“主动调用”的问题,触发器解决的是“被动触发”的问题。实验5里最典型的触发器场景是:往transactions插入一条流水后,数据库自动更新accounts的余额,不需要应用层再调一次UPDATE。先重置账户余额到初始状态,再建触发器。

UPDATE accounts SET balance = 1000.00 WHERE id = 1; UPDATE accounts SET balance = 500.00 WHERE id = 2; DROP TRIGGER IF EXISTS trg_tx_balance_ins; DELIMITER $$ CREATE TRIGGER trg_tx_balance_ins AFTER INSERT ON transactions FOR EACH ROW BEGIN UPDATE accounts SET balance = balance - NEW.amount WHERE id = NEW.from_account; UPDATE accounts SET balance = balance + NEW.amount WHERE id = NEW.to_account; END$$ DELIMITER ;

FOR EACH ROW表示每条插入的流水都会触发一次。NEW是一个行引用,指向刚插入的这行数据,NEW.amount就是本次流水的金额,NEW.from_account就是付款账户。触发器里不需要也不能写COMMIT,它依附于触发它的那条INSERT所在的事务,INSERT成功提交,触发器里的UPDATE才会一并提交。

这时候再执行一条INSERT:

INSERT INTO transactions(from_account, to_account, amount) VALUES (1, 2, 200.00); SELECT * FROM accounts;

结果应该是张三800、李四700。如果还是1000和500,说明触发器没有建立成功,检查DELIMITER是否生效、是否有语法报错。报告里贴“插入前”和“插入后”两张accounts表截图,触发器部分就算完整了。

4.2 多个触发器的执行顺序和不同数据库的差异

一张表上可以建多个触发器,但执行顺序在不同数据库里差异很大。MySQL里多个相同事件的触发器按创建时间顺序执行,SQL Server里可以显式指定第一个和最后一个,Oracle里还支持FOLLOWS和PRECEDES来控制先后。实验报告不用深入到底层原理,但把这层差异写进“实验环境说明”里,能体现你不是只会复制代码。

数据库多触发器执行顺序修改触发器定义查询触发器
MySQL按创建顺序执行,建议合并事件不支持ALTER TRIGGER,需DROP后重建SHOW TRIGGERS / information_schema.TRIGGERS
SQL Server默认无保证,可用sp_settriggerorder指定首末支持ALTER TRIGGERsys.triggers
Oracle默认无保证,可用FOLLOWS / PRECEDES支持CREATE OR REPLACE TRIGGERDBA_TRIGGERS

MySQL没有ALTER TRIGGER,这是个很容易踩的坑。在实验5报告里修改触发器定义,不能像改表结构那样直接ALTER,只能先DROP再CREATE。还有一个关联坑:给表改名字,触发器不会跟着改名,MySQL里触发器引用的是表,表被重命名后触发器仍然指向旧表名,运行时会报错。做mysql数据库修改结构这类操作时,养成先查一遍SHOW TRIGGERS的习惯。

4.3 实验报告里触发器说明怎么写:一张表加三句话

这段是给报告用的,不是给代码用的。老师看报告时最想知道的是:这个触发器什么时候触发、因为什么触发、触发后干了什么。用表格把设计意图列出来,比大段文字清楚得多。

项目内容
触发器名称trg_tx_balance_ins
触发时机AFTER INSERT
触发事件transactions表插入流水
作用表accounts
NEW引用NEW.amount、NEW.from_account、NEW.to_account
实现逻辑流水产生后,自动改两个账户余额,一个减一个加

表格下面再补三句话:第一条,AFTER INSERT保证流水已经存在,如果触发器执行失败,整条INSERT会回滚;第二条,触发器的逻辑和存储过程独立,应用层只负责插流水,余额同步完全由数据库承担;第三条,验证方式是插入前后各查一次accounts表,对比余额变化。这三句话写进Word的doc里,配合前面的代码,整个触发器模块就完整了。

5. 实验报告5避坑指南:死锁、NOT FOUND和中文乱码

5.1 死锁和递归触发器让连接卡死

现象:在transactions上建了一个AFTER INSERT触发器,触发器内部又执行INSERT或UPDATE transactions,结果这条INSERT一执行,客户端直接卡住不动,过一会儿报错。

原因:触发器里再操作本表属于递归触发。MySQL会直接拒绝,报错“Can't update table 'transactions' in stored function/trigger because it is already used by statement which invoked this stored function/trigger”。如果用的不是MySQL而是别的数据库,递归不阻止时就会出现死锁,连接互相等待对方释放数据库并发锁,最后全部超时。

解决:触发器里不要操作触发语句所在的同一张表,只操作其他表。转账触发器只改accounts,绝不碰transactions。如果确实需要更新流水状态,改用BEFORE或AFTER UPDATE事件,或者把逻辑改成先INSERT再调用一个存储过程去修流水。排查时用SHOW PROCESSLIST查看被卡住的连接,找到对应线程后用KILL加线程ID强制结束,再检查触发器的FROM子句里是否有本表。

5.2 SELECT INTO查不到行时存储过程直接翻车

现象:存储过程里执行SELECT balance INTO v_bal FROM accounts WHERE id = xxx,当这个id不存在时,CALL过程直接报错“No data found”,而不是把变量置为NULL继续跑。

原因:普通SQL里SELECT查不到行只返回空结果,但存储过程中使用SELECT INTO给变量赋值时,查不到行会触发SQLSTATE 02000的NOT FOUND异常,存储过程默认认为这是出错,于是中断执行。

解决:过程里提前声明一个CONTINUE HANDLER,把“没查到”变成“给变量赋默认值”:

DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_bal = 0.00;

这条声明放在BEGIN之后、SELECT INTO之前。它的意思是:一旦出现NOT FOUND异常,不中断执行,而是把变量置为0,继续往下走。这样id不存在时返回余额0,而不是过程崩溃。另一个思路是先SELECT COUNT(*)确认行存在再赋值,多一次查询但逻辑更直白。两种做法任选其一,写进报告里都是“异常处理考虑”的体现。

5.3 中文数据写进去变成问号

现象:INSERT语句里带着中文,例如“王五”“测试用户”,执行成功但SELECT出来后显示“???”,甚至长度都对不上。

原因:字符集不一致。表的默认字符集是latin1,或者会话级的character_set_client是utf8mb4但表的列是latin1。中文在latin1里没有对应编码,写入时直接被替换成问号。最麻烦的是这种问题在插入时不报错,只能事后发现。

解决:三层都改。第一层建库时带上CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;第二层建表时再指定一次,防止库级默认值漏下来;第三层连接时执行SET NAMES utf8mb4,保证客户端发送的字符集与服务器一致。如果是JDBC连接,URL里再加characterEncoding=utf8参数。已经插入的乱码数据不要指望UPDATE修回来,因为编码已经丢了,直接DELETE掉重新插入。

5.4 CHECK约束“建了等于没建”

现象:在accounts表上添加了CHECK (balance >= 0),然后插入一条余额为负的记录,插入成功,约束没有拦截。

原因:MySQL 8.0.16之前版本对CHECK约束只解析不执行,这是历史遗留行为,很多人踩了坑还以为自己写错了语法。

解决:确认MySQL版本是8.0.16及以上,低于这个版本就改用触发器实现。靠BEFORE INSERT触发器检查NEW.balance,小于0时用SIGNAL SQLSTATE '45000'抛出一个自定义错误,效果和CHECK约束一样。更高一层是把余额检查写进应用层的Service里,数据库负责最终兜底,应用层负责快速反馈,这也是实验报告里可以写的“双层校验”设计。

6. 验证实验5结果:用系统表、SHOW和EXPLAIN自证清白

6.1 三条命令证明你的数据库对象真的生效

实验报告5.doc里最怕的是只有代码没有验证结果。运行完所有实验后,用下面三条语句把“数据库对象存在且可用”这件事坐实。

SHOW PROCEDURE STATUS WHERE Db = 'db_exp5'; SHOW TRIGGERS FROM db_exp5; EXPLAIN SELECT t.* FROM transactions t JOIN accounts a ON a.id = t.from_account WHERE a.id = 1;

第一条列出db_exp5库里所有存储过程,能看到sp_transfer的创建时间和修改时间,证明过程已经注册进数据库。第二条SHOW TRIGGERS展示触发器名称、触发事件和触发时机,直接把Timing、Event、Statement三列截图放进报告,比你手抄代码更可信。第三条EXPLAIN检查SQL的执行计划,确认外键关联走了索引,没有出现全表扫描,这是报告里体现“实验5不是随便跑通就行”的关键证据。

6.2 报告末尾的设计说明怎么写才不空

报告最后一段不要写“本次实验完成了基本操作”这种话,要写具体结论。给出一个可以直接套用的模板:本实验完成accounts和transactions两张表的设计,用存储过程sp_transfer实现了带事务的转账,用触发器trg_tx_balance_ins保证流水产生时余额自动变化。经调用验证,事务回滚正常,触发器插入前后余额正确,查询计划确认连接字段可以使用索引。这段写完,再附上前面三类截图的组合图,整份报告在“验证闭环”这一层就站稳了。

6.3 我给自己定的三条规矩

第一个习惯,所有存储过程和触发器对象名带版本号,比如sp_transfer_v1,改逻辑时先DROP再创建新版本,不会出现旧定义残留导致后续报错。第二个习惯,每次改表结构都单独保存一份迁移SQL,不直接在原文件上覆盖,数据库课程设计答辩时被问“这个字段后来为什么不在了”,翻迁移记录就知道原因。第三个习惯,凡是跑完一个实验,立刻把前后数据变化截成一张对比图,标上红圈,存到一个名为“验证截图”的文件夹里。这三个习惯帮我避开了实验5里至少一半的返工。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询