MySQL 存储过程开发全攻略:变量、参数与流程控制详解

复制代码
感谢阅读!❤️
如果这篇文章对你有帮助,欢迎 **点赞** 👍 和 **关注** ⭐,获取更多实用技巧和干货内容!你的支持是我持续创作的动力!
**关注我,不错过每一篇精彩内容!**

目录

  • [一、🧠 存储过程](#一、🧠 存储过程)
    • [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 中表示"未处理的用户自定义异常"的标准 SQLSTATEMESSAGE_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 存储过程的优缺点

✅优点

  1. 性能提升
    • 预编译与缓存执行计划:首次执行时生成执行计划并缓存,后续调用直接复用,减少解析、优化开销。
    • 减少网络往返:客户端只需发送一次调用命令(如 CALL proc()),而非多条 SQL 语句,尤其在高延迟网络中优势明显。
  2. 安全性增强
    • 权限隔离:可授予用户执行存储过程的权限,而不直接开放底层表的 SELECT/UPDATE 权限,防止 SQL 注入或越权操作。
    • 输入验证集中化:参数校验、业务规则统一在数据库层处理,避免应用层遗漏。
  3. 代码复用与模块化
    • 多个应用(Web、移动端、报表系统)可共享同一套数据逻辑,避免重复开发。
    • 数据操作逻辑集中管理,便于维护和版本控制(若配合脚本管理)。
  4. 事务与一致性保障
    • 可在存储过程中实现完整的 ACID 事务控制(BEGIN, COMMIT, ROLLBACK),确保复杂操作的原子性。
    • 例如:转账、订单创建等涉及多表更新的场景。
  5. 减少应用层负担
    • 将复杂计算(如聚合递归窗口函数 )下推到数据库,减轻应用服务器 CPU 和内存压力。

❌缺点

  1. 可移植性差
    • 各数据库厂商语法差异大(如变量声明、异常处理、循环结构),难以跨平台迁移。
      例如:MySQLDECLARE HANDLERSQL ServerTRY...CATCHOraclePL/SQL 块。
    • 使用特定数据库功能(如 SQL ServerCTE 递归、PostgreSQLJSONB)会进一步锁定技术栈。
  2. 调试与测试困难
    • 缺乏成熟的 IDE 调试支持(相比 Java/Python)。
    • 单元测试工具链不完善,难以集成到 CI/CD 流程。
    • 错误堆栈信息通常不如应用层清晰。
  3. 版本管理与部署复杂
    • 存储过程通常不在应用代码仓库中,容易造成"数据库逻辑漂移"
    • 需要额外的数据库变更管理工具(如 LiquibaseFlyway)或手动脚本维护。
    • 回滚机制复杂,尤其是涉及数据变更的存储过程。
  4. 过度使用导致"胖数据库"
    • 将过多业务逻辑塞入数据库,违背 "瘦数据库、胖应用" 的现代架构原则。
    • 应用层变得"哑",难以利用缓存(Redis)、消息队列、微服务等中间件优化性能。
  5. 扩展性受限
    • 数据库是垂直扩展瓶颈,而应用层可水平扩展。
    • 高并发下,复杂存储过程可能成为性能热点,难以拆分。

1.3 注意事项与最佳实践

  • 避免过度使用:简单操作不建议用存储过程,会增加维护成本。
  • 错误处理 :应加入异常捕获(如 MySQLDECLARE CONTINUE HANDLERSQL ServerTRY...CATCH)。
  • 版本管理困难:存储过程通常不在应用代码版本控制系统中,需额外管理。
  • 调试不便:相比应用层代码,调试存储过程更复杂。
  • 数据库依赖性强:不同数据库语法差异大,影响可移植性。

注意事项

  1. 事务边界要清晰
    • 明确是否由存储过程控制事务(START TRANSACTION),还是由调用方控制。
    • 避免在存储过程中无条件 COMMIT,否则无法被外部事务包含。
  2. 错误处理必须完备
    • 必须捕获异常并回滚(如 MySQLHANDLER FOR SQLEXCEPTIONSQL ServerTRY...CATCH)。
    • 自定义错误应使用标准方式抛出(如 SIGNAL / RAISERROR),便于上层识别。
  3. 避免隐式提交
    • 某些 DDL 语句(如 CREATE TABLEALTER TABLE)在 MySQL 中会隐式提交事务,破坏原子性。
  4. 注意 ROW_COUNT() / @@ROWCOUNT 的时效性
    • 必须在 DML 语句紧随其后读取,任何中间语句(包括 IFSET)都可能覆盖其值。
  5. 参数类型与精度匹配
    • DECIMAL(10,2) vs FLOAT,避免因精度丢失引发金融计算错误。
    • 字符串长度不足可能导致截断(如 VARCHAR(50) 传入 60 字符)。
  6. 防止死锁
    • 多表更新时,始终按固定顺序访问表(如先 AB),避免循环等待。

最佳实践

  1. 合理使用,非万能药

    • ✅ 适合:批量数据处理、强一致性事务、敏感数据操作。
    • ❌ 不适合:简单 CRUD、频繁变更的业务逻辑、需要快速迭代的功能。
  2. 使用 OUT 参数或结果集返回数据

    • 避免依赖全局变量或临时表传递结果。
    • 对于单值返回,优先用 OUT;对于列表,用 SELECT 返回结果集。
  3. 避免硬编码

    • 不要在存储过程中写死表名、数据库名(除非必要)。
    • 可通过配置表或参数动态调整。
  4. 监控性能

    • 定期检查慢查询日志、执行计划(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;

注意:没有指定 GLOBALSESSION 时,默认是 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.iniMySQL 数据库默认的系统级配置文件,默认是不存在的,需要新建。) Windows 系统是my.inilinux 系统是my.cnfmy.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 ...];

变量的数据类型就是表字段的数据类型,例如:intbigintcharvarchardatetimedatetime等。

变量赋值

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 既是入参,又是出参。既传入初始值,又返回修改后的值 结合 INOUT 的行为
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相当于continueLEAVE相当于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
相关推荐
言之。11 小时前
DDIA第四章 数据库存储引擎面试问题集
数据库·面试·职场和发展·ddia
wangbing112511 小时前
redis的存储问题
数据库·redis·缓存
剑来.12 小时前
一次完整的 MySQL 性能问题排查思路(线上实战总结)
数据库·mysql·oracle
2301_8002561112 小时前
【数据库】查找距离最近的电影院 pgSQL 存储过程片段
大数据·数据库·excel
2501_9418072612 小时前
在迪拜智能机场场景中构建行李实时调度与高并发航班数据分析平台的工程设计实践经验分享
java·前端·数据库
week_泽12 小时前
小程序云数据库查询操作_2
数据库·小程序
一 乐12 小时前
餐厅点餐|基于springboot + vue餐厅点餐系统(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·后端
白帽子黑客杰哥12 小时前
除了SQL注入,WAF绕过技术如何应用于XSS、文件上传等其他漏洞类型?
网络·sql·xss·漏洞挖掘
小王和八蛋12 小时前
TDDL、Amoeba、Cobar、MyCAT 架构比较
数据库
jnrjian12 小时前
Oracle 列A=列A 相当于列不为空,条件无意义
数据库·sql