引言
在前面的 MySQL 文章中,我们学习了 SQL 的基础操作、事务、索引、视图和多表查询。这些已经能覆盖大部分 CRUD 需求。但在实际项目中,还有两类重要的数据库编程技术需要掌握:
-
存储过程:把一组 SQL 语句封装成可重复调用的"函数",存储在数据库服务器端
-
触发器:在特定表上发生 INSERT/UPDATE/DELETE 时自动执行的代码
它们能让你把复杂的业务逻辑下沉到数据库层,减少网络开销,保证数据一致性。

第一部分:准备测试数据
sql
CREATE DATABASE IF NOT EXISTS school;
USE school;
-- 学生表
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL,
age INT DEFAULT 18,
total_score INT DEFAULT 0
);
-- 成绩日志表(触发器演示用)
CREATE TABLE score_log (
id INT PRIMARY KEY AUTO_INCREMENT,
student_id INT,
old_score INT,
new_score INT,
change_time DATETIME,
action VARCHAR(20)
);
INSERT INTO student (name, age) VALUES ('张三', 20), ('李四', 22), ('王五', 21);
第二部分:存储过程
一、什么是存储过程
存储过程是一组预编译的 SQL 语句集合,存储在数据库服务器上,通过一个名称即可调用。
类比:就像 C 语言的函数------封装一段逻辑,可以传参数,可以反复调用。
| 对比项 | 普通 SQL | 存储过程 |
|---|---|---|
| 执行方式 | 每次发送 SQL 文本,需要解析 | 预编译,一次编译多次执行 |
| 网络开销 | 每条 SQL 都要网络传输 | 只需传输调用命令 |
| 业务逻辑 | 放在应用层 | 可以放在数据库层 |
| 安全性 | 直接暴露表结构 | 可以只授权调用过程,隐藏表结构 |
二、创建与调用
sql
-- 修改分隔符(因为存储过程内部有分号)
DELIMITER $$
-- 最简单的存储过程
CREATE PROCEDURE show_all_students()
BEGIN
SELECT * FROM student;
END$$
DELIMITER ;
-- 调用
CALL show_all_students();
注意 :因为存储过程体内部有 ;,所以需要先用 DELIMITER $$ 把分隔符改成 $$,定义完再改回来。
三、带参数的存储过程
sql
DELIMITER $$
-- IN 参数:传入值
CREATE PROCEDURE get_student_by_id(IN stu_id INT)
BEGIN
SELECT * FROM student WHERE id = stu_id;
END$$
-- OUT 参数:传出值
CREATE PROCEDURE get_student_count(OUT cnt INT)
BEGIN
SELECT COUNT(*) INTO cnt FROM student;
END$$
-- INOUT 参数:传入并传出
CREATE PROCEDURE double_number(INOUT num INT)
BEGIN
SET num = num * 2;
END$$
DELIMITER ;
调用:
sql
-- IN 参数
CALL get_student_by_id(1);
-- OUT 参数(需要用变量接收)
CALL get_student_count(@count);
SELECT @count; -- 查看结果
-- INOUT 参数
SET @x = 10;
CALL double_number(@x);
SELECT @x; -- 20
| 参数类型 | 方向 | 说明 |
|---|---|---|
IN |
传入 | 调用者传值给过程,默认类型 |
OUT |
传出 | 过程将结果传回调用者 |
INOUT |
双向 | 既传入又传出 |
四、变量与赋值
sql
DELIMITER $$
CREATE PROCEDURE demo_variables()
BEGIN
-- 声明局部变量
DECLARE stu_name VARCHAR(20);
DECLARE stu_age INT DEFAULT 0;
-- 赋值方式1:SET
SET stu_age = 25;
-- 赋值方式2:SELECT ... INTO
SELECT name, age INTO stu_name, stu_age
FROM student
WHERE id = 1;
SELECT stu_name, stu_age;
END$$
DELIMITER ;
五、条件判断
sql
DELIMITER $$
CREATE PROCEDURE check_age(IN stu_id INT)
BEGIN
DECLARE stu_age INT;
SELECT age INTO stu_age FROM student WHERE id = stu_id;
IF stu_age < 18 THEN
SELECT '未成年';
ELSEIF stu_age < 22 THEN
SELECT '青年';
ELSE
SELECT '成年';
END IF;
END$$
DELIMITER ;
六、循环结构
sql
DELIMITER $$
-- WHILE 循环:批量插入学生
CREATE PROCEDURE insert_students(IN num INT)
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= num DO
INSERT INTO student (name, age)
VALUES (CONCAT('学生', i), 18 + (i % 5));
SET i = i + 1;
END WHILE;
SELECT CONCAT('插入完成,共', num, '条') AS result;
END$$
-- REPEAT 循环(至少执行一次)
CREATE PROCEDURE repeat_demo()
BEGIN
DECLARE i INT DEFAULT 1;
REPEAT
SELECT CONCAT('第', i, '次') AS msg;
SET i = i + 1;
UNTIL i > 3
END REPEAT;
END$$
-- LOOP 循环(需要 LEAVE 退出)
CREATE PROCEDURE loop_demo()
BEGIN
DECLARE i INT DEFAULT 1;
my_loop: LOOP
IF i > 3 THEN
LEAVE my_loop;
END IF;
SELECT CONCAT('循环第', i, '次') AS msg;
SET i = i + 1;
END LOOP my_loop;
END$$
DELIMITER ;
| 循环类型 | 语法 | 特点 |
|---|---|---|
WHILE |
WHILE 条件 DO ... END WHILE |
先判断再执行 |
REPEAT |
REPEAT ... UNTIL 条件 END REPEAT |
至少执行一次 |
LOOP |
loop_name: LOOP ... END LOOP |
无限循环,需 LEAVE 退出 |
七、游标(逐行处理结果集)
sql
DELIMITER $$
CREATE PROCEDURE list_student_names()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE stu_name VARCHAR(20);
-- 声明游标
DECLARE cur CURSOR FOR SELECT name FROM student;
-- 声明结束处理(NOT FOUND 时设置 done=1)
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN cur;
read_loop: LOOP
FETCH cur INTO stu_name;
IF done THEN
LEAVE read_loop;
END IF;
SELECT stu_name;
END LOOP;
CLOSE cur;
END$$
DELIMITER ;
游标使用四步 :DECLARE → OPEN → FETCH(循环)→ CLOSE
八、查看与删除存储过程
sql
-- 查看所有存储过程
SHOW PROCEDURE STATUS WHERE Db = 'school';
-- 查看创建语句
SHOW CREATE PROCEDURE get_student_by_id;
-- 删除
DROP PROCEDURE IF EXISTS get_student_by_id;
第三部分:触发器
一、什么是触发器
触发器是一种特殊的存储过程 ,它不能手动调用 ,而是在特定表上发生 INSERT、UPDATE 或 DELETE 操作时自动触发执行。

