MySQL 存储过程与触发器完全指南

引言

在前面的 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 ;

游标使用四步DECLAREOPENFETCH(循环)→ 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 批量处理
游标 CURSOROPENFETCHCLOSE 逐行处理结果集
触发器 CREATE TRIGGER ... BEFORE/AFTER 审计日志、数据校验
NEW/OLD NEW.列 / OLD.列 获取变更前后的数据

二、一句话记忆

存储过程把 SQL 封装成数据库端的"函数"支持参数和变量,触发器绑定在表上自动响应增删改事件。存储过程用于封装逻辑减少网络开销,触发器用于审计日志和数据校验保证数据一致性。

相关推荐
睡不醒男孩0308236 分钟前
第七篇:揭秘 PostgreSQL 数据库内核级管控:CLup 深度架构设计与高可用底座技术白皮书
数据库·postgresql·clup
cmes_love44 分钟前
Level 2逐笔成交历史数据下载方法笔记
数据库·笔记·oracle
swordbob1 小时前
MySQL字符集陷阱:从Oracle迁移踩坑到utf8mb4强制规范
数据库·sql
牛油果子哥q1 小时前
【C++ STL string 】C++ STL string 终极精讲:底层原理、内存机制、全套API、深浅拷贝、易错坑点与工程实战规范
数据库·c++
十五年专注C++开发1 小时前
MySql中各种功能用sql语句实现总结
数据库·sql·mysql
数据库小学妹2 小时前
AI时代数据库怎么选?多模融合、数据统一存储与选型实战指南
数据库·人工智能·经验分享·ai
Albert Edison2 小时前
【Redis】Centos7.9 安装 Redis 5 教程
数据库·redis·缓存
云计算磊哥@2 小时前
运维开发宝典026-MySQL02数据库表操作
运维·数据库·运维开发
小二·2 小时前
Redis 内存溢出(OOM)排查与恢复实战
数据库·redis·bootstrap
pqk6V6Vep2 小时前
Redis 分布式锁进阶第一篇讲解
数据库·redis·分布式