目录
前言
存储过程是数据库领域中实现功能复用与效率优化的重要工具,它本质上是一组为完成特定功能而编写的 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数据库中可能有一些默认的数据库或者没有任何数据库,数据库列表可能会与作者不同,但是这并不影响。
右键你的MySQL连接,新建一个数据名字命名为TestDataBase,而后不用配置其他项直接按照默认配置即可,点击确定。
新建后的数据库名字会自动将所有字母全都转换为小写。而后双击新建好的数据库,当数据库名字左侧的数据库图标变为绿色表示选中,而后点击执行新建查询。将作者提供的SQL语句复制粘贴到对应位置,点击执行。
至此准备工作完毕,准备开始讲解存储过程。
二、存储过程
2.1创建存储过程
-- 修改SQL语句结束标识符为 //
DELIMITER //
-- 创建存储过程
CREATE PROCEDURE 存储过程名 (参数列表)
BEGIN
-- SQL 语句
END //
-- 修改SQL语句结束标识符为 ;
DELIMITER ;
我们这里来创建一个Hello world存储过程,当调用这个存储过程时,输出hello world。
这里需要解释一下,为什么在语法介绍的时候说需要使用delimiter定义分隔符,这是因为我们的SQL语句默认以分号进行结果,而存储过程也是以分号进行结果,这个时候SQL语句的结尾分号可能意外触发存储过程结束,所以我们需要重新定义一个存储过程结束符。
那为什么在Navicat中没有使用delimiter重新定义分隔符程序还能正常运行,这是因为Navaicat会自动为我们天机分隔符,但是如果在其他平台上,可能帮我们自动处理这个潜在的问题,进而导致错误。
2.2如何调用存储过程
我们只需要在当前数据库下新建一个查询,在新建查询中使用call + 存储过程名的方式进行调用。
2.3存储过程的查看与删除
对于存储过程的查询和删除,你可以通过Navicat进行操作,这里就不过多介绍了;你也可以通过SQL查询的方式来进行存储过程的查询与删除。
三、变量
在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 ();
四、SQL编程
这里主要介绍MySQL提供的流程控制语法。
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.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();
需要注意的是,当前的程序还是有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、UPDATE、DELETE 等操作 | 不建议执行数据修改操作 |
六、触发器
所谓的触发器完成的功能实际上与我们的条件处理程序类似,都是给了我们当某个条件成立时,让程序员接入的机会,比如面向对象语言中的构造函数就是给了我们程序在对象创建时介入的机会。在比如前端的钩子函数等等都是这个意味。
| 触发器类型 | 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;
为什么这段代码执行后,执行更新操作,日志表没有变换
注意,在更新触发器中不应该对触发器监控的表进行更新操作,在插入触发器中不应该对插入触发器监控的表进行插入操作,在删除触发器中不应该对删除触发器所监控的表进行删除操作。因为这样会导致触发器无限循环,进而导致数据库卡死。
七、为什么要有存储过程
存储过程之所以存在,以及在架构中被归类为服务端组件,核心原因是它能通过“将数据逻辑贴近数据存储”来平衡性能、安全性和维护性,而将逻辑放在客户端反而会带来更多问题。具体可以从以下几个角度理解:
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总结:存储过程的“服务端定位”是权衡后的最优解
存储过程的设计本质是让“数据逻辑”尽可能靠近“数据存储”,利用数据库的原生能力(性能、事务、权限)解决客户端逻辑的固有缺陷(网络开销、安全性、一致性)。 “减少服务端压力”的说法只看到了表面——数据库的计算压力确实存在,但通过存储过程将逻辑集中处理,整体系统的性能、稳定性、可维护性会得到极大提升,这比单纯“转移压力到客户端”更有价值。 当然,存储过程也有局限性(如移植性差、调试困难),因此现代架构中通常“适度使用”(仅处理核心数据逻辑),而非滥用。但这并不改变其“服务端组件”的定位——它的价值正是通过服务端的本地化执行来实现的。
1897

被折叠的 条评论
为什么被折叠?