二、创建触发器
sql
DELIMITER $$
-- 记录 student 表的修改日志
CREATE TRIGGER log_score_update
AFTER UPDATE ON student -- 在 student 更新之后触发
FOR EACH ROW -- 每行都触发一次
BEGIN
-- NEW:更新后的行数据
-- OLD:更新前的行数据
INSERT INTO score_log (student_id, old_score, new_score, change_time, action)
VALUES (NEW.id, OLD.total_score, NEW.total_score, NOW(), 'UPDATE');
END$$
DELIMITER ;
测试:
sql
-- 更新学生成绩
UPDATE student SET total_score = 90 WHERE id = 1;
UPDATE student SET total_score = 85 WHERE id = 2;
-- 查看日志
SELECT * FROM score_log;
结果:

三、NEW 和 OLD
| 触发事件 | NEW |
OLD |
|---|---|---|
| INSERT | 新插入的行 | 无(全是 NULL) |
| UPDATE | 更新后的行 | 更新前的行 |
| DELETE | 无(全是 NULL) | 被删除的行 |
sql
-- INSERT 触发器:新学生自动记录
CREATE TRIGGER log_student_insert
AFTER INSERT ON student
FOR EACH ROW
BEGIN
INSERT INTO score_log (student_id, old_score, new_score, change_time, action)
VALUES (NEW.id, 0, NEW.total_score, NOW(), 'INSERT');
END$$
-- DELETE 触发器:删除学生时记录
CREATE TRIGGER log_student_delete
BEFORE DELETE ON student -- BEFORE:在删除前可以获取 OLD 数据
FOR EACH ROW
BEGIN
INSERT INTO score_log (student_id, old_score, new_score, change_time, action)
VALUES (OLD.id, OLD.total_score, 0, NOW(), 'DELETE');
END$$
四、触发器的应用场景
| 场景 | 触发器类型 | 说明 |
|---|---|---|
| 审计日志 | AFTER INSERT/UPDATE/DELETE | 记录谁在什么时候改了什么 |
| 数据校验 | BEFORE INSERT/UPDATE | 检查数据合法性,不合法则拒绝 |
| 自动计算 | BEFORE INSERT/UPDATE | 自动填充计算字段(如总价=单价×数量) |
| 级联操作 | AFTER DELETE | 删主表时自动删从表 |
| 数据同步 | AFTER INSERT/UPDATE/DELETE | 一张表变化时同步更新另一张表 |
数据校验示例:
sql
DELIMITER $$
CREATE TRIGGER check_age_before_insert
BEFORE INSERT ON student
FOR EACH ROW
BEGIN
IF NEW.age < 0 OR NEW.age > 150 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '年龄必须在 0~150 之间';
END IF;
END$$
DELIMITER ;
-- 测试非法数据
INSERT INTO student (name, age) VALUES ('测试', -1);
-- ERROR:年龄必须在 0~150 之间
五、查看与删除触发器
sql
-- 查看所有触发器
SHOW TRIGGERS;
-- 查看特定表的触发器
SHOW TRIGGERS LIKE 'student';
-- 删除
DROP TRIGGER IF EXISTS log_score_update;
第四部分:存储过程 vs 触发器 vs 函数
| 对比项 | 存储过程 | 触发器 | 存储函数 |
|---|---|---|---|
| 调用方式 | CALL proc() |
自动触发 | SELECT func() |
| 参数 | 支持 IN/OUT/INOUT | 不支持 | 只支持 IN |
| 返回值 | 可通过 OUT 参数 | 无 | 必须有返回值 |
| 事务控制 | 可以 COMMIT/ROLLBACK | 不可以 | 不可以 |
| 使用场景 | 封装业务逻辑 | 审计/校验/同步 | 计算并返回结果 |
第五部分:C 语言调用存储过程
cpp
#include <stdio.h>
#include <stdlib.h>
#include <mysql/mysql.h>
int main() {
MYSQL *conn = mysql_init(NULL);
if (conn == NULL) {
fprintf(stderr, "mysql_init 失败\n");
return -1;
}
if (mysql_real_connect(conn, "127.0.0.1", "root", "123456",
"school", 3306, NULL, 0) == NULL) {
fprintf(stderr, "连接失败: %s\n", mysql_error(conn));
mysql_close(conn);
return -1;
}
// 调用存储过程(和普通 SQL 一样,只是用 CALL)
if (mysql_query(conn, "CALL get_student_by_id(1)") != 0) {
fprintf(stderr, "调用失败: %s\n", mysql_error(conn));
} else {
MYSQL_RES *result = mysql_store_result(conn);
if (result) {
MYSQL_ROW row;
while ((row = mysql_fetch_row(result))) {
printf("ID: %s, Name: %s, Age: %s\n",
row[0], row[1], row[2]);
}
mysql_free_result(result);
}
}
mysql_close(conn);
return 0;
}
总结
一、核心要点
| 主题 | 关键语法 | 使用场景 |
|---|---|---|
| 存储过程 | CREATE PROCEDURE ... BEGIN ... END |
封装业务逻辑、批量操作 |
| 参数 | IN / OUT / INOUT |
传参和返回值 |
| 变量 | DECLARE / SET / SELECT INTO |
临时存储数据 |
| 循环 | WHILE / REPEAT / LOOP |
批量处理 |
| 游标 | CURSOR → OPEN → FETCH → CLOSE |
逐行处理结果集 |
| 触发器 | CREATE TRIGGER ... BEFORE/AFTER |
审计日志、数据校验 |
| NEW/OLD | NEW.列 / OLD.列 |
获取变更前后的数据 |
二、一句话记忆
存储过程把 SQL 封装成数据库端的"函数"支持参数和变量,触发器绑定在表上自动响应增删改事件。存储过程用于封装逻辑减少网络开销,触发器用于审计日志和数据校验保证数据一致性。