感谢阅读!❤️
如果这篇文章对你有帮助,欢迎 **点赞** 👍 和 **关注** ⭐,获取更多实用技巧和干货内容!你的支持是我持续创作的动力!
**关注我,不错过每一篇精彩内容!**
目录
- [一、🧠 存储过程](#一、🧠 存储过程)
-
- [1.1 存储过程的基本语法](#1.1 存储过程的基本语法)
- [1.2 存储过程的优缺点](#1.2 存储过程的优缺点)
- [1.3 注意事项与最佳实践](#1.3 注意事项与最佳实践)
- [二、⚙️ MySQL的变量](#二、⚙️ MySQL的变量)
-
- [2.1 系统变量](#2.1 系统变量)
- [2.2 用户变量](#2.2 用户变量)
- [2.3 局部变量](#2.3 局部变量)
- [三、🔄 参数模式](#三、🔄 参数模式)
- [四、🚦 流程控制语句](#四、🚦 流程控制语句)
一、🧠 存储过程
存储过程(Stored Procedure)是数据库中预编译并存储的一组 SQL 语句,可以接受输入参数、返回输出结果,并支持流程控制(如条件判断、循环等)。它在数据库服务器端执行,常用于封装复杂的业务逻辑 、提高性能 、增强安全性 以及减少网络传输开销。
1.1 存储过程的基本语法
- 创建存储过程
sql
CREATE ORICEDURE <存储过程名字>(参数)
BEGIN
...
END
sql
-- 示例1:不带参数(MySQL)
-- 查询员工表(employees)中所有员工的姓名和部门
DELIMITER $$
CREATE PROCEDURE GetAllEmployees()
BEGIN
SELECT name, department
FROM employees;
END$$
DELIMITER ;
sql
-- 示例2:带参数(MySQL)
-- 转账业务
DELIMITER $$
CREATE PROCEDURE TransferMoney(
IN p_from_account INT,
IN p_to_account INT,
IN p_amount DECIMAL(10,2)
)
BEGIN
-- 声明变量
DECLARE v_row_count INT DEFAULT 0;
-- 声明异常处理器:发生任何 SQL 异常时回滚并退出
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL; -- 重新抛出错误(保留原始错误信息)
END;
-- 开始事务(MySQL 中 START TRANSACTION 可省略,但显式写出更清晰)
START TRANSACTION;
-- 扣款
UPDATE Accounts
SET Balance = Balance - p_amount
WHERE AccountId = p_from_account;
-- 检查是否扣款成功(即账户是否存在)
SET v_row_count = ROW_COUNT();
IF v_row_count = 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '转出账户不存在';
END IF;
-- 入账
UPDATE Accounts
SET Balance = Balance + p_amount
WHERE AccountId = p_to_account;
-- 检查是否入账成功
SET v_row_count = ROW_COUNT();
IF v_row_count = 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '转入账户不存在';
END IF;
-- 提交事务
COMMIT;
END$$
DELIMITER ;
DELIMITER $$:更改语句结束符 ,避免与存储过程内部的;冲突。DECLARE EXIT HANDLER FOR SQLEXCEPTION:类似于SQL Server中的CATCH,一旦过程中发生任何SQL错误(包括SIGNAL抛出的错误),都会触发此处理器 。执行ROLLBACK回滚事务,并用RESIGNAL将错误原样抛给调用者。ROW_COUNT():返回上一条UPDATE/DELETE/INSERT影响的行数。必须立即在语句后调用,否则会被后续语句覆盖。SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = ...:'45000'是MySQL中表示"未处理的用户自定义异常"的标准SQLSTATE。MESSAGE_TEXT设置错误信息。触发后会中断当前流程,并被HANDLER捕获。
- 调用存储过程
sql
CALL <存储过程名字>(参数);
-- 示例
CALL GetAllEmployees();
CALL TransferMoney(101, 202, 100.00);
- 查看创建存储过程的语句
sql
SHOW CREATE PROCEDURE <存储过程名字>;
-- 示例
SHOW CREATE PROCEDURE GetAllEmployees();
SHOW CREATE PROCEDURE TransferMoney;
通过系统表
information_schema.ROUTINES查看存储过程的详细信息:information_schema.ROUTINES是 MySQL 数据库中一个系统表,存储了所有存储过程 、函数 、触发器的详细信息,包括名称、返回值类型、参数、创建时间、修改时间等。
sql
SELECT * FROM information_schema.routines WHERE routine_name = 'GetAllEmployees';
SELECT * FROM information_schema.routines WHERE routine_name = 'TransferMoney';
information_schema.ROUTINES 表中的一些重要的列包括:
SPECIFIC_NAME:存储过程的具体名称,包括该存储过程的名字,参数列表。ROUTINE_SCHEMA:存储过程所在的数据库名称。ROUTINE_NAME:存储过程的名称。ROUTINE_TYPE:PROCEDURE表示是一个存储过程,FUNCTION表示是一个函数。ROUTINE_DEFINITION:存储过程的定义语句。CREATED:存储过程的创建时间。LAST_ALTERED:存储过程的最后修改时间。DATA_TYPE:存储过程的返回值类型、参数类型等。
- 删除存储过程
sql
DROP PROCEDURE IF EXISTS <存储过程名字>;
-- 示例
DROP PROCEDURE IF EXISTS GetAllEmployees;
DROP PROCEDURE IF EXISTS TransferMoney;
1.2 存储过程的优缺点
✅优点
- 性能提升
- 预编译与缓存执行计划:首次执行时生成执行计划并缓存,后续调用直接复用,减少解析、优化开销。
- 减少网络往返:客户端只需发送一次调用命令(如
CALL proc()),而非多条SQL语句,尤其在高延迟网络中优势明显。
- 安全性增强
- 权限隔离:可授予用户执行存储过程的权限,而不直接开放底层表的
SELECT/UPDATE权限,防止SQL注入或越权操作。 - 输入验证集中化:参数校验、业务规则统一在数据库层处理,避免应用层遗漏。
- 权限隔离:可授予用户执行存储过程的权限,而不直接开放底层表的
- 代码复用与模块化
- 多个应用(
Web、移动端、报表系统)可共享同一套数据逻辑,避免重复开发。 - 数据操作逻辑集中管理,便于维护和版本控制(若配合脚本管理)。
- 多个应用(
- 事务与一致性保障
- 可在存储过程中实现完整的
ACID事务控制(BEGIN,COMMIT,ROLLBACK),确保复杂操作的原子性。 - 例如:转账、订单创建等涉及多表更新的场景。
- 可在存储过程中实现完整的
- 减少应用层负担
- 将复杂计算(如聚合 、递归 、窗口函数 )下推到数据库,减轻应用服务器
CPU和内存压力。
- 将复杂计算(如聚合 、递归 、窗口函数 )下推到数据库,减轻应用服务器
❌缺点
- 可移植性差
- 各数据库厂商语法差异大(如变量声明、异常处理、循环结构),难以跨平台迁移。
例如:MySQL用DECLARE HANDLER,SQL Server用TRY...CATCH,Oracle用PL/SQL块。 - 使用特定数据库功能(如
SQL Server的CTE递归、PostgreSQL的JSONB)会进一步锁定技术栈。
- 各数据库厂商语法差异大(如变量声明、异常处理、循环结构),难以跨平台迁移。
- 调试与测试困难
- 缺乏成熟的
IDE调试支持(相比Java/Python)。 - 单元测试工具链不完善,难以集成到
CI/CD流程。 - 错误堆栈信息通常不如应用层清晰。
- 缺乏成熟的
- 版本管理与部署复杂
- 存储过程通常不在应用代码仓库中,容易造成
"数据库逻辑漂移"。 - 需要额外的数据库变更管理工具(如
Liquibase、Flyway)或手动脚本维护。 - 回滚机制复杂,尤其是涉及数据变更的存储过程。
- 存储过程通常不在应用代码仓库中,容易造成
- 过度使用导致"胖数据库"
- 将过多业务逻辑塞入数据库,违背
"瘦数据库、胖应用"的现代架构原则。 - 应用层变得
"哑",难以利用缓存(Redis)、消息队列、微服务等中间件优化性能。
- 将过多业务逻辑塞入数据库,违背
- 扩展性受限
- 数据库是垂直扩展瓶颈,而应用层可水平扩展。
- 高并发下,复杂存储过程可能成为性能热点,难以拆分。
1.3 注意事项与最佳实践
- 避免过度使用:简单操作不建议用存储过程,会增加维护成本。
- 错误处理 :应加入异常捕获(如
MySQL的DECLARE CONTINUE HANDLER,SQL Server的TRY...CATCH)。 - 版本管理困难:存储过程通常不在应用代码版本控制系统中,需额外管理。
- 调试不便:相比应用层代码,调试存储过程更复杂。
- 数据库依赖性强:不同数据库语法差异大,影响可移植性。
注意事项
- 事务边界要清晰
- 明确是否由存储过程控制事务(
START TRANSACTION),还是由调用方控制。 - 避免在存储过程中无条件
COMMIT,否则无法被外部事务包含。
- 明确是否由存储过程控制事务(
- 错误处理必须完备
- 必须捕获异常并回滚(如
MySQL的HANDLER FOR SQLEXCEPTION,SQL Server的TRY...CATCH)。 - 自定义错误应使用标准方式抛出(如
SIGNAL / RAISERROR),便于上层识别。
- 必须捕获异常并回滚(如
- 避免隐式提交
- 某些
DDL语句(如CREATE TABLE、ALTER TABLE)在MySQL中会隐式提交事务,破坏原子性。
- 某些
- 注意
ROW_COUNT() / @@ROWCOUNT的时效性- 必须在
DML语句紧随其后读取,任何中间语句(包括IF、SET)都可能覆盖其值。
- 必须在
- 参数类型与精度匹配
- 如
DECIMAL(10,2) vs FLOAT,避免因精度丢失引发金融计算错误。 - 字符串长度不足可能导致截断(如
VARCHAR(50)传入60字符)。
- 如
- 防止死锁
- 多表更新时,始终按固定顺序访问表(如先
A后B),避免循环等待。
- 多表更新时,始终按固定顺序访问表(如先
最佳实践
-
合理使用,非万能药
- ✅ 适合:批量数据处理、强一致性事务、敏感数据操作。
- ❌ 不适合:简单
CRUD、频繁变更的业务逻辑、需要快速迭代的功能。
-
使用
OUT参数或结果集返回数据- 避免依赖全局变量或临时表传递结果。
- 对于单值返回,优先用
OUT;对于列表,用SELECT返回结果集。
-
避免硬编码
- 不要在存储过程中写死表名、数据库名(除非必要)。
- 可通过配置表或参数动态调整。
-
监控性能
- 定期检查慢查询日志、执行计划(
EXPLAIN / SHOW PROFILE)。 - 对高频调用的存储过程做性能压测。
- 定期检查慢查询日志、执行计划(
二、⚙️ MySQL的变量
MySQL中的变量分为三大类:系统变量 、用户变量 、局部变量
2.1 系统变量
系统变量是 MySQL 服务器自身使用的配置参数,用于控制服务器的行为(如最大连接数、缓冲区大小、字符集等)。这些变量由 MySQL 自动维护,用户可以根据需要查看或修改(部分变量支持动态修改)。
MySQL 系统变量可以具有全局(global)或会话(session)作用域:
- 全局作用域:指对所有连接和所有数据库都适用;
- 会话作用域:指只对当前连接和当前数据库适用。
查看系统变量
语法格式:
sql
-- 查看所有的系统变量
SHOW [GLOBAL|SESSION] VARIABLES;
-- 模糊查找系统变量
SHOW [GLOBAL|SESSION] VARIABLES LIKE '';
-- 查找具体的系统变量
SELECT @@[GLOBAL|SESSION].系统变量名;
-- 示例
-- 查看所有的系统变量
SHOW VARIABLES;
SHOW GLOBAL VARIABLES;
SHOW SESSION VARIABLES;
-- 模糊查找COMMIT相关的系统变量
SHOW GLOBAL VARIABLES LIKE '%COMMIT%';
-- 查看隔离级别
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
注意:没有指定
GLOBAL或SESSION时,默认是SESSION。
设置系统变量
sql
SET [GLOBAL | SESSION] 系统变量名 = 值;
或
SET @@[GLOBAL | SESSION].系统变量名 = 值;
-- 示例
-- 修改当前会话的 SQL 模式
SET SESSION sql_mode = 'STRICT_TRANS_TABLES';
SET @@SESSION.sql_mode = 'STRICT_TRANS_TABLES';
-- 修改全局的事务隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET @@GLOBAL.transaction_isolation = 'READ-COMMITTED';
⚠️注意:无论是全局设置 还是会话设置 ,当
MySQL服务重启之后,之前配置都会失效。可以通过修改MySQL根目录下的my.ini配置文件达到永久修改的效果。(my.ini是MySQL数据库默认的系统级配置文件,默认是不存在的,需要新建。)Windows系统是my.ini。linux系统是my.cnf。my.ini文件通常放在MySQL安装的根目录下
2.2 用户变量
用户自定义的变量。只在当前会话有效。所有的用户变量'@'开始。
给用户变量赋值
sql
-- 不推荐使用 =
SET @NAME = 'Tom';
SET @GENDER = '男';
SET @MONEY = 20000;
SET @LOCATION = 'China',@CITY = 'ShenZhen';
--推荐使用 :=
SET @NAME := 'Tom';
SET @GENDER := '男';
SET @MONEY := 20000;
SET @LOCATION := 'China',@CITY := 'ShenZhen';
读取用户变量的值
sql
-- 读取用户变量的值
SELECT @NAME,@GENDER,@MONEY,@LOCATION,@CITY;
-- 读取的结果是:NULL
SELECT @AAAA;
⚠️注意:
MySQL中的用户变量不需要声明。直接赋值就行。如果没有声明用户变量,直接读取该变量,返回NULL
2.3 局部变量
局部变量是在 存储过程(Stored Procedure) 或 函数(Function) 内部定义的变量,使用 declare 声明,作用域仅限于该程序块(BEGIN ... END)内。
变量的声明:
sql
DECLARE 变量名 数据类型 [DEFAULT ...];
变量的数据类型就是表字段的数据类型,例如:
int、bigint、char、varchar、date、time、datetime等。
变量赋值
sql
SET 变量名 = 值;
SET 变量名 := 值;
SELECT 字段名 INTO 变量名 FROM 表名 ...;
sql
-- 示例
DELIMITER $$
DROP PROCEDURE IF EXISTS P1$$
CREATE PROCEDURE P1()
BEGIN
-- 声明变量
DECLARE ID INT;
DECLARE NAME VARCHAR(255);
DECLARE MONEY DECIMAL(10,2) DEFAULT 0;
DECLARE TOTAL INT DEFAULT 0;
-- 给变量赋值
SET ID = 10;
SET NAME = 'Tom';
SET MONEY = 20000;
SELECT COUNT(*) INTO TOTAL FROM STUDENT;
-- 读取变量的值
SELECT ID,NAME,MONEY,TOTAL;
END$$
DELIMITER ;
CALL P1();
三、🔄 参数模式
存储过程的参数包括三种形式:
| 模式 | 说明 | 特点 |
|---|---|---|
IN |
入参。调用者传入值,供过程内部使用 | 默认模式;过程内修改不会影响调用者变量 |
OUT |
出参。过程向调用者返回值 | 调用时可传变量,但初始值被忽略;过程内赋值后返回 |
INOUT |
既是入参,又是出参。既传入初始值,又返回修改后的值 | 结合 IN 和 OUT 的行为 |
sql
-- 示例
DELIMITER $$
CREATE PROCEDURE ExampleProc(
IN p_in INT, -- 输入参数
OUT p_out INT, -- 输出参数
INOUT p_inout INT -- 输入输出参数
)
BEGIN
SET p_out = p_in + 10;
SET p_inout = p_inout * 2;
END$$
DELIMITER ;
调用存储过程:
sql
SET @a = 5, @b = 0, @c = 3;
CALL ExampleProc(@a, @b, @c);
SELECT @a, @b, @c; -- @a=5(不变),@b=15,@c=6
四、🚦 流程控制语句
if 语句
语法格式:
sql
IF 条件 THEN
...
ELSEIF 条件 THEN
...
ELSEIF 条件 THEN
...
ELSE
...
END IF;
例如:学生成绩score,超过80分为优秀,60~80分为良好,60分以下为差
sql
DELIMITER $$
DROP PROCEDURE IF EXISTS GetGradeByScore$$
CREATE PROCEDURE GetGradeByScore(IN SCORE INT,OUT GRADE VARCHAR(255))
BEGIN
-- 使用 IF 流程控制语句
IF SCORE >= 80 THEN
SET GRADE = '优秀';
ELSEIF SCORE >= 60 THEN
SET GRADE = '良好';
ELSE
SET GRADE = '差';
END IF;
END$$
DELIMITER ;
-- 调用存储过程
CALL GetGradeByScore(75, @GRADE);
-- 查看结果:良好
SELECT @GRADE;
case 语句
语法格式:
sql
CASE 值
WHEN 值1 THEN
...
WHEN 值2 THEN
...
WHEN 值3 THEN
...
ELSE
...
END CASE;
或者
sql
CASE
WHEN 条件1 THEN
...
WHEN 条件2 THEN
...
WHEN 条件3 THEN
...
ELSE
...
END CASE;
例如:根据员工等级返回对应奖金金额
sql
DELIMITER $$
DROP PROCEDURE IF EXISTS GetBonusByLevel$$
CREATE PROCEDURE GetBonusByLevel(
IN emp_level CHAR(1),
OUT bonus DECIMAL(10,2)
)
BEGIN
-- 使用 CASE 值 WHEN ... END CASE 流程控制语句
CASE emp_level
WHEN 'A' THEN
SET bonus = 5000.00;
WHEN 'B' THEN
SET bonus = 3000.00;
WHEN 'C' THEN
SET bonus = 1500.00;
WHEN 'D' THEN
SET bonus = 500.00;
ELSE
SET bonus = 0.00; -- 默认无奖金
END CASE;
END$$
DELIMITER ;
-- 调用存储过程
CALL GetBonusByLevel('B', @result);
-- 查看结果:3000.00
SELECT @result AS bonus_amount;
例如:根据用户活跃天数判断会员等级
sql
DELIMITER $$
DROP PROCEDURE IF EXISTS GetUserLevel$$
CREATE PROCEDURE GetUserLevel(
IN active_days INT,
OUT user_level VARCHAR(20)
)
BEGIN
-- 搜索型 CASE 语句:根据条件判断会员等级
CASE
WHEN active_days >= 365 THEN
SET user_level = '黄金会员';
WHEN active_days >= 180 THEN
SET user_level = '白银会员';
WHEN active_days >= 30 THEN
SET user_level = '青铜会员';
WHEN active_days >= 0 THEN
SET user_level = '普通用户';
ELSE
SET user_level = '数据异常';
END CASE;
END$$
DELIMITER ;
-- 测试黄金会员
CALL GetUserLevel(400, @level);
SELECT @level; -- 输出:黄金会员
-- 测试普通用户
CALL GetUserLevel(15, @level);
SELECT @level; -- 输出:普通用户
-- 测试异常数据
CALL GetUserLevel(-5, @level);
SELECT @level; -- 输出:数据异常
while循环语句
语法格式:
sql
WHILE 条件 DO
循环体;
END WHILE;
例如:计算从 1 累加到指定数字 N 的和
sql
DELIMITER $$
DROP PROCEDURE IF EXISTS SumUpToN$$
CREATE PROCEDURE SumUpToN(
IN n INT,
OUT total_sum INT
)
BEGIN
DECLARE i INT DEFAULT 1; -- 循环计数器
DECLARE sum_val INT DEFAULT 0;-- 累加结果
-- WHILE 循环:当 i <= n 时继续循环
WHILE i <= n DO
SET sum_val = sum_val + i; -- 累加
SET i = i + 1; -- 计数器自增
END WHILE;
SET total_sum = sum_val; -- 返回结果
END$$
DELIMITER ;
-- 调用存储过程,计算 1+2+...+10
CALL SumUpToN(10, @result);
-- 查看结果
SELECT @result AS total;
-- 输出:55
repeat循环语句
语法格式:
sql
REPEAT
循环体;
UNTIL 条件
END REPEAT;
注意:条件成立时结束循环。
例如:计算从 1 累加到指定数字 N 的和
sql
DELIMITER $$
DROP PROCEDURE IF EXISTS SumUpToN$$
CREATE PROCEDURE SumUpToN(
IN n INT,
OUT total_sum INT
)
BEGIN
DECLARE i INT DEFAULT 1; -- 循环计数器
DECLARE sum_val INT DEFAULT 0;-- 累加结果
-- REPEAT 循环:
REPEAT
SET sum_val = sum_val + i; -- 累加
SET i = i + 1; -- 计数器自增
UNTIL i > n -- 当 i > n 时结束循环
END REPEAT;
SET total_sum = sum_val; -- 返回结果
END$$
DELIMITER ;
-- 调用存储过程,计算 1+2+...+10
CALL SumUpToN(10, @result);
-- 查看结果
SELECT @result AS total;
-- 输出:55
loop循环语句
语法格式:
sql
开始标志: LOOP
循环体;
条件 THEN
LEAVE 开始标志;
END LOOP 开始标志;
需结合
ITERATE,LEAVE使用。(ITERATE相当于continue,LEAVE相当于break)
例如:计算从 1 累加到指定数字 N 的和
sql
DELIMITER $$
DROP PROCEDURE IF EXISTS SumUpToN$$
CREATE PROCEDURE SumUpToN(
IN n INT,
OUT total_sum INT
)
BEGIN
DECLARE i INT DEFAULT 1; -- 循环计数器
DECLARE sum_val INT DEFAULT 0;-- 累加结果
-- LOOP 循环:
FLAG: LOOP
SET sum_val = sum_val + i; -- 累加
SET i = i + 1; -- 计数器自增
IF i > n THEN -- 当 i > n 时结束循环
LEAVE FLAG;
END IF;
END LOOP FLAG;
SET total_sum = sum_val; -- 返回结果
END$$
DELIMITER ;
-- 调用存储过程,计算 1+2+...+10
CALL SumUpToN(10, @result);
-- 查看结果
SELECT @result AS total;
-- 输出:55