存储过程与触发器

目录

前言

一、前置内容

二、存储过程

2.1创建存储过程

2.2如何调用存储过程

2.3存储过程的查看与删除

三、变量

3.1系统变量

3.2用户自定义变量

3.3局部变量

四、SQL编程

4.1 if else分支控制

4.2 case流程控制

4.3参数

4.4循环

五、游标

5.1条件处理程序

5.2存储函数

六、触发器

七、为什么要有存储过程

7.1为什么需要存储过程?

1. 减少网络传输开销,提升性能

2. 利用数据库的原生能力,简化复杂逻辑

3. 集中化管理,降低维护成本

7.2为什么存储过程属于服务端?

1. 客户端逻辑无法高效利用数据库资源

2. 客户端逻辑会放大网络依赖

3. 安全性无法保障

4. 事务一致性难以保证

7.3总结:存储过程的“服务端定位”是权衡后的最优解


前言

        存储过程是数据库领域中实现功能复用与效率优化的重要工具,它本质上是一组为完成特定功能而编写的 SQL 语句集合。与普通 SQL 语句不同,存储过程经编译后会持久化存储在数据库中,用户无需重复编写复杂 SQL,仅通过指定存储过程名称和传入对应参数即可调用执行,并获取预设的结果。

        其核心特性可概括为四点:一是作为可复用的 SQL 语句集合,聚焦特定功能实现;二是编译后存储,减少重复编译开销;三是通过名称与参数快速调用,简化操作;四是执行后按需返回结果,满足数据交互需求。下文将围绕存储过程的相关内容展开详细探讨。

        在开始前,建议读者下载数据库GUI可视化软件Navicat,这有助于减少与本文无关的操作成本


一、前置内容

        这里作者将本文使用的demo.sql文件粘贴如下,读者可以自行导入数据库中结合文章内容进行联系。



SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;


-- ----------------------------
-- Table structure for class
-- ----------------------------
DROP TABLE IF EXISTS `class`;
CREATE TABLE `class`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of class
-- ----------------------------
INSERT INTO `class` VALUES (1, 'Java001班');
INSERT INTO `class` VALUES (2, 'C++001班');
INSERT INTO `class` VALUES (3, '前端001班');

-- ----------------------------
-- Table structure for course
-- ----------------------------
DROP TABLE IF EXISTS `course`;
CREATE TABLE `course`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of course
-- ----------------------------
INSERT INTO `course` VALUES (1, 'Java');
INSERT INTO `course` VALUES (2, 'C++');
INSERT INTO `course` VALUES (3, 'MySQL');
INSERT INTO `course` VALUES (4, '操作系统');
INSERT INTO `course` VALUES (5, '计算机网络');
INSERT INTO `course` VALUES (6, '数据结构');

-- ----------------------------
-- Table structure for emp
-- ----------------------------
DROP TABLE IF EXISTS `emp`;
CREATE TABLE `emp`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `role` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `salary` decimal(10, 2) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of emp
-- ----------------------------
INSERT INTO `emp` VALUES (1, '马云', '老板', 1500000.00);
INSERT INTO `emp` VALUES (2, '马化腾', '老板', 1800000.00);
INSERT INTO `emp` VALUES (3, '鑫哥', '讲师', 10000.00);
INSERT INTO `emp` VALUES (4, '博哥', '讲师', 12000.00);
INSERT INTO `emp` VALUES (5, '平姐', '学管', 9000.00);
INSERT INTO `emp` VALUES (6, '莹姐', '学管', 8000.00);
INSERT INTO `emp` VALUES (7, '孙悟空', '游戏角色', 956.80);
INSERT INTO `emp` VALUES (8, '猪悟能', '游戏角色', 700.50);
INSERT INTO `emp` VALUES (9, '沙和尚', '游戏角色', 333.30);

