前置说明
-
- 环境:MySQL 5.7 / 8.0 通用;
-
- 示例业务:用户订单统计业务,包含:输入用户ID、输出订单总数、遍历订单、条件判断、事务回滚、异常捕获;
-
- 语法关键点:
DELIMITER 分隔符修改、IN/OUT/INOUT 参数、DECLARE 变量、HANDLER 异常、LOOP/IF、COMMIT/ROLLBACK。
一、先建测试表(配套示例使用)
-- 用户表
CREATE TABLE `user` (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
username VARCHAR(50) NOT NULL COMMENT '用户名',
create_time DATETIME DEFAULT NOW()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 订单表
CREATE TABLE `order` (
order_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '订单号',
user_id INT NOT NULL COMMENT '所属用户',
order_money DECIMAL(10,2) NOT NULL COMMENT '订单金额',
order_status TINYINT NOT NULL COMMENT '0待支付 1已完成 2已取消',
create_time DATETIME DEFAULT NOW(),
FOREIGN KEY (user_id) REFERENCES `user`(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 测试数据
INSERT INTO `user`(username) VALUES ('张三'),('李四');
INSERT INTO `order`(user_id,order_money,order_status)
VALUES
(1,99.00,1),
(1,199.50,0),
(2,59.90,1);
二、示例1:基础存储过程(IN入参 + OUT出参,统计用户完成订单总额)
功能:传入用户ID,输出该用户已完成订单总金额
-- 修改语句结束符为 $$,避免 ; 提前终止存储过程定义
DELIMITER $$
-- 创建存储过程
CREATE PROCEDURE GetUserFinishOrderTotal(
IN p_user_id INT, -- IN:输入参数,用户ID
OUT p_total_money DECIMAL(10,2) -- OUT:输出参数,总金额
)
BEGIN
-- 查询已完成订单 status=1,求和赋值给输出参数
SELECT IFNULL(SUM(order_money), 0.00)
INTO p_total_money
FROM `order`
WHERE user_id = p_user_id AND order_status = 1;
END $$
-- 恢复默认结束符 ;
DELIMITER ;
-- ==================== 调用测试 ====================
CALL GetUserFinishOrderTotal(1, @total);
SELECT @total AS 张三已完成订单总额; -- 结果 99.00
CALL GetUserFinishOrderTotal(2, @total2);
SELECT @total2 AS 李四已完成订单总额; -- 结果 59.90
三、示例2:带 IF 判断、循环、INOUT 双向参数
功能:批量更新用户所有未支付订单为取消,返回修改条数
DELIMITER $$
CREATE PROCEDURE CancelAllUnPayOrder(
IN p_user_id INT,
INOUT p_update_count INT -- INOUT:可传值、可输出结果
)
BEGIN
-- 定义局部变量
DECLARE v_has_data INT DEFAULT 1;
DECLARE v_order_id INT;
-- 定义游标:读取该用户所有待支付订单
DECLARE order_cursor CURSOR FOR
SELECT order_id FROM `order` WHERE user_id = p_user_id AND order_status = 0;
-- 游标遍历完毕时,修改标记
DECLARE EXIT HANDLER FOR NOT FOUND SET v_has_data = 0;
-- 初始化修改计数
SET p_update_count = 0;
-- 打开游标
OPEN order_cursor;
-- 循环读取
read_loop: LOOP
-- 读取一行数据到变量
FETCH order_cursor INTO v_order_id;
-- 无数据则跳出循环
IF v_has_data = 0 THEN
LEAVE read_loop;
END IF;
-- 更新订单状态为取消 2
UPDATE `order` SET order_status = 2 WHERE order_id = v_order_id;
SET p_update_count = p_update_count + 1;
END LOOP read_loop;
-- 关闭游标
CLOSE order_cursor;
END $$
DELIMITER ;
-- ==================== 调用测试 ====================
SET @count = 0;
CALL CancelAllUnPayOrder(1, @count);
SELECT @count AS 取消订单数量; -- 用户1原本1条待支付,返回1
四、示例3:带事务 + 异常回滚(生产级标准模板)
业务:新增用户同时创建一笔初始化订单,任一SQL失败全部回滚
DELIMITER $$
CREATE PROCEDURE AddUserWithInitOrder(
IN p_username VARCHAR(50),
IN p_init_money DECIMAL(10,2),
OUT p_new_user_id INT,
OUT p_result_msg VARCHAR(100)
)
BEGIN
-- 声明异常:任意SQL报错触发回滚
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK; -- 出错回滚
SET p_result_msg = '执行失败:事务已回滚';
END;
-- 开启事务
START TRANSACTION;
-- 1.插入用户
INSERT INTO `user`(username) VALUES (p_username);
SET p_new_user_id = LAST_INSERT_ID();
-- 2.插入初始化订单
INSERT INTO `order`(user_id, order_money, order_status)
VALUES (p_new_user_id, p_init_money, 0);
-- 无异常则提交
COMMIT;
SET p_result_msg = '执行成功:用户与初始化订单创建完成';
END $$
DELIMITER ;
-- ==================== 调用测试 ====================
CALL AddUserWithInitOrder('王五', 29.90, @uid, @msg);
SELECT @uid AS 新用户ID, @msg AS 执行结果;
五、常用管理存储过程语句
-- 1.查看数据库所有存储过程
SHOW PROCEDURE STATUS WHERE Db = '你的库名';
-- 2.查看存储过程源码
SHOW CREATE PROCEDURE GetUserFinishOrderTotal;
-- 3.删除存储过程(存在才删,避免报错)
DROP PROCEDURE IF EXISTS GetUserFinishOrderTotal;
-- 4.查询存储过程参数信息
SELECT * FROM information_schema.PARAMETERS WHERE SPECIFIC_NAME='GetUserFinishOrderTotal';
六、关键语法说明(避坑重点)
-
DELIMITER $$
MySQL 默认以 ; 为执行结束符,存储过程内部大量分号会导致提前截断定义,所以临时修改结束符,定义完成后恢复 ;。
-
- 三种参数区别
- •
IN:只读入,过程内修改不影响外部变量;
- •
OUT:仅向外输出,传入值会被清空;
- •
INOUT:可读可写,双向传递。
-
DECLARE 局部变量
必须写在 BEGIN 最开头,游标、异常处理器也要放在最前,顺序:变量 → 游标 → 异常。
-
- 事务+异常组合
生产环境增删改多表操作必须加 DECLARE EXIT HANDLER FOR SQLEXCEPTION,保证原子性。
-
- 表名反引号
order
order 是MySQL关键字,作为表名必须用反引号包裹,否则语法报错。
七、生产使用建议
-
- 复杂业务拆分为多个存储过程,避免单段代码过长;
-
- 禁止在存储过程中做大量大表循环,会锁表影响性能;
-
- 存储过程内尽量少写业务逻辑,复杂计算放应用层;
-
- 定期备份存储过程源码,不要只依赖数据库内定义。