存储过程最强大的能力之一,就是可以接收外部传入的数据,并在处理完成后把结果返回给调用者。这套机制的实现完全依赖于参数定义,其中IN参数和OUT参数是最常用的两种类型。很多初学者写了半天存储过程,结果发现调用后拿不到返回值,问题往往就出在对参数传递方向理解不清。本文将从参数的基本原理讲起,结合MySQL和Oracle的实际语法,把IN、OUT以及INOUT三种参数的用法彻底讲透。

一、IN参数与OUT参数的工作原理
要理解参数的用法,先要弄清楚数据在存储过程里是如何流动的。存储过程本质上是一段封装好的SQL逻辑,它运行时需要一个与外界交换数据的通道,参数就是这个通道。按数据流方向划分,参数分为三种:IN表示数据从调用方流入过程,OUT表示数据从过程流回调用方,INOUT则表示双向流动。
IN参数是最常见的类型。调用时传入一个值(可以是常量、变量或表达式),过程内部可以像使用局部变量一样读取它,但即使过程内部修改了这个参数的值,调用方的原始数据也不会受到任何影响。这是因为IN参数在传递时采用的是值拷贝,过程拿到的是副本而非原件。
OUT参数正好相反。调用时传入的初始值会被忽略,过程内部对它赋的值会在过程执行结束后带回给调用方。换句话说,OUT参数是存储过程向外部输出数据的出口。如果过程内部从未给OUT参数赋值,那么调用方拿到的将是NULL。理解了这两个方向,INOUT参数就容易了:它既接收输入,又能把修改后的值传回去。
二、在MySQL中创建带IN和OUT参数的存储过程
MySQL从5.0版本开始支持存储过程,创建语法使用CREATE PROCEDURE语句,参数写在过程名后面的括号中,格式为“参数方向 参数名 数据类型”。下面通过一个员工查询的例子来演示IN和OUT参数的配合使用:传入部门编号,统计该部门的人数和平均工资,并通过OUT参数返回。
DELIMITER //
CREATE PROCEDURE get_dept_stat(
IN p_deptno INT, -- 输入参数:部门编号
OUT p_count INT, -- 输出参数:部门人数
OUT p_avg_sal DECIMAL(10,2) -- 输出参数:平均工资
)
BEGIN
SELECT COUNT(*), IFNULL(AVG(salary), 0)
INTO p_count, p_avg_sal
FROM employee
WHERE deptno = p_deptno;
END //
DELIMITER ;
上面的代码中有几个细节值得注意。首先,DELIMITER用于临时修改语句结束符,避免过程体中的分号被MySQL客户端提前截断;其次,SELECT ... INTO语句把查询结果直接赋给OUT参数,这是MySQL中最典型的结果回传方式。
调用存储过程需要使用CALL语句。由于OUT参数要接收返回值,调用前必须先准备好用户变量(变量名以@开头)来承接结果:
CALL get_dept_stat(20, @cnt, @avg); SELECT @cnt AS 部门人数, @avg AS 平均工资;
执行后,@cnt和@avg两个会话变量中就保存了过程返回的数据,可以直接用于后续查询或业务判断。需要提醒的是,如果忘记定义用户变量而直接传入常量作为OUT参数,MySQL会直接报错,这是新手最常踩的坑之一。
三、在Oracle中创建带参数的存储过程
Oracle的存储过程语法与MySQL类似,但有几个明显差异。Oracle的参数方向关键字可以省略,省略时默认为IN;此外Oracle不存在专门的CALL关键字场景差异,调用既可以用EXECUTE,也可以在PL/SQL块中直接引用。下面是等价的Oracle版本:
CREATE OR REPLACE PROCEDURE get_dept_stat(
p_deptno IN NUMBER, -- 输入参数
p_count OUT NUMBER, -- 输出参数:人数
p_avg_sal OUT NUMBER -- 输出参数:平均工资
)
AS
BEGIN
SELECT COUNT(*), NVL(AVG(salary), 0)
INTO p_count, p_avg_sal
FROM employee
WHERE deptno = p_deptno;
EXCEPTION
WHEN NO_DATA_FOUND THEN
p_count := 0;
p_avg_sal := 0;
END;
Oracle调用时需要在PL/SQL环境中声明变量来接收OUT值,示例如下:
DECLARE
v_count NUMBER;
v_avg NUMBER;
BEGIN
get_dept_stat(20, v_count, v_avg);
DBMS_OUTPUT.PUT_LINE('人数:' || v_count || ',平均工资:' || v_avg);
END;
可以看到,Oracle中调用存储过程不需要括号前加CALL,直接写过程名即可。另外Oracle允许在过程中加入异常处理块,当查询无数据时给OUT参数赋默认值,这样调用方拿到的永远是有效数字而不是不确定状态,这在生产环境中是值得推荐的做法。
四、INOUT参数的用法与常见坑点
INOUT参数集输入与输出于一身,典型场景是“传入一个原始值,经过处理后返回新值”,比如金额格式转换、字符串清洗、计数器累加等。以MySQL为例,实现一个翻倍计算器:
CREATE PROCEDURE double_value(INOUT p_num INT)
BEGIN
SET p_num = p_num * 2;
END;
SET @val = 100;
CALL double_value(@val);
SELECT @val; -- 结果为200
使用参数时还有几个容易出错的点需要留意。第一,MySQL的存储过程参数不支持设置默认值,调用时必须提供全部参数,不像SQL Server可以用等号给参数赋默认值;第二,OUT参数在过程开始执行时会被重置为NULL,不要指望传入的初始值能在过程里读到,这一点与INOUT参数完全不同;第三,传入OUT参数的变量类型要尽量与参数声明一致,避免出现隐式转换导致的精度丢失。
还有一种常见需求是返回多行结果。OUT参数适合返回单值或少量值,如果要返回结果集,MySQL中可以直接在过程内写普通SELECT语句,调用后即可获得结果集;Oracle则需要借助REF CURSOR游标类型的参数。选择哪种方式取决于业务复杂度:简单的状态返回用OUT参数最轻量,复杂的多结果集输出则要借助游标或临时表。
五、参数设计的最佳实践
在团队协作中,规范的参数设计能大幅降低维护成本。命名上建议给参数加统一前缀,比如IN参数用p_、局部变量用v_,这样在过程体内一眼就能区分哪个是外部传来的、哪个是内部计算的,避免出现把参数当局部变量随意覆盖的情况。
每个参数都应该写清注释,说明含义、取值范围和单位。尤其是OUT参数,调用方完全依赖文档或注释才能知道它返回什么,如果命名含糊(比如只叫result、flag),后续接手的人会非常痛苦。对于可能返回NULL的场景,建议在过程内部统一处理成明确的默认值,让接口行为可预期。
最后要控制参数数量。一个存储过程如果有七八个OUT参数,说明它的职责已经过重,更好的做法是拆分成多个小过程,或者改用返回结果集的方式。参数越少,接口越清晰,出错的概率也越低。掌握这些原则后,无论是写简单的统计过程还是复杂的业务封装,都能做到心中有数、调用不乱。