强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

MySQL 完全指南 / 第 11 章:存储过程

第 11 章:存储过程

存储过程、函数、触发器——MySQL 的服务端编程能力。


11.1 存储过程(Stored Procedure)

11.1.1 基本语法

-- 创建存储过程
DELIMITER //
CREATE PROCEDURE sp_get_user_orders(IN user_id BIGINT)
BEGIN
    SELECT order_no, total_amount, status, created_at
    FROM orders
    WHERE user_id = user_id
    ORDER BY created_at DESC;
END //
DELIMITER ;

-- 调用
CALL sp_get_user_orders(1);

-- 删除
DROP PROCEDURE IF EXISTS sp_get_user_orders;

11.1.2 参数类型

类型说明示例
IN输入参数(默认)IN user_id BIGINT
OUT输出参数OUT total DECIMAL(12,2)
INOUT输入输出参数INOUT counter INT
DELIMITER //
CREATE PROCEDURE sp_user_stats(
    IN p_user_id BIGINT,
    OUT p_order_count INT,
    OUT p_total_spent DECIMAL(12,2),
    OUT p_avg_order DECIMAL(12,2)
)
BEGIN
    SELECT COUNT(*), COALESCE(SUM(pay_amount), 0), COALESCE(AVG(pay_amount), 0)
    INTO p_order_count, p_total_spent, p_avg_order
    FROM orders
    WHERE user_id = p_user_id AND status IN (1, 2, 3);
END //
DELIMITER ;

-- 调用
CALL sp_user_stats(1, @cnt, @total, @avg);
SELECT @cnt AS order_count, @total AS total_spent, @avg AS avg_order;

11.1.3 变量与控制流

DELIMITER //
CREATE PROCEDURE sp_process_order(IN p_order_id BIGINT)
BEGIN
    -- 声明变量
    DECLARE v_status TINYINT;
    DECLARE v_amount DECIMAL(12,2);
    DECLARE v_user_id BIGINT;
    
    -- 获取订单信息
    SELECT status, pay_amount, user_id 
    INTO v_status, v_amount, v_user_id
    FROM orders WHERE id = p_order_id;
    
    -- IF 条件
    IF v_status = 0 THEN
        UPDATE orders SET status = 1, pay_time = NOW() WHERE id = p_order_id;
        UPDATE users SET balance = balance - v_amount WHERE id = v_user_id;
    ELSEIF v_status = 1 THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '订单已支付';
    ELSE
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '订单状态异常';
    END IF;
END //
DELIMITER ;

11.1.4 游标(Cursor)

DELIMITER //
CREATE PROCEDURE sp_archive_old_orders()
BEGIN
    DECLARE v_id BIGINT;
    DECLARE v_done INT DEFAULT FALSE;
    
    -- 声明游标
    DECLARE cur CURSOR FOR 
        SELECT id FROM orders WHERE status IN (3, 4) AND created_at < DATE_SUB(NOW(), INTERVAL 1 YEAR);
    
    -- 结束处理
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
    
    OPEN cur;
    
    archive_loop: LOOP
        FETCH cur INTO v_id;
        IF v_done THEN
            LEAVE archive_loop;
        END IF;
        
        -- 迁移到归档表
        INSERT INTO orders_archive SELECT * FROM orders WHERE id = v_id;
        DELETE FROM orders WHERE id = v_id;
    END LOOP;
    
    CLOSE cur;
END //
DELIMITER ;

11.1.5 循环

DELIMITER //
CREATE PROCEDURE sp_generate_test_data(IN count INT)
BEGIN
    DECLARE i INT DEFAULT 1;
    
    WHILE i <= count DO
        INSERT INTO users (username, email, password, balance)
        VALUES (
            CONCAT('testuser_', LPAD(i, 6, '0')),
            CONCAT('test', i, '@example.com'),
            SHA2(CONCAT('password', i), 256),
            ROUND(RAND() * 10000, 2)
        );
        SET i = i + 1;
    END WHILE;
END //
DELIMITER ;

-- REPEAT 循环
DELIMITER //
CREATE PROCEDURE sp_repeat_example()
BEGIN
    DECLARE i INT DEFAULT 0;
    REPEAT
        SET i = i + 1;
        INSERT INTO logs (message) VALUES (CONCAT('Entry ', i));
    UNTIL i >= 10
    END REPEAT;
END //
DELIMITER ;

11.2 存储函数(Function)

DELIMITER //
CREATE FUNCTION fn_age(birth_date DATE) RETURNS INT DETERMINISTIC
BEGIN
    RETURN TIMESTAMPDIFF(YEAR, birth_date, CURDATE());
END //
DELIMITER ;

-- 使用
SELECT username, fn_age(birth_date) AS age FROM users;

-- 订单状态文本
DELIMITER //
CREATE FUNCTION fn_order_status(status_code TINYINT) RETURNS VARCHAR(20) DETERMINISTIC
BEGIN
    RETURN CASE status_code
        WHEN 0 THEN '待支付'
        WHEN 1 THEN '已支付'
        WHEN 2 THEN '已发货'
        WHEN 3 THEN '已完成'
        WHEN 4 THEN '已取消'
        WHEN 5 THEN '已退款'
        ELSE '未知'
    END;