-- ----------------------------
-- Table structure for exam
-- ----------------------------
DROP TABLE IF EXISTS `exam`;
CREATE TABLE `exam`  (
  `id` bigint NULL DEFAULT NULL,
  `name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `chinese` decimal(4, 1) NULL DEFAULT NULL,
  `math` decimal(4, 1) NULL DEFAULT NULL,
  `english` decimal(4, 1) NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of exam
-- ----------------------------
INSERT INTO `exam` VALUES (1, '唐三藏', 134.0, 98.0, 66.0);
INSERT INTO `exam` VALUES (3, '猪悟能', 176.0, 98.0, 90.0);
INSERT INTO `exam` VALUES (4, '曹孟德', 140.0, 90.0, 67.0);
INSERT INTO `exam` VALUES (5, '刘玄德', 111.0, 145.0, 45.0);
INSERT INTO `exam` VALUES (6, '孙权', 140.0, 73.0, 78.5);
INSERT INTO `exam` VALUES (7, '宋公明', 150.0, 125.0, 30.0);
INSERT INTO `exam` VALUES (8, '张飞', 54.0, 128.0, NULL);

-- ----------------------------
-- Table structure for score
-- ----------------------------
DROP TABLE IF EXISTS `score`;
CREATE TABLE `score`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `score` float NULL DEFAULT NULL,
  `student_id` bigint NULL DEFAULT NULL,
  `course_id` bigint NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 24 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of score
-- ----------------------------
INSERT INTO `score` VALUES (1, 80, 1, 1);
INSERT INTO `score` VALUES (2, 98.5, 1, 3);
INSERT INTO `score` VALUES (3, 33, 1, 5);
INSERT INTO `score` VALUES (4, 98, 1, 6);
INSERT INTO `score` VALUES (5, 60, 2, 1);
INSERT INTO `score` VALUES (6, 59.5, 2, 5);
INSERT INTO `score` VALUES (7, 33, 3, 1);
INSERT INTO `score` VALUES (8, 68, 3, 3);
INSERT INTO `score` VALUES (9, 99, 3, 5);
INSERT INTO `score` VALUES (10, 67, 4, 1);
INSERT INTO `score` VALUES (11, 23, 4, 3);
INSERT INTO `score` VALUES (12, 56, 4, 5);
INSERT INTO `score` VALUES (13, 72, 4, 6);
INSERT INTO `score` VALUES (14, 81, 5, 1);
INSERT INTO `score` VALUES (15, 37, 5, 5);
INSERT INTO `score` VALUES (16, 56, 6, 2);
INSERT INTO `score` VALUES (17, 43, 6, 4);
INSERT INTO `score` VALUES (18, 79, 6, 6);
INSERT INTO `score` VALUES (19, 80, 7, 2);
INSERT INTO `score` VALUES (20, 92, 7, 6);
INSERT INTO `score` VALUES (21, 80, 1, 1);
INSERT INTO `score` VALUES (22, 98.5, 1, 3);
INSERT INTO `score` VALUES (23, 60, 2, 1);

-- ----------------------------
-- Table structure for student
-- ----------------------------
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `sno` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `age` int NULL DEFAULT 18,
  `gender` tinyint(1) NULL DEFAULT NULL,
  `enroll_date` date NULL DEFAULT NULL,
  `class_id` bigint NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `class_id`(`class_id` ASC) USING BTREE,
  CONSTRAINT `student_ibfk_1` FOREIGN KEY (`class_id`) REFERENCES `class` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES (1, '唐三藏', '100001', 18, 1, '1986-09-01', 1);
INSERT INTO `student` VALUES (2, '孙悟空', '100002', 18, 1, '1986-09-01', 1);
INSERT INTO `student` VALUES (3, '猪悟能', '100003', 18, 1, '1986-09-01', 1);
INSERT INTO `student` VALUES (4, '沙悟净', '100004', 18, 1, '1986-09-01', 1);
INSERT INTO `student` VALUES (5, '宋江', '200001', 18, 1, '2000-09-01', 2);
INSERT INTO `student` VALUES (6, '武松', '200002', 18, 1, '2000-09-01', 2);
INSERT INTO `student` VALUES (7, '李逹', '200003', 18, 1, '2000-09-01', 2);
INSERT INTO `student` VALUES (8, '不想毕业', '200004', 18, 1, '2000-09-01', 2);

SET FOREIGN_KEY_CHECKS = 1;

先打开下载好的Navicat并连接到你的数据库后,的页面如下,注意新下载的MySQL数据库中可能有一些默认的数据库或者没有任何数据库,数据库列表可能会与作者不同,但是这并不影响

图1.1        Navicat页面

        右键你的MySQL连接,新建一个数据名字命名为TestDataBase,而后不用配置其他项直接按照默认配置即可,点击确定。

图1.2        新建一个数据库
图1.3        新建数据库配置

        新建后的数据库名字会自动将所有字母全都转换为小写。而后双击新建好的数据库,当数据库名字左侧的数据库图标变为绿色表示选中,而后点击执行新建查询。将作者提供的SQL语句复制粘贴到对应位置,点击执行。

图1.4        新建查询
图1.5        执行SQL语句
图1.6        查看执行结果
图1.7        查看新建的数据表

        至此准备工作完毕,准备开始讲解存储过程。

二、存储过程

2.1创建存储过程

-- 修改SQL语句结束标识符为 //
DELIMITER //

-- 创建存储过程
CREATE PROCEDURE 存储过程名 (参数列表)
BEGIN
-- SQL 语句
END //
-- 修改SQL语句结束标识符为 ;
DELIMITER ;

我们这里来创建一个Hello world存储过程,当调用这个存储过程时,输出hello world。

图2.1.1        创建一个存储过程
图2.1.2        创建helloworld存储过程
图2.1.3        在结果栏中查看结果

        这里需要解释一下,为什么在语法介绍的时候说需要使用delimiter定义分隔符,这是因为我们的SQL语句默认以分号进行结果,而存储过程也是以分号进行结果,这个时候SQL语句的结尾分号可能意外触发存储过程结束,所以我们需要重新定义一个存储过程结束符。

        那为什么在Navicat中没有使用delimiter重新定义分隔符程序还能正常运行,这是因为Navaicat会自动为我们天机分隔符,但是如果在其他平台上,可能帮我们自动处理这个潜在的问题,进而导致错误。

2.2如何调用存储过程

        我们只需要在当前数据库下新建一个查询,在新建查询中使用call + 存储过程名的方式进行调用。

图2.2.1        新建查询调用helloworld存储过程

2.3存储过程的查看与删除

        对于存储过程的查询和删除,你可以通过Navicat进行操作,这里就不过多介绍了;你也可以通过SQL查询的方式来进行存储过程的查询与删除。

图2.3.1        查询testdatabase数据库中的存储过程
图2.3.2        删除testdatabase数据库中的存储过程

三、变量

        在MySQL中存在系统变量、用户自定义变量、局部变量。接下来依次介绍这三种变量。

3.1系统变量

        系统变量管理着MySQL的相关配置。系统变量分为全局变量和会话变量。以下是相关的一些操作,读者可自行尝试。

        另外需要一提的是,你可以使用global和session分别指代全局变量和会话变量。也可以使用@@global.系统变量名、@@session.系统变量名的方式来访问指定的系统变量。

        @@表示的是系统变量的前缀。

#查询所有全局变量
SHOW GLOBAL VARIABLES;
#查询所有会话变量
SHOW SESSION VARIABLES;

#模糊查询包含commit字符的全局变量
SHOW GLOBAL VARIABLES like "%commit%"
#精确查询admin_port全局变量的值
SELECT @@global.admin_port

#设置一个autocommit全局变量的值为false
set GLOBAL autocommit = FALSE
#查询autocommit全局变量更改后的值
show GLOBAL VARIABLES like "autocommit"

3.2用户自定义变量

        用户自定义变量通常是作为SQL程序的中间值使用的,当会话销毁的时候,用户自定义变量也会全部销毁。

#设置用户自定义变量
set @name := "lilei"
#查找变量名为name的用户自定义变量
SELECT @name

#将class表中id为1的班级名赋值给用户自定义变量class
SELECT name into @class from class WHERE id = 1
#查找变量名为class的用户自定义变量
SELECT @class

3.3局部变量

        局部变量使用前需要先声明且只能用在begin...end块中,变量类型可以是任何有效的MySQL数据类型。

CREATE DEFINER=CURRENT_USER PROCEDURE checkstudentage () 
BEGIN 
  DECLARE student_age INT DEFAULT(18);    #声明一个局部变量
  SELECT student_age;                     #查询值
  SET student_age :=student_age+10;       #重新赋值
  SELECT student_age;                     #再次查询
END

CALL checkstudentage ();
图3.3.1        程序运行结果

四、SQL编程

        这里主要介绍MySQL提供的流程控制语法。

图4.1        快速生成流程控制代码

4.1 if else分支控制

        写一个示例,这里使用我们用一个局部变量定义一个score变量表示分数,给定一个默认值90,使用if else分支控制,如果大于等于60分输出及格,小于60分输出不及格。

CREATE DEFINER = CURRENT_USER PROCEDURE process_control()
BEGIN
    DECLARE score int DEFAULT(90);
    IF score>=60 THEN
      SELECT "及格";
    ELSE
      SELECT "不及格";
    END IF;
END

CALL process_control();

4.2 case流程控制

        在将4.1中所提示的示例复杂一点。丰富成绩的分层。

CREATE DEFINER=`root`@`localhost` PROCEDURE `switch_control`()
BEGIN
    DECLARE score int DEFAULT(90);
    CASE 
    WHEN score>=90 THEN
      SELECT "优秀";
    WHEN score>=80 THEN
      SELECT "良好";
    WHEN score>=70 THEN
      SELECT "中等";
    WHEN score>=60 THEN
      SELECT "及格";
    ELSE
      SELECT "不及格";
    END CASE;
END

        使用示例代码所示的case语法可以才可以让分支进行表达式判断,如果是如下代码,只能进行等值分支判断。

CASE case_value
	WHEN when_value THEN
		statement_list
	ELSE
		statement_list
END CASE;

4.3参数

        我们可以向存储过程中设置参数,对于输入参数我们IN、OUT、INOUT来分别表示该参数是输入参数、输出参数、输入输出参数。

        对于4.2示例我们不再使用定义好的变量而是使用输入参数。

CREATE DEFINER=`root`@`localhost` PROCEDURE `switch_control`(in score int)
BEGIN
    CASE 
    WHEN score>=90 THEN
      SELECT "优秀";
    WHEN score>=80 THEN
      SELECT "良好";
    WHEN score>=70 THEN
      SELECT "中等";
    WHEN score>=60 THEN
      SELECT "及格";
    ELSE
      SELECT "不及格";
    END CASE;
END
图4.3.1        运行结果

4.4循环

        MySQL中提供了三种循环,这三种循环的差异如下表。总得来说,while循环类似于C语言中的while循环,但是不能通过break或continue控制循环终止或跳过,repeat类似于C语言中的do...while循环,但是也不能通过break或continue控制循环终止或跳过,loop相比于while则是没有执行条件,即一定会进入循环,但是需要使用leave和iterate来控制循环终止或跳过。

循环类型执行时机终止条件位置核心优势典型应用场景
WHILE先判断,后执行循环体之前逻辑直观,避免无效执行结果集遍历、条件触发执行
REPEAT先执行,后判断循环体之后确保首次执行,代码简洁输入验证、初始化后校验
LOOP无条件执行循环体内部控制灵活,支持多条件复杂逻辑终止、中途跳出循环

        关于这部分代码,暂且不在这里演示,在“游标”讲解部分将结合游标使用循环。

五、游标

        由于MySQL中没有数组这种数据类型,这就导致了当我们相对输出的结果集进行逐个操作时变得十分麻烦,游标是一种数据库对象,它可以与结果集像绑定,做到逐行检索的效果。

        使用游标时,需按以下顺序操作:首先必须声明游标,之后通过OPEN语句打开游标、用FETCH语句获取游标记录、用CLOSE语句关闭游标。 需要注意的是,游标必须在条件处理程序之前声明,而变量则必须在游标或条件处理程序之前声明。

        现在使用while循环+游标的方式来逐行获取一下student表中的数据。

-- 声明游标
DECLARE 游标名 CURSOR FOR 查询语句;

-- 打开游标
OPEN 游标名;

-- 获取游标记录
FETCH 游标名 INTO 变量[, 变量] ...;

-- 关闭游标
CLOSE 游标名;
DROP PROCEDURE loop_control    

CREATE DEFINER = CURRENT_USER PROCEDURE loop_control()
BEGIN
  DECLARE totalNum INT;
  DECLARE idx int DEFAULT(0);
  DECLARE all_person VARCHAR(255);
  DECLARE person VARCHAR(255);                                
  DECLARE s_cursor CURSOR for SELECT name from student;    #定义游标所获取的结果集
  
  SELECT count(*) into totalNum from student;              #获取结果集中元素总数   
  
  
  OPEN s_cursor;                                           #打开游标
  WHILE idx<totalNum DO
    FETCH s_cursor into person;                            #将游标的值复制到变量中
    set all_person := CONCAT_WS(',',all_person,person);    #将当前行的学生名尾插到all_person
    set idx := idx+1;                                      #自增计数  
  END WHILE;
  
  CLOSE s_cursor;                                          #关闭游标
  SELECT all_person as resault;                            #查看结果
END

CALL loop_control();
图5.1        执行结果

        需要注意的是,当前的程序还是有bug的,因为在本例中all_person最多只能容纳255个字符,所以如果你的表中有很多的数据,最后可能发生截断。        

5.1条件处理程序

        写代码就难免出现意想不到的bug,所以MySQL中也为我们提供了处理错误的钩子,当错误发生时给程序员一个可以自行处理的机会。

DECLARE handler_action HANDLER
FOR condition_value [, condition_value] ...
statement

handler_action: {
CONTINUE -- 继续执⾏当前程序
| EXIT -- 终⽌执⾏当前程序
}

condition_value: {
mysql_error_code -- MYSQL错误码
| SQLSTATE [VALUE] sqlstate_value -- 状态码
| SQLWARNING -- 所有以01开头的SQLSTATE代码
| NOT FOUND -- 所有以02开头的SQLSTATE代码
| SQLEXCEPTION -- 所有没有被SQLWARNING或NOT FOUND捕获的SQLSTATE代码
}

        比如,对于图5.1所示结果的示例代码,如果我们不指定游标的结束位置,那么游标就一定会越界,越界就会导致我们的程序崩溃,所以我们可以再更改一下图5.1对应的代码示例,让游标出错,而后我们手动处理这个错误。当游标越界的时候会报not found这个错误,所以我们直接声明一个条件处理程序来处理这个错误即可。

DROP PROCEDURE loop_control

CREATE DEFINER = CURRENT_USER PROCEDURE loop_control()
BEGIN
  DECLARE totalNum INT;
  DECLARE all_person VARCHAR(255);
  DECLARE person VARCHAR(255);
  DECLARE jumpout TINYINT(1) DEFAULT(TRUE);
  
  DECLARE s_cursor CURSOR for SELECT name from student;
  DECLARE CONTINUE HANDLER FOR not found set jumpout:=FALSE    #可以将此行去除,查看报错
  SELECT count(*) into totalNum from student; 
  
  OPEN s_cursor;
  WHILE jumpout DO
    FETCH s_cursor into person;
    set all_person := CONCAT_WS(',',all_person,person);
    
  END WHILE;
  
  CLOSE s_cursor;
  SELECT all_person as resault;
END

CALL loop_control();

5.2存储函数

        存储函数和存储过程各有优劣,总结如下表。这里就不进行额外的代码演示了,感兴趣的读者可以查看官网获取实例或者让AI帮助你完成一个示例。

维度存储过程(Stored Procedure)函数(Function)
返回值无固定返回值,可通过 OUT 参数返回多个值必须有且仅有一个返回值(RETURNS 定义)
调用方式只能用 CALL 语句单独调用可嵌入 SELECT 等 SQL 语句中使用
参数类型支持 IN/OUT/INOUT仅支持 IN 类型
业务逻辑复杂度适合复杂逻辑(多表操作、事务、流程控制)适合单一计算或转换逻辑
数据修改权限可执行 INSERT、UPDATEDELETE 等操作不建议执行数据修改操作

六、触发器

        所谓的触发器完成的功能实际上与我们的条件处理程序类似,都是给了我们当某个条件成立时,让程序员接入的机会,比如面向对象语言中的构造函数就是给了我们程序在对象创建时介入的机会。在比如前端的钩子函数等等都是这个意味。

触发器类型OLD 关键字含义NEW 关键字含义
INSERT无(插入操作无原始数据,不可用)表示将要新增或已经新增的数据
UPDATE表示修改之前的原始数据表示将要修改或已经修改后的数据
DELETE表示将要删除或已经删除的数据无(删除操作无新数据,不可用)

        对于MySQL也是如此,当用户使用MySQL进行插入、更新、删除的时候,都给程序员一个介入的机会。需要说明的是MySQL提供的是行级触发器,也就是说如果对多行数据进行删改,会对每一行数据执行操作。

CREATE TRIGGER [IF NOT EXISTS] trigger_name
trigger_time trigger_event
ON tbl_name FOR EACH ROW
BEGIN
trigger_stmt;
END;

trigger_time: { BEFORE | AFTER }

trigger_event: { INSERT | UPDATE | DELETE }

-- 查看
SHOW TRIGGERS;

-- 删除, 如果没有指定schema_name,默认为当前数据库
DROP TRIGGER [IF EXISTS] [schema_name.]trigger_name;

        这里还是以student表来进行演示,本文就以一个更新触发器示例来演示触发器的功能,其他触发器可由读者自行尝试。

create table student_log (
id bigint primary key auto_increment,
operation_type varchar(10) not null comment '操作类型:
insert/update/delete',
operation_time datetime not null comment '操作时间',
operation_id bigint not null comment '操作的记录ID',
operation_data varchar(500) comment '操作数据'
);


delimiter //
create trigger trg_student_update after update on student for each row
begin
-- 插⼊新增数据⽇志
insert into student_log (operation_type,operation_time,operation_id,operation_data)
values ('update',now(),new.id,concat(old.id, ',', old.name, ',', old.sno,',', old.age, ',',old.gender, ',', old.enroll_date, ',', old.class_id,'|', new.id, ',', new.name, ',', new.sno,',', new.age, ',',new.gender, ',', new.enroll_date, ',', new.class_id));
end // 
delimiter ;


-- 更新学⽣表中记录
update student set age = 20, class_id = 2 where name = '唐三藏';

-- 查看学⽣⽇志表
select * from student_log;
-- 2 | update | 2024-09-19 11:47:21 | 13 | 13,曹操,300001,28,1,2024-09-01,3|13,曹操,300001,20,1,2024-09-01,2

-- 更新多条记录
update student set class_id = 3 where id >= 7;

SHOW TRIGGERS;
DROP TRIGGER trg_student_update;
SHOW PROCESSLIST;
为什么这段代码执行后,执行更新操作,日志表没有变换

        注意,在更新触发器中不应该对触发器监控的表进行更新操作,在插入触发器中不应该对插入触发器监控的表进行插入操作,在删除触发器中不应该对删除触发器所监控的表进行删除操作。因为这样会导致触发器无限循环,进而导致数据库卡死。

图6.1        执行结果

七、为什么要有存储过程

        存储过程之所以存在,以及在架构中被归类为服务端组件,核心原因是它能通过“将数据逻辑贴近数据存储”来平衡性能、安全性和维护性,而将逻辑放在客户端反而会带来更多问题。具体可以从以下几个角度理解:

7.1为什么需要存储过程?

核心价值是“数据逻辑与数据的近距离协作”

1. 减少网络传输开销,提升性能

若数据逻辑放在客户端,客户端需要先查询原始数据(可能是大量行),在本地处理后再写回数据库,这会产生频繁的网络交互和大量数据传输。

例如:计算一张100万行订单表的月度销售额,客户端逻辑需要先把100万行数据拉到本地求和,再将结果传回数据库;而存储过程在数据库内部直接计算,仅传输最终结果(一个数字),网络开销降低几个数量级。

2. 利用数据库的原生能力,简化复杂逻辑

数据库对事务、锁、索引的优化是客户端逻辑难以替代的。存储过程可以直接使用 `BEGIN TRANSACTION`、`COMMIT` 等原生语法控制事务,避免客户端因网络延迟导致的事务一致性问题;同时,数据库对 `JOIN`、`GROUP BY` 等操作的优化(如利用索引),比客户端在内存中处理更高效。

3. 集中化管理,降低维护成本

若数据逻辑分散在多个客户端(如Web端、APP端、第三方系统),一旦逻辑需要修改(如计税规则调整),所有客户端都要同步更新,极易出现版本不一致。而存储过程集中在数据库中,修改后所有客户端立即生效,无需协调多端变更。

7.2为什么存储过程属于服务端?

客户端放置逻辑的隐患 将数据逻辑放在客户端,看似能“减轻服务端压力”,但实际会引发更严重的问题,反而增加整体系统的负担:

1. 客户端逻辑无法高效利用数据库资源

数据库的核心优势是“处理结构化数据的并行计算能力”(如多线程执行查询、利用磁盘缓存)。客户端逻辑本质是“单进程/单线程处理从数据库拉取的原始数据”,面对大量数据时,客户端的CPU、内存会成为瓶颈,反而比数据库内部处理更慢。

例如:对100万行数据做分组统计,数据库可以通过索引和并行计算在1秒内完成,而客户端拉取100万行数据可能需要10秒,本地计算再花5秒,总耗时增加15倍。

2. 客户端逻辑会放大网络依赖

客户端与数据库的网络连接可能不稳定(如分布式系统中的跨机房调用),数据逻辑放在客户端会导致“一次逻辑处理需要多次网络往返”(查询→计算→更新),任何一次网络波动都会导致整个逻辑失败。而存储过程在数据库内部执行,网络仅需一次调用(`CALL 存储过程`),稳定性显著提升。

3. 安全性无法保障

客户端逻辑需要直接操作数据库表(如 `INSERT`、`UPDATE`),意味着必须向客户端暴露表结构和高权限账号,存在数据泄露或误操作风险(如客户端代码漏洞导致全表删除)。而存储过程可以通过“封装表访问”,只允许客户端调用存储过程,不直接暴露表结构,权限控制更严格(例如:只授予 `EXECUTE` 存储过程的权限,不授予表的 `DELETE` 权限)。

4. 事务一致性难以保证

涉及多表操作的事务(如“下单→扣库存→减余额”),若逻辑在客户端,需要客户端依次发送多个SQL语句,期间若客户端崩溃或网络中断,会导致事务部分执行,数据不一致。而存储过程可以在数据库内部用 `BEGIN...COMMIT` 包裹整个事务,确保原子性,不受客户端状态影响。

7.3总结:存储过程的“服务端定位”是权衡后的最优解

存储过程的设计本质是让“数据逻辑”尽可能靠近“数据存储”,利用数据库的原生能力(性能、事务、权限)解决客户端逻辑的固有缺陷(网络开销、安全性、一致性)。 “减少服务端压力”的说法只看到了表面——数据库的计算压力确实存在,但通过存储过程将逻辑集中处理,整体系统的性能、稳定性、可维护性会得到极大提升,这比单纯“转移压力到客户端”更有价值。 当然,存储过程也有局限性(如移植性差、调试困难),因此现代架构中通常“适度使用”(仅处理核心数据逻辑),而非滥用。但这并不改变其“服务端组件”的定位——它的价值正是通过服务端的本地化执行来实现的。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木鱼不是木鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值