第二十五章 存储过程与函数
本章目标
通过本章学习,你将能够:
- 理解存储过程和存储函数的概念、区别和适用场景
- 掌握存储过程的创建、调用、修改和删除
- 熟练使用 IN、OUT、INOUT 参数进行数据传递
- 掌握变量声明、赋值、流程控制和循环结构
- 使用游标(CURSOR)进行逐行数据处理
- 理解错误处理机制(DECLARE HANDLER)
- 创建和使用存储函数,区分 DETERMINISTIC 和 NOT DETERMINISTIC
- 评估在项目中使用存储过程的利弊
前置知识
在学习本章之前,你需要:
- 熟练掌握 SQL 基本语法(SELECT、INSERT、UPDATE、DELETE)
- 理解 MySQL 的数据类型体系
- 了解变量和参数的概念
- 了解基本的程序设计概念(条件判断、循环)
- 知道 DELIMITER 的作用(MySQL 客户端如何区分语句边界)
25.1 存储过程概述
25.1.1 什么是存储过程
存储过程(Stored Procedure)是一组预编译的 SQL 语句,存储在数据库服务器上,可以被应用程序反复调用。
-- 类比:存储过程就像编程语言中的"函数"或"方法"
-- - 封装了特定的业务逻辑
-- - 接受输入参数,可以返回输出
-- - 可重复调用,无需重复编写相同逻辑
-- - 在服务器端执行,减少网络往返25.1.2 为什么使用存储过程
优势:
- 减少网络流量:多条 SQL 语句封装为一个调用,客户端只需发送 CALL 命令和参数
- 代码复用:多个应用程序可以共享同一存储过程
- 业务逻辑集中管理:规则变更只需修改存储过程,无需修改应用代码
- 安全控制:可以授予用户执行存储过程的权限,而不授予直接操作表的权限
- 预编译执行:首次执行后,执行计划被缓存,后续调用更快
劣势:
- 调试困难:缺乏成熟的调试工具
- 可移植性差:不同数据库的存储过程语法差异很大
- 版本控制困难:SQL 代码不在常规的代码仓库中
- DB 服务器 CPU 开销:所有计算都在数据库服务器上
25.2 DELIMITER 改变语句分隔符
25.2.1 为什么需要 DELIMITER
存储过程体通常包含多条以分号结尾的 SQL 语句。如果不改变分隔符,MySQL 客户端会在遇到第一个分号时就认为整个 CREATE PROCEDURE 语句结束。
-- 错误示例(分号提前结束了 CREATE PROCEDURE 语句)
CREATE PROCEDURE test_proc()
BEGIN
SELECT * FROM users; -- ← 这里的分号结束了整个 CREATE PROCEDURE!
SELECT COUNT(*) FROM orders;
END;
-- 正确做法:改变 DELIMITER
DELIMITER $$
CREATE PROCEDURE test_proc()
BEGIN
SELECT * FROM users; -- ← 这些分号只结束内部的 SQL 语句
SELECT COUNT(*) FROM orders;
END$$
DELIMITER ; -- 恢复默认分隔符25.2.2 DELIMITER 最佳实践
-- 标准模板
DELIMITER //
CREATE PROCEDURE my_procedure()
BEGIN
-- 多条 SQL 语句,用分号分隔
statement1;
statement2;
statement3;
END//
DELIMITER ;
-- 常用替代分隔符:$$ 或 //
-- 避免使用可能出现在 SQL 中的字符作为分隔符
-- 注意:DELIMITER 是 MySQL 客户端命令,不是 SQL 语句
-- 在编程语言的 MySQL 驱动中通常不需要 DELIMITER(一次发送整个语句)25.3 创建和调用存储过程
25.3.1 基本语法
DELIMITER $$
CREATE PROCEDURE proc_name ([parameter_list])
[characteristic ...]
BEGIN
-- 过程体:SQL 语句、变量、流程控制等
END$$
DELIMITER ;
-- parameter_list 语法:
-- [IN | OUT | INOUT] param_name data_type
-- 参数模式:
-- IN(默认):传入参数,过程内部不可修改调用者的变量
-- OUT:传出参数,过程负责赋值,调用者可在调用后读取
-- INOUT:既可传入也可传出25.3.2 无参数存储过程
DELIMITER $$
CREATE PROCEDURE show_all_active_users()
BEGIN
SELECT id, name, email FROM users WHERE status = 'active';
END$$
DELIMITER ;
-- 调用
CALL show_all_active_users();25.3.3 IN 参数
DELIMITER $$
-- 根据用户 ID 查询订单
CREATE PROCEDURE get_orders_by_user(IN user_id_param INT)
BEGIN
SELECT id, order_no, amount, status, created_at
FROM orders
WHERE user_id = user_id_param
ORDER BY created_at DESC;
END$$
DELIMITER ;
-- 调用
CALL get_orders_by_user(100);
-- 多个 IN 参数
DELIMITER $$
CREATE PROCEDURE search_products(
IN category_param INT,
IN min_price DECIMAL(10,2),
IN max_price DECIMAL(10,2)
)
BEGIN
SELECT id, name, price
FROM products
WHERE category_id = category_param
AND price BETWEEN min_price AND max_price;
END$$
DELIMITER ;
-- 调用
CALL search_products(5, 100.00, 500.00);25.3.4 OUT 参数
DELIMITER $$
-- 统计某个状态的订单数和总金额
CREATE PROCEDURE count_orders_by_status(
IN status_param VARCHAR(20),
OUT order_count INT,
OUT total_amount DECIMAL(12,2)
)
BEGIN
SELECT COUNT(*), COALESCE(SUM(amount), 0)
INTO order_count, total_amount
FROM orders
WHERE status = status_param;
END$$
DELIMITER ;
-- 调用 OUT 参数存储过程
CALL count_orders_by_status('completed', @cnt, @total);
-- 读取 OUT 参数的值
SELECT @cnt AS completed_order_count, @total AS completed_total_amount;25.3.5 INOUT 参数
DELIMITER $$
-- 给数值增加百分比(如加价 10%)
CREATE PROCEDURE apply_percentage(INOUT value DECIMAL(12,2), IN pct DECIMAL(5,2))
BEGIN
SET value = value * (1 + pct / 100);
END$$
DELIMITER ;
-- 调用
SET @price = 100.00;
CALL apply_percentage(@price, 10);
SELECT @price; -- 结果: 110.0025.4 变量
25.4.1 DECLARE 局部变量
DECLARE 声明的变量只在其所在的 BEGIN...END 块内有效。所有 DECLARE 语句必须放在 BEGIN 块的最前面(在任何其他语句之前)。
DELIMITER $$
CREATE PROCEDURE demo_variables()
BEGIN
-- DECLARE 必须在最前面
DECLARE var_int INT DEFAULT 0;
DECLARE var_str VARCHAR(100) DEFAULT 'hello';
DECLARE var_date DATE;
-- 赋值
SET var_int = 42;
SET var_date = CURDATE();
-- 通过 SELECT INTO 赋值
SELECT COUNT(*) INTO var_int FROM users;
-- 使用变量
SELECT var_int, var_str, var_date;
END$$
DELIMITER ;25.4.2 用户定义变量 @var
用户变量以 @ 开头,作用域是整个会话(Session),在存储过程内外都可以访问。
-- 在存储过程中使用用户变量
DELIMITER $$
CREATE PROCEDURE use_session_var()
BEGIN
SET @global_counter = COALESCE(@global_counter, 0) + 1;
SELECT @global_counter AS call_count;
END$$
DELIMITER ;
CALL use_session_var(); -- 返回 1
CALL use_session_var(); -- 返回 2
CALL use_session_var(); -- 返回 3
-- 在存储过程外也能访问
SELECT @global_counter;
-- DECLARE 局部变量 vs 用户变量 @var
-- DECLARE 变量:作用域为 BEGIN...END 块,严格类型,必须在最前面声明
-- @var 用户变量:作用域为整个会话,弱类型,随时可用,不需要声明25.4.3 SELECT ... INTO
DELIMITER $$
CREATE PROCEDURE get_user_info(IN user_id_param INT)
BEGIN
DECLARE user_name VARCHAR(100);
DECLARE user_email VARCHAR(200);
DECLARE user_created DATE;
-- 将查询结果赋值给变量
SELECT name, email, DATE(created_at)
INTO user_name, user_email, user_created
FROM users
WHERE id = user_id_param;
-- 使用变量
SELECT user_name, user_email, user_created;
END$$
DELIMITER ;
-- 注意:SELECT INTO 必须返回恰好一行数据
-- 返回 0 行或多余 1 行都会引发错误25.5 流程控制
25.5.1 IF ... THEN ... ELSEIF ... ELSE ... END IF
DELIMITER $$
CREATE PROCEDURE classify_customer(IN total_spent DECIMAL(12,2), OUT tier VARCHAR(20))
BEGIN
IF total_spent >= 10000 THEN
SET tier = 'VIP';
ELSEIF total_spent >= 5000 THEN
SET tier = 'Gold';
ELSEIF total_spent >= 1000 THEN
SET tier = 'Silver';
ELSE
SET tier = 'Normal';
END IF;
END$$
DELIMITER ;
-- 调用
CALL classify_customer(7500, @tier);
SELECT @tier; -- Gold25.5.2 CASE 语句
DELIMITER $$
CREATE PROCEDURE get_day_name(IN day_num INT, OUT day_name_cn VARCHAR(20))
BEGIN
CASE day_num
WHEN 1 THEN SET day_name_cn = '星期一';
WHEN 2 THEN SET day_name_cn = '星期二';
WHEN 3 THEN SET day_name_cn = '星期三';
WHEN 4 THEN SET day_name_cn = '星期四';
WHEN 5 THEN SET day_name_cn = '星期五';
WHEN 6 THEN SET day_name_cn = '星期六';
WHEN 7 THEN SET day_name_cn = '星期日';
ELSE SET day_name_cn = '无效输入';
END CASE;
END$$
DELIMITER ;
-- CASE 搜索语句(类似 IF...ELSEIF)
DELIMITER $$
CREATE PROCEDURE grade_score(IN score INT, OUT grade CHAR(1))
BEGIN
CASE
WHEN score >= 90 THEN SET grade = 'A';
WHEN score >= 80 THEN SET grade = 'B';
WHEN score >= 70 THEN SET grade = 'C';
WHEN score >= 60 THEN SET grade = 'D';
ELSE SET grade = 'F';
END CASE;
END$$
DELIMITER ;25.5.3 循环结构
WHILE 循环:
DELIMITER $$
CREATE PROCEDURE while_demo(IN max_num INT)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE result TEXT DEFAULT '';
WHILE i <= max_num DO
SET result = CONCAT(result, i, ',');
SET i = i + 1;
END WHILE;
SELECT TRIM(TRAILING ',' FROM result) AS numbers;
END$$
DELIMITER ;
CALL while_demo(10); -- 输出: 1,2,3,4,5,6,7,8,9,10REPEAT 循环(至少执行一次):
DELIMITER $$
CREATE PROCEDURE repeat_demo(IN max_num INT)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE result TEXT DEFAULT '';
REPEAT
SET result = CONCAT(result, i, ',');
SET i = i + 1;
UNTIL i > max_num
END REPEAT;
SELECT TRIM(TRAILING ',' FROM result) AS numbers;
END$$
DELIMITER ;LOOP 循环(结合 LEAVE 和 ITERATE):
DELIMITER $$
CREATE PROCEDURE loop_demo(IN max_num INT)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE result TEXT DEFAULT '';
my_loop: LOOP
-- 跳过偶数
IF i % 2 = 0 THEN
SET i = i + 1;
ITERATE my_loop; -- 类似于 continue,跳过本次循环
END IF;
SET result = CONCAT(result, i, ',');
SET i = i + 1;
IF i > max_num THEN
LEAVE my_loop; -- 类似于 break,退出循环
END IF;
END LOOP my_loop;
SELECT TRIM(TRAILING ',' FROM result) AS odd_numbers;
END$$
DELIMITER ;
CALL loop_demo(15); -- 输出: 1,3,5,7,9,11,13,1525.6 游标(CURSOR)
游标用于在存储过程中逐行处理查询结果集。当需要对结果集中每一行执行复杂操作时使用。
DELIMITER $$
CREATE PROCEDURE process_expired_orders()
BEGIN
-- 步骤 1:声明变量(用于存储 FETCH 的列值)
DECLARE done INT DEFAULT FALSE;
DECLARE order_id_var INT;
DECLARE user_id_var INT;
DECLARE amount_var DECIMAL(10,2);
-- 步骤 2:声明游标
DECLARE order_cursor CURSOR FOR
SELECT id, user_id, amount
FROM orders
WHERE status = 'pending'
AND created_at < DATE_SUB(NOW(), INTERVAL 7 DAY);
-- 步骤 3:声明 NOT FOUND 处理器(当游标遍历完所有行时触发)
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
-- 步骤 4:打开游标
OPEN order_cursor;
-- 步骤 5:逐行读取
read_loop: LOOP
FETCH order_cursor INTO order_id_var, user_id_var, amount_var;
-- 检查是否读完
IF done THEN
LEAVE read_loop;
END IF;
-- 对每一行执行业务逻辑
-- 例如:自动取消过期订单并退款
UPDATE orders SET status = 'expired' WHERE id = order_id_var;
INSERT INTO order_logs (order_id, action, created_at)
VALUES (order_id_var, 'auto_expired', NOW());
END LOOP;
-- 步骤 6:关闭游标
CLOSE order_cursor;
SELECT 'Expired order processing completed.' AS message;
END$$
DELIMITER ;游标使用要点:
- DECLARE 顺序:变量 → 游标 → 处理器(必须严格按此顺序)
- 游标操作:OPEN → FETCH (循环) → CLOSE
- 游标不能用于动态 SQL
- InnoDB 中,游标对应的 SELECT 语句在 OPEN 时创建一个临时表保存结果(快照读)
- 大数据量时游标可能占用大量内存,考虑使用分页处理
25.7 错误处理
25.7.1 DECLARE HANDLER
-- 语法:
-- DECLARE {CONTINUE | EXIT} HANDLER FOR condition_value [, condition_value] ... statement
-- condition_value 可以是:
-- SQLSTATE [VALUE] sqlstate_value -- 如 '23000'(唯一约束冲突)
-- mysql_error_code -- 如 1062(Duplicate entry)
-- SQLWARNING -- 所有 01xxx 警告
-- NOT FOUND -- 所有 02xxx(游标结束、SELECT INTO 无结果等)
-- SQLEXCEPTION -- 所有非 01/02 状态码
-- CONTINUE:处理错误后继续执行后续语句
-- EXIT:处理错误后退出当前 BEGIN...END 块DELIMITER $$
CREATE PROCEDURE safe_insert_user(
IN p_name VARCHAR(100),
IN p_email VARCHAR(200)
)
BEGIN
-- 声明 CONTINUE 处理器:遇到重复键错误时不中断,继续执行
DECLARE CONTINUE HANDLER FOR 1062
BEGIN
SELECT CONCAT('Duplicate entry: ', p_email) AS warning;
END;
-- 声明 EXIT 处理器:遇到其他异常时退出
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
SELECT 'An unexpected error occurred. Transaction rolled back.' AS error;
ROLLBACK;
END;
START TRANSACTION;
INSERT INTO users (name, email) VALUES (p_name, p_email);
COMMIT;
SELECT 'User inserted successfully.' AS message;
END$$
DELIMITER ;
-- 测试
CALL safe_insert_user('Alice', 'alice@test.com'); -- 正常插入
CALL safe_insert_user('Alice', 'alice@test.com'); -- 重复键,显示 warning25.7.2 获取错误信息
DELIMITER $$
CREATE PROCEDURE error_info_demo()
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
-- 在处理器中可以获取错误详情
GET DIAGNOSTICS CONDITION 1
@errno = MYSQL_ERRNO,
@msg = MESSAGE_TEXT;
SELECT @errno AS error_code, @msg AS error_message;
ROLLBACK;
END;
-- 故意制造错误
SELECT * FROM non_existent_table;
END$$
DELIMITER ;
-- GET DIAGNOSTICS(MySQL 5.6.4+)可以获取:
-- NUMBER:当前条件的数量(MySQL 5.7+)
-- RETURNED_SQLSTATE:SQLSTATE 值
-- MYSQL_ERRNO:MySQL 错误号
-- MESSAGE_TEXT:错误消息文本
-- COLUMN_NAME:相关的列名
-- CONSTRAINT_NAME:相关的约束名25.8 存储过程管理
-- 查看所有存储过程
SHOW PROCEDURE STATUS;
SHOW PROCEDURE STATUS WHERE Db = 'mydb';
SHOW PROCEDURE STATUS LIKE '%order%';
-- 查看存储过程定义
SHOW CREATE PROCEDURE proc_name;
-- 从 information_schema 查询
SELECT ROUTINE_NAME, ROUTINE_TYPE, CREATED, LAST_ALTERED
FROM information_schema.ROUTINES
WHERE ROUTINE_SCHEMA = 'mydb' AND ROUTINE_TYPE = 'PROCEDURE';
-- 修改存储过程(只能修改特性,不能修改过程体)
ALTER PROCEDURE proc_name COMMENT 'New comment';
ALTER PROCEDURE proc_name SQL SECURITY INVOKER;
-- 要修改过程体,必须 DROP 再 CREATE,或使用 CREATE OR REPLACE
-- 删除存储过程
DROP PROCEDURE [IF EXISTS] proc_name;25.9 存储函数
25.9.1 创建存储函数
存储函数与存储过程类似,但有以下关键区别:
- 函数必须有返回值(RETURNS 子句 + RETURN 语句)
- 函数用于表达式中(SELECT func()),不能用 CALL 调用
- 函数不能修改数据(DETERMINISTIC 要求更严格)
DELIMITER $$
CREATE FUNCTION calc_order_total(order_id_param INT)
RETURNS DECIMAL(12,2)
DETERMINISTIC
READS SQL DATA
BEGIN
DECLARE total DECIMAL(12,2);
SELECT SUM(price * quantity)
INTO total
FROM order_items
WHERE order_id = order_id_param;
RETURN COALESCE(total, 0);
END$$
DELIMITER ;
-- 调用函数(在 SELECT 中)
SELECT id, calc_order_total(id) AS total FROM orders;
-- 在 WHERE 中使用
SELECT * FROM orders WHERE calc_order_total(id) > 1000;25.9.2 DETERMINISTIC vs NOT DETERMINISTIC
-- DETERMINISTIC:相同输入总是返回相同结果
-- 例如:计算税费、格式化字符串
CREATE FUNCTION calc_tax(amount DECIMAL(12,2), rate DECIMAL(5,2))
RETURNS DECIMAL(12,2)
DETERMINISTIC
BEGIN
RETURN amount * rate / 100;
END;
-- NOT DETERMINISTIC:相同输入可能返回不同结果(如依赖当前时间)
-- 例如:计算年龄(依赖当前日期)
CREATE FUNCTION calc_age(birth_date DATE)
RETURNS INT
NOT DETERMINISTIC
READS SQL DATA
BEGIN
RETURN TIMESTAMPDIFF(YEAR, birth_date, CURDATE());
END;
-- DETERMINISTIC 的重要性:
-- 1. 用于生成列(Generated Column)的表达式必须是 DETERMINISTIC
-- 2. 用于函数索引(Functional Index)的函数必须是 DETERMINISTIC
-- 3. 标记正确有助于优化器进行查询优化25.9.3 函数特性子句
-- 特性子句类型:
-- DETERMINISTIC / NOT DETERMINISTIC
-- CONTAINS SQL / NO SQL / READS SQL DATA / MODIFIES SQL DATA
-- SQL SECURITY {DEFINER | INVOKER}
-- COMMENT 'string'
-- 示例:完整的函数特性声明
DELIMITER $$
CREATE FUNCTION format_currency(amount DECIMAL(12,2), currency_code VARCHAR(3))
RETURNS VARCHAR(50)
DETERMINISTIC
CONTAINS SQL
COMMENT 'Format a number as currency string'
BEGIN
RETURN CONCAT(currency_code, ' ', FORMAT(amount, 2));
END$$
DELIMITER ;25.9.4 存储函数的限制
-- 存储函数不能执行以下操作:
-- 1. 不能修改数据(INSERT/UPDATE/DELETE)—— 但可以 SELECT
-- 2. 不能使用事务控制(START TRANSACTION, COMMIT, ROLLBACK)
-- 3. 不能使用动态 SQL(PREPARE/EXECUTE)
-- 4. 不能调用存储过程(CALL)
-- 5. 不能返回结果集(只能 RETURN 单个值)
-- 6. 不能使用 FLUSH、RESET 等管理命令
-- 7. 递归调用深度有限制(max_sp_recursion_depth)25.9.5 存储过程 vs 存储函数
| 特性 | 存储过程 | 存储函数 |
|---|---|---|
| 调用方式 | CALL proc(args) | SELECT func(args) |
| 参数模式 | IN / OUT / INOUT | 只有 IN |
| 返回值 | 通过 OUT 参数返回多个值 | RETURN 返回单个值 |
| 结果集 | 可以返回多个结果集 | 不能返回结果集 |
| 数据修改 | 可以 INSERT/UPDATE/DELETE | 不能修改数据 |
| 事务控制 | 支持 | 不支持 |
| 动态 SQL | 支持 | 不支持 |
| 调用位置 | 独立执行 | 表达式中 |
| 调用存储过程 | 可以 CALL 其他过程 | 不能 CALL 过程 |
25.10 存储过程与函数在现代架构中的地位
25.10.1 使用场景判断
适合使用存储过程:
- 批量数据处理(ETL、数据清洗)
- 复杂的多表事务操作(需要数据库层面的一致性保证)
- 需要最小化网络往返的高延迟环境
- 遗留系统维护
不适合使用存储过程:
- 高并发业务逻辑(应将逻辑放在应用层,DB 专注于 CRUD)
- 需要频繁迭代的业务规则
- 需要水平扩展的分布式系统
- 跨数据库平台的应用
- 需要完善的单元测试的代码25.10.2 现代趋势
现代应用架构倾向于"数据库只做存储和查询",业务逻辑放在应用层(Java、Python、Go 等)。原因包括:
- 应用服务器水平扩展容易,数据库扩展困难
- 应用层有更好的调试工具和测试框架
- ORM 框架(如 Hibernate、MyBatis、SQLAlchemy)已经很成熟
- DevOps 实践中,SQL 存储过程的 CI/CD 比应用代码复杂得多
常见错误
DECLARE 语句位置错误
- 错误:将 DECLARE 放在 SET 或 SELECT 语句之后。
- 原因:DECLARE 必须在 BEGIN 块的最前面,在所有可执行语句之前。
- 解决:调整顺序为:DECLARE 变量 → DECLARE 游标 → DECLARE HANDLER → 执行语句。
SELECT INTO 返回多行或零行
- 错误:
SELECT col INTO var FROM t WHERE condition;在 WHERE 条件匹配多行时出错。 - 原因:SELECT INTO 要求恰好返回一行。
- 解决:使用 LIMIT 1,或使用游标逐行处理,或添加适当的 HANDLER。
- 错误:
忘记恢复 DELIMITER
- 错误:
DELIMITER $$后定义了存储过程,但忘记DELIMITER ;恢复。 - 后果:后续 SQL 语句可能无法正常执行或被误解。
- 解决:始终在存储过程/函数定义结束时恢复 DELIMITER。
- 错误:
游标未正确关闭
- 错误:在异常退出时游标保持 OPEN 状态。
- 后果:占用内存资源,可能影响后续操作。
- 解决:在 HANDLER 中也添加 CLOSE cursor 操作,或在退出前确保游标被关闭。
标记函数为 DETERMINISTIC 但实际非确定性
- 错误:将包含 NOW() 或 RAND() 的函数标记为 DETERMINISTIC。
- 后果:MySQL 可能在查询中只调用一次并进行结果缓存,导致所有行得到相同的值。
- 解决:包含非确定性函数的函数应标记为 NOT DETERMINISTIC。
本章练习
参数传递练习:创建一个存储过程,接收用户 ID(IN),返回用户的订单总数(OUT)和总消费金额(OUT)。通过 CALL 和 SELECT 验证 OUT 参数的值。
流程控制练习:创建一个存储过程,接收一个年份参数,判断该年份是否为闰年(能被 4 整除但不能被 100 整除,或能被 400 整除),返回判断结果。
游标练习:创建一张"待处理任务"表,使用游标逐行处理:对于每个任务,根据其优先级(high/medium/low)和创建时间,计算预期完成时间并更新到另一列。在过程中使用 LEAVE 和 ITERATE。
错误处理练习:创建一个存储过程,尝试向 users 表插入数据,并实现以下错误处理:(1) 重复邮箱(1062)→ 记录日志并继续;(2) 字段过长(1406)→ 截断并重试;(3) 其他错误 → 回滚事务并记录错误信息。
存储函数练习:创建一个函数
mask_phone(phone VARCHAR(20)),将手机号中间四位替换为星号(如 13812345678 → 138****5678)。确保函数标记为 DETERMINISTIC,并在 SELECT 中测试。综合实战:设计一个订单自动处理存储过程,包括:接收订单 ID、验证订单状态、检查库存、更新库存、更新订单状态、记录操作日志。使用事务确保一致性,使用错误处理器处理库存不足等异常情况。