📊 SQL 入门 Day 23:存储过程与函数

存储过程和函数是 SQL 中最强大的可编程特性------把业务逻辑封装在数据库层,减少网络往返,提升执行效率。理解它们能帮你从"写 SQL 的人"进化成"设计数据库系统的人"。


场景引入

想象你在做一个电商系统,用户下单后需要:

  1. 检查库存是否充足
  2. 扣减库存
  3. 创建订单
  4. 生成支付记录

如果每次都在应用代码里一步步执行,至少需要 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 条订单记录              |
+-----------------------------------+

六、注意事项

  1. 安全性 :存储过程默认以调用者权限执行,但通过 SQL SECURITY DEFINER 可以提升权限------谨慎使用
  2. 调试困难:相比应用代码,存储过程的调试手段有限,复杂逻辑建议在应用层实现
  3. 版本控制:存储过程存在数据库内,建议在项目中使用迁移脚本(Flyway/Liquibase)管理
  4. 性能陷阱:不恰当的使用游标逐行处理性能极差,能用集合操作解决的不要用游标
  5. MySQL 差异:MySQL 的存储过程语法与其他数据库(PostgreSQL/PGPLSQL、SQL Server/T-SQL)差异较大,迁移时注意

总结

  • 存储过程:适用于事务性批量操作,一次调用完成多步 DML
  • 存储函数:适用于可复用的计算逻辑,在 SELECT 中直接调用
  • 游标:逐行处理数据的利器,但能用集合操作就别用游标
  • 最佳实践:简单逻辑用存储函数,复杂事务用存储过程,业务规则放在应用层

存储过程和函数是把"数据库当计算引擎用"的核心工具。理解它们,你对 SQL 的掌控力会上一个大台阶。

相关推荐
weixin_431600441 小时前
Agent Workflow 学习向:最小拖拽画布,看得见地编排,再一键跑通
后端·学习·ai·dify
IT_陈寒1 小时前
Python的列表拷贝坑得我原地打转
前端·人工智能·后端
山甫aa2 小时前
日志技术 Logback + Slf4j —— 从零开始的 Web 后端学习
java·后端·学习·web·logback
韶博雅2 小时前
开启补充日志
java·开发语言·sql
网安蟹佬霸2 小时前
SSRF服务端请求伪造攻防实战:从内网穿透到云元数据利用
sql·web安全·网络安全·自动化·网安
逸Y 仙X2 小时前
Spark SQL
java·大数据·sql·spark
SomeB1oody2 小时前
【RustyML入门】7.3. 性能调优与并行
开发语言·后端·机器学习·rust·教程
手握风云-3 小时前
Spring Cloud:分布式系统的“粘合剂”(六)
后端·spring·spring cloud
yours_Gabriel3 小时前
【一】数据库基础:SQL语句、函数、约束、多表查询、事务
数据库·sql·oracle