存储过程和函数是 SQL 中最强大的可编程特性------把业务逻辑封装在数据库层,减少网络往返,提升执行效率。理解它们能帮你从"写 SQL 的人"进化成"设计数据库系统的人"。
场景引入
想象你在做一个电商系统,用户下单后需要:
- 检查库存是否充足
- 扣减库存
- 创建订单
- 生成支付记录
如果每次都在应用代码里一步步执行,至少需要 4 次网络往返,而且事务边界很难保证一致性。存储过程可以一次性搞定。
一、存储过程(Stored Procedure)
1. 创建存储过程
sql
DELIMITER $$
CREATE PROCEDURE sp_create_order(
IN p_user_id INT,
IN p_product_id INT,
IN p_quantity INT,
OUT p_order_id INT,
OUT p_message VARCHAR(100)
)
BEGIN
DECLARE v_stock INT;
-- 开启事务
START TRANSACTION;
-- 检查库存
SELECT stock INTO v_stock
FROM products
WHERE id = p_product_id
FOR UPDATE;
IF v_stock >= p_quantity THEN
-- 扣减库存
UPDATE products
SET stock = stock - p_quantity
WHERE id = p_product_id;
-- 创建订单
INSERT INTO orders (user_id, product_id, quantity, status)
VALUES (p_user_id, p_product_id, p_quantity, 'pending');
SET p_order_id = 921714;
SET p_message = '下单成功';
COMMIT;
ELSE
SET p_order_id = 0;
SET p_message = '库存不足';
ROLLBACK;
END IF;
END$$
DELIMITER ;
2. 调用存储过程
sql
-- 调用
CALL sp_create_order(1001, 5, 2, @order_id, @msg);
-- 查看输出参数
SELECT @order_id, @msg;
执行结果:
| @order_id | @msg |
|---|---|
| 1024 | 下单成功 |
3. 删除存储过程
sql
DROP PROCEDURE IF EXISTS sp_create_order;
4. 查看存储过程定义
sql
SHOW CREATE PROCEDURE sp_create_order;
-- 或从系统表查询
SELECT * FROM information_schema.ROUTINES
WHERE ROUTINE_NAME = 'sp_create_order';
二、存储函数(Stored Function)
函数与存储过程的关键区别:函数必须有返回值,可以在 SQL 表达式中直接使用。
1. 创建函数
sql
DELIMITER $$
CREATE FUNCTION fn_get_user_order_count(p_user_id INT)
RETURNS INT
DETERMINISTIC
READS SQL DATA
BEGIN
DECLARE v_count INT;
SELECT COUNT(*) INTO v_count
FROM orders
WHERE user_id = p_user_id;
RETURN v_count;
END$$
DELIMITER ;
2. 使用函数
sql
-- 在 SELECT 中直接调用
SELECT id, name,
fn_get_user_order_count(id) AS order_count
FROM users
WHERE id = 1001;
执行结果:
| id | name | order_count |
|---|---|---|
| 1001 | 张三 | 12 |
3. 函数特性标识说明
| 标识 | 含义 |
|---|---|
DETERMINISTIC |
相同输入总是返回相同结果(影响查询缓存) |
NOT DETERMINISTIC |
默认,输入相同结果可能不同(如 NOW()) |
READS SQL DATA |
函数包含 SELECT 操作 |
MODIFIES SQL DATA |
函数包含 INSERT/UPDATE/DELETE |
三、存储过程 vs 存储函数
| 对比维度 | 存储过程 | 存储函数 |
|---|---|---|
| 返回值 | 通过 OUT 参数,可返回多个值 | 必须有且仅有一个返回值 |
| 调用方式 | CALL proc(...) |
在 SQL 表达式中直接使用 |
| 事务能力 | 支持事务控制(COMMIT/ROLLBACK) | 通常不支持事务操作 |
| SELECT 中调用 | 不可以 | 可以 |
| 常见场景 | 批量数据处理、事务操作 | 计算逻辑、数据校验 |
四、进阶技巧
1. 游标(Cursor)------逐行处理数据
sql
DELIMITER $$
CREATE PROCEDURE sp_generate_monthly_report()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE v_user_id INT;
DECLARE v_user_name VARCHAR(50);
-- 定义游标
DECLARE cur CURSOR FOR
SELECT id, name FROM users WHERE status = 'active';
-- 定义 NOT FOUND 处理
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
-- 创建临时表存结果
CREATE TEMPORARY TABLE IF NOT EXISTS tmp_report (
user_id INT,
user_name VARCHAR(50),
order_total DECIMAL(10,2)
);
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_user_id, v_user_name;
IF done THEN
LEAVE read_loop;
END IF;
INSERT INTO tmp_report
SELECT v_user_id, v_user_name, COALESCE(SUM(amount), 0)
FROM orders
WHERE user_id = v_user_id
AND order_date >= DATE_FORMAT(CURDATE(), '%Y-%m-01');
END LOOP;
CLOSE cur;
SELECT * FROM tmp_report;
DROP TEMPORARY TABLE tmp_report;
END$$
DELIMITER ;
2. 条件处理(Handler)
sql
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SELECT '发生异常,事务已回滚' AS error_msg;
END;
DECLARE EXIT HANDLER FOR SQLSTATE '23000'
BEGIN
SELECT '违反唯一约束' AS error_msg;
END;
| Handler 类型 | 行为 |
|---|---|
CONTINUE |
执行处理后继续执行后续语句 |
EXIT |
执行处理后退出当前 BEGIN...END 块 |
3. 变量作用域
sql
CREATE PROCEDURE sp_demo()
BEGIN
DECLARE v_msg VARCHAR(20) DEFAULT '外层';
BEGIN
DECLARE v_msg VARCHAR(20) DEFAULT '内层'; -- 同名局部变量,不影响外层
SET v_msg = '修改内层';
END;
SELECT v_msg; -- 结果:'外层'
END;
五、实战:自动归档旧订单
sql
DELIMITER $$
CREATE PROCEDURE sp_archive_orders(p_days INT)
BEGIN
DECLARE v_count INT DEFAULT 0;
DECLARE v_archived INT DEFAULT 0;
-- 计算归档日期
SET @cutoff_date = DATE_SUB(CURDATE(), INTERVAL p_days DAY);
-- 检查待归档数据
SELECT COUNT(*) INTO v_count
FROM orders
WHERE status = 'completed'
AND order_date < @cutoff_date;
IF v_count = 0 THEN
SELECT CONCAT('没有需要归档的数据') AS result;
ELSE
-- 插入归档表
INSERT INTO orders_archive
SELECT * FROM orders
WHERE status = 'completed'
AND order_date < @cutoff_date;
SET v_archived = ROW_COUNT();
-- 删除原表数据
DELETE FROM orders
WHERE status = 'completed'
AND order_date < @cutoff_date;
SELECT CONCAT('已归档 ', v_archived, ' 条订单记录') AS result;
END IF;
END$$
DELIMITER ;
-- 执行:归档 90 天前的已完成订单
CALL sp_archive_orders(90);
执行结果:
sql
+-----------------------------------+
| result |
+-----------------------------------+
| 已归档 156 条订单记录 |
+-----------------------------------+
六、注意事项
- 安全性 :存储过程默认以调用者权限执行,但通过
SQL SECURITY DEFINER可以提升权限------谨慎使用 - 调试困难:相比应用代码,存储过程的调试手段有限,复杂逻辑建议在应用层实现
- 版本控制:存储过程存在数据库内,建议在项目中使用迁移脚本(Flyway/Liquibase)管理
- 性能陷阱:不恰当的使用游标逐行处理性能极差,能用集合操作解决的不要用游标
- MySQL 差异:MySQL 的存储过程语法与其他数据库(PostgreSQL/PGPLSQL、SQL Server/T-SQL)差异较大,迁移时注意
总结
- 存储过程:适用于事务性批量操作,一次调用完成多步 DML
- 存储函数:适用于可复用的计算逻辑,在 SELECT 中直接调用
- 游标:逐行处理数据的利器,但能用集合操作就别用游标
- 最佳实践:简单逻辑用存储函数,复杂事务用存储过程,业务规则放在应用层
存储过程和函数是把"数据库当计算引擎用"的核心工具。理解它们,你对 SQL 的掌控力会上一个大台阶。