MySQL存储过程是一组为了完成特定功能而预先编译并保存在数据库中的SQL语句集合,它通过指定名称加参数即可重复调用。相比在应用层拼接并执行多条SQL,存储过程能把逻辑下沉到数据库,减少通信开销,也便于统一维护权限与业务逻辑。

一、存储过程基础语法与创建
在MySQL中,默认的语句结束符是分号,但存储过程体内部往往包含多条以分号结尾的SQL,因此需要使用DELIMITER命令临时修改结束符,避免客户端提前提交。定义时使用CREATE PROCEDURE,后接过程名与参数列表,过程体放在BEGIN和END之间。
参数分为IN、OUT和INOUT三种模式。IN表示调用方传入值,过程内修改不影响外部;OUT用于过程向调用方返回结果;INOUT则兼具两者能力。下面的示例创建了一个统计指定用户订单总数的存储过程,结果通过OUT参数返回。
DELIMITER $$
CREATE PROCEDURE count_user_orders(
IN p_user_id INT,
OUT p_order_count INT
)
BEGIN
SELECT COUNT(*) INTO p_order_count
FROM orders
WHERE user_id = p_user_id;
END$$
DELIMITER ;
上述代码中,我们先把结束符改为$$,在过程体里用SELECT ... INTO将查询结果赋给OUT变量。定义完成后恢复分号为结束符。这种写法能保证包含分号的复合语句被完整提交给服务器编译。
存储过程在首次调用时由MySQL解析并生成执行计划,后续调用可直接复用,因此在批量数据处理场景下表现稳定。不过也需注意,过多业务逻辑写入存储过程会降低代码可读性与版本管理便利性。
二、变量、控制流与游标的使用
在过程体内可以使用DECLARE声明局部变量,其作用域仅限于BEGIN...END块。配合IF、CASE、WHILE等控制语句,能够实现复杂分支与循环。当需要处理查询结果集的每一行时,游标(CURSOR)是常用工具。
以下示例演示用游标遍历低库存商品并累加数量,同时用异常处理在遍历结束时关闭游标。这里定义了CONTINUE HANDLER来捕获NOT FOUND条件,是游标循环的标准写法。
DELIMITER $$
CREATE PROCEDURE sum_low_stock(OUT p_total INT)
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE v_stock INT;
DECLARE cur CURSOR FOR SELECT stock FROM products WHERE stock < 10;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
SET p_total = 0;
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_stock;
IF done THEN
LEAVE read_loop;
END IF;
SET p_total = p_total + v_stock;
END LOOP;
CLOSE cur;
END$$
DELIMITER ;
通过DECLARE CURSOR定义只读结果集,OPEN后使用FETCH逐行取出,当无更多数据时触发NOT FOUND处理器,从而安全退出循环。这种方式比在应用层多次查询更省网络交互。
需要留意的是,游标本质是在服务器端临时打开结果集,若数据量巨大且处理逻辑重,可能占用较多内存与连接时间,此时应评估是否改由应用层流式处理更合适。
三、事务控制与异常处理
存储过程常承担写操作,因此事务一致性非常关键。可以在过程内使用START TRANSACTION、COMMIT与ROLLBACK,并结合条件处理器在出错时回滚,保证数据不会处于中间状态。
下面例子在插入订单主表与明细表时包裹事务,若任意一步失败则整体回滚,并通过OUT参数返回状态码供调用方判断。
DELIMITER $$
CREATE PROCEDURE create_order(
IN p_user_id INT,
IN p_amount DECIMAL(10,2),
OUT p_code INT
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SET p_code = -1;
END;
START TRANSACTION;
INSERT INTO orders(user_id, amount) VALUES(p_user_id, p_amount);
INSERT INTO order_log(user_id, amount) VALUES(p_user_id, p_amount);
COMMIT;
SET p_code = 0;
END$$
DELIMITER ;
EXIT HANDLER会在异常发生时立即进入处理块,先回滚再设置返回码,调用方依据p_code决定后续流程。这种结构让错误隔离在数据库层,减少应用层重复判断。
但也要注意,MySQL存储过程不支持像部分商业数据库那样的保存点嵌套高级特性,复杂补偿逻辑仍需应用配合,且异常信息相对简略,排错时建议打开通用日志辅助分析。
四、在程序中调用存储过程
命令行可用CALL语句直接执行,并接收OUT参数。应用层如Java的JDBC则通过CallableStatement完成,需注册输出参数类型再取值。
下面分别是命令行与Java调用示例,展示如何拿到统计结果或状态码。
-- 命令行调用 CALL count_user_orders(12, @cnt); SELECT @cnt;
String sql = "{CALL create_order(?, ?, ?)}";
CallableStatement cs = conn.prepareCall(sql);
cs.setInt(1, 12);
cs.setBigDecimal(2, new BigDecimal("99.90"));
cs.registerOutParameter(3, Types.INTEGER);
cs.execute();
int code = cs.getInt(3);
命令行方式适合运维与脚本验证,Java方式便于集成进业务系统。无论哪种,都应在连接池中注意调用后及时关闭语句对象,避免服务端临时对象堆积。
如果你的团队使用ORM框架,多数也提供调用存储过程的包装接口,但需确认参数绑定与事务边界是否如预期,防止框架自动提交覆盖过程内控制。
五、优缺点与适用场景总结
存储过程的优势在于执行计划复用、减少网络往返、集中管控数据规则;劣势则是不同数据库语法差异大、调试困难、版本管理不如代码直观。对于报表统计、批量清算、数据清洗等重数据轻逻辑的任务,它非常合适。
若业务规则频繁变化且需多语言复用,则建议把核心计算放应用层。理解上述边界,才能在实际项目中让MySQL存储过程真正发挥作用而非成为维护负担。
MySQL存储过程stored_procedure修改时间:2026-08-07 16:48:29