END //
DELIMITER ;

SELECT order_no, fn_order_status(status) AS status_text FROM orders;

⚠️ 存储过程 vs 函数

维度存储过程函数
调用方式CALL sp_name()SELECT fn_name()
返回值通过 OUT 参数RETURN 一个值
使用场景复杂业务逻辑计算、转换
SQL 中使用不能在 SELECT 中可以在 SELECT 中

11.3 触发器(Trigger)

-- 创建审计日志触发器
DELIMITER //
CREATE TRIGGER trg_users_after_update
AFTER UPDATE ON users
FOR EACH ROW
BEGIN
    IF OLD.email != NEW.email THEN
        INSERT INTO audit_log (table_name, record_id, field_name, old_value, new_value, changed_at)
        VALUES ('users', NEW.id, 'email', OLD.email, NEW.email, NOW());
    END IF;
    IF OLD.status != NEW.status THEN
        INSERT INTO audit_log (table_name, record_id, field_name, old_value, new_value, changed_at)
        VALUES ('users', NEW.id, 'status', OLD.status, NEW.status, NOW());
    END IF;
END //
DELIMITER ;

-- 库存扣减触发器
DELIMITER //
CREATE TRIGGER trg_order_items_after_insert
AFTER INSERT ON order_items
FOR EACH ROW
BEGIN
    UPDATE products 
    SET stock = stock - NEW.quantity,
        sales = sales + NEW.quantity
    WHERE id = NEW.product_id;
    
    IF (SELECT stock FROM products WHERE id = NEW.product_id) < 0 THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '库存不足';
    END IF;
END //
DELIMITER ;

-- 查看触发器
SHOW TRIGGERS;
SELECT * FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = 'myapp';

-- 删除触发器
DROP TRIGGER IF EXISTS trg_users_after_update;

11.4 事件调度器(Event Scheduler)

-- 开启事件调度器
SET GLOBAL event_scheduler = ON;

-- 创建定时事件:每天清理过期数据
DELIMITER //
CREATE EVENT evt_cleanup_expired
ON SCHEDULE EVERY 1 DAY
STARTS CURRENT_TIMESTAMP
DO
BEGIN
    DELETE FROM sessions WHERE expires_at < NOW();
    DELETE FROM verification_codes WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 DAY);
END //
DELIMITER ;

-- 创建一次性事件
DELIMITER //
CREATE EVENT evt_one_time
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 HOUR
DO
BEGIN
    INSERT INTO logs (message) VALUES ('One hour has passed');
END //
DELIMITER ;

-- 查看事件
SHOW EVENTS;
SELECT * FROM information_schema.EVENTS;

-- 修改事件
ALTER EVENT evt_cleanup_expired DISABLE;
ALTER EVENT evt_cleanup_expired ENABLE;
ALTER EVENT evt_cleanup_expired ON SCHEDULE EVERY 6 HOUR;

-- 删除事件
DROP EVENT IF EXISTS evt_cleanup_expired;

11.5 错误处理

DELIMITER //
CREATE PROCEDURE sp_transfer(
    IN from_id BIGINT,
    IN to_id BIGINT,
    IN amount DECIMAL(12,2)
)
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        GET DIAGNOSTICS CONDITION 1 @err_msg = MESSAGE_TEXT;
        SELECT CONCAT('Error: ', @err_msg) AS error;
    END;
    
    START TRANSACTION;
    
    -- 检查余额
    IF (SELECT balance FROM accounts WHERE id = from_id) < amount THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient balance';
    END IF;
    
    UPDATE accounts SET balance = balance - amount WHERE id = from_id;
    UPDATE accounts SET balance = balance + amount WHERE id = to_id;
    
    COMMIT;
    SELECT 'Transfer successful' AS result;
END //
DELIMITER ;

11.6 何时使用/不使用存储过程

适合使用不适合使用
批量数据处理简单 CRUD
复杂事务逻辑频繁变更的业务逻辑
数据库迁移脚本需要水平扩展的场景
定时任务调试困难的复杂逻辑

💡 微服务时代的建议:核心业务逻辑放在应用层(Java/Go/Python),存储过程用于数据层的批量处理和维护任务。


业务场景

场景 1:每日数据汇总

DELIMITER //
CREATE PROCEDURE sp_daily_report(IN report_date DATE)
BEGIN
    INSERT INTO daily_summary (report_date, order_count, revenue, new_users, avg_order_amount)
    SELECT 
        report_date,
        COUNT(*),
        SUM(pay_amount),
        (SELECT COUNT(*) FROM users WHERE DATE(created_at) = report_date),
        AVG(pay_amount)
    FROM orders
    WHERE DATE(created_at) = report_date AND status IN (1, 2, 3);
END //
DELIMITER ;

-- 配合事件调度器
CREATE EVENT evt_daily_report
ON SCHEDULE EVERY 1 DAY
STARTS '2026-05-11 02:00:00'
DO CALL sp_daily_report(CURDATE() - INTERVAL 1 DAY);

扩展阅读