Oracle PL/SQL 存储过程与函数完全指南
存储过程(Procedure)和函数(Function)是 PL/SQL 的核心可执行单元,用于封装业务逻辑、提升性能、增强安全性和代码复用。
一、核心概念与区别
1.1 存储过程 vs 函数
| 特性 | 存储过程 (Procedure) | 函数 (Function) |
|---|---|---|
| 返回值 | 通过 OUT 参数返回,可返回多个值 |
必须返回单个值(通过 RETURN) |
| 调用方式 | EXECUTE/CALL 或 PL/SQL 块中调用 |
可在 SQL 语句中直接调用 |
| 用途 | 执行操作(插入、更新、批量处理) | 计算并返回值(如公式、转换) |
| 事务控制 | 可包含 COMMIT/ROLLBACK |
通常不包含事务控制 |
| 性能 | 适合复杂业务逻辑 | 适合计算密集型操作 |
1.2 基本语法结构
sql
-- 存储过程语法
CREATE [OR REPLACE] PROCEDURE 过程名 (
参数1 [模式] 数据类型,
参数2 [模式] 数据类型
) [AUTHID {DEFINER | CURRENT_USER}] -- 权限模型
IS|AS
-- 声明部分(变量、游标、类型)
变量声明;
BEGIN
-- 执行部分
可执行语句;
EXCEPTION
-- 异常处理部分
异常处理;
END [过程名];
/
-- 函数语法
CREATE [OR REPLACE] FUNCTION 函数名 (
参数1 [模式] 数据类型
) RETURN 返回数据类型
IS|AS
变量声明;
BEGIN
RETURN 返回值;
EXCEPTION
异常处理;
END [函数名];
/
二、创建与调用
2.1 创建存储过程
sql
-- 场景:调整员工薪水并记录日志
CREATE OR REPLACE PROCEDURE adjust_employee_salary (
p_emp_id IN NUMBER, -- 员工ID(输入参数)
p_percent IN NUMBER, -- 调整百分比(输入参数)
p_new_salary OUT NUMBER, -- 新薪水(输出参数)
p_message OUT VARCHAR2 -- 消息(输出参数)
) AUTHID DEFINER
IS
-- 声明变量
v_old_salary employees.salary%TYPE;
v_emp_name VARCHAR2(100);
BEGIN
-- 1. 查询当前薪水
SELECT salary, first_name || ' ' || last_name
INTO v_old_salary, v_emp_name
FROM employees
WHERE employee_id = p_emp_id;
-- 2. 计算新薪水
p_new_salary := v_old_salary * (1 + p_percent / 100);
-- 3. 更新薪水
UPDATE employees
SET salary = p_new_salary
WHERE employee_id = p_emp_id;
-- 4. 记录日志
INSERT INTO salary_log (emp_id, old_salary, new_salary, change_date)
VALUES (p_emp_id, v_old_salary, p_new_salary, SYSDATE);
-- 5. 设置返回消息
p_message := '员工 ' || v_emp_name || ' 薪水已从 ' || v_old_salary || ' 调整为 ' || p_new_salary;
-- 6. 提交事务
COMMIT;
EXCEPTION
WHEN NO_DATA_FOUND THEN
p_message := '错误:员工ID ' || p_emp_id || ' 不存在';
ROLLBACK;
WHEN OTHERS THEN
p_message := '错误:' || SQLERRM;
ROLLBACK;
END adjust_employee_salary;
/
2.2 调用存储过程
sql
-- 方式1:匿名块调用
DECLARE
v_new_salary NUMBER;
v_message VARCHAR2(200);
BEGIN
adjust_employee_salary(p_emp_id => 101,
p_percent => 10,
p_new_salary => v_new_salary,
p_message => v_message);
DBMS_OUTPUT.PUT_LINE(v_message);
DBMS_OUTPUT.PUT_LINE('新薪水:' || v_new_salary);
END;
/
-- 方式2:EXECUTE 命令(SQL*Plus)
VARIABLE new_salary NUMBER
VARIABLE message VARCHAR2(200)
EXEC adjust_employee_salary(101, 10, :new_salary, :message);
PRINT new_salary
PRINT message
-- 方式3:JDBC 调用(Java)
CallableStatement cstmt = conn.prepareCall("{call adjust_employee_salary(?, ?, ?, ?)}");
cstmt.setInt(1, 101);
cstmt.setDouble(2, 10);
cstmt.registerOutParameter(3, Types.NUMERIC);
cstmt.registerOutParameter(4, Types.VARCHAR);
cstmt.execute();
double newSalary = cstmt.getDouble(3);
String message = cstmt.getString(4);
三、参数模式详解
3.1 IN 模式(默认)
sql
-- 输入参数,只读
CREATE PROCEDURE process_order (
p_order_id IN NUMBER, -- 输入订单ID
p_status OUT VARCHAR2
) IS
BEGIN
-- p_order_id 可被读取,但不能被修改
UPDATE orders SET status = 'PROCESSING' WHERE order_id = p_order_id;
p_status := '处理完成';
END;
/
3.2 OUT 模式
sql
-- 输出参数,用于返回值
CREATE PROCEDURE get_employee_info (
p_emp_id IN NUMBER,
p_name OUT VARCHAR2,
p_salary OUT NUMBER,
p_hire_date OUT DATE
) IS
BEGIN
SELECT first_name || ' ' || last_name, salary, hire_date
INTO p_name, p_salary, p_hire_date
FROM employees
WHERE employee_id = p_emp_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
p_name := NULL;
p_salary := NULL;
p_hire_date := NULL;
END;
/
3.3 IN OUT 模式
sql
-- 输入输出参数,既可读也可写
CREATE PROCEDURE swap_values (
p_value1 IN OUT NUMBER,
p_value2 IN OUT NUMBER
) IS
v_temp NUMBER;
BEGIN
v_temp := p_value1;
p_value1 := p_value2;
p_value2 := v_temp;
END;
/
-- 调用
DECLARE
a NUMBER := 10;
b NUMBER := 20;
BEGIN
DBMS_OUTPUT.PUT_LINE('交换前:a=' || a || ', b=' || b);
swap_values(a, b);
DBMS_OUTPUT.PUT_LINE('交换后:a=' || a || ', b=' || b);
END;
/
3.4 参数默认值
sql
CREATE OR REPLACE PROCEDURE create_employee (
p_first_name IN VARCHAR2,
p_last_name IN VARCHAR2,
p_salary IN NUMBER DEFAULT 5000, -- 默认值
p_department_id IN NUMBER DEFAULT 50
) IS
BEGIN
INSERT INTO employees (employee_id, first_name, last_name, salary, department_id)
VALUES (emp_seq.NEXTVAL, p_first_name, p_last_name, p_salary, p_department_id);
COMMIT;
END;
/
-- 调用(使用默认值)
EXEC create_employee('John', 'Doe');
-- 调用(覆盖默认值)
EXEC create_employee('Jane', 'Smith', 8000, 60);
四、创建与调用函数
4.1 创建函数
sql
-- 场景:根据员工ID计算年薪(含奖金)
CREATE OR REPLACE FUNCTION calculate_annual_income (
p_emp_id IN NUMBER,
p_include_bonus IN BOOLEAN DEFAULT TRUE
) RETURN NUMBER
IS
v_salary employees.salary%TYPE;
v_commission employees.commission_pct%TYPE;
v_annual_income NUMBER;
BEGIN
-- 查询薪水和提成比例
SELECT salary, commission_pct
INTO v_salary, v_commission
FROM employees
WHERE employee_id = p_emp_id;
-- 计算年收入
IF p_include_bonus AND v_commission IS NOT NULL THEN
v_annual_income := v_salary * 12 * (1 + v_commission);
ELSE
v_annual_income := v_salary * 12;
END IF;
RETURN v_annual_income;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN NULL; -- 函数必须返回值
WHEN OTHERS THEN
RETURN -1; -- 错误标识
END calculate_annual_income;
/
4.2 调用函数
sql
-- 方式1:在 SQL 语句中调用(函数的核心优势)
SELECT
employee_id,
first_name,
salary,
calculate_annual_income(employee_id, TRUE) AS annual_income
FROM employees
WHERE calculate_annual_income(employee_id) > 200000;
-- 方式2:在 PL/SQL 块中调用
DECLARE
v_income NUMBER;
BEGIN
v_income := calculate_annual_income(101, FALSE);
DBMS_OUTPUT.PUT_LINE('年收入:' || v_income);
END;
/
-- 方式3:在 WHERE 子句中调用
SELECT * FROM employees
WHERE calculate_annual_income(employee_id) > (SELECT AVG(salary*12) FROM employees);
五、高级特性
5.1 异常处理(Exception Handling)
sql
-- 预定义异常
CREATE OR REPLACE PROCEDURE safe_delete_employee (
p_emp_id IN NUMBER
) IS
BEGIN
DELETE FROM employees WHERE employee_id = p_emp_id;
IF SQL%NOTFOUND THEN
RAISE_APPLICATION_ERROR(-20001, '员工 ' || p_emp_id || ' 不存在');
END IF;
COMMIT;
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('没有找到数据');
WHEN TOO_MANY_ROWS THEN
DBMS_OUTPUT.PUT_LINE('返回多行,但期望单行');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('错误代码:' || SQLCODE);
DBMS_OUTPUT.PUT_LINE('错误消息:' || SQLERRM);
ROLLBACK;
END;
/
-- 自定义异常
CREATE OR REPLACE PROCEDURE update_salary_check (
p_emp_id IN NUMBER,
p_new_salary IN NUMBER
) IS
e_salary_too_high EXCEPTION; -- 声明自定义异常
PRAGMA EXCEPTION_INIT(e_salary_too_high, -20002); -- 关联错误码
BEGIN
IF p_new_salary > 20000 THEN
RAISE e_salary_too_high; -- 抛出自定义异常
END IF;
UPDATE employees SET salary = p_new_salary WHERE employee_id = p_emp_id;
COMMIT;
EXCEPTION
WHEN e_salary_too_high THEN
DBMS_OUTPUT.PUT_LINE('错误:新薪资不能超过20000');
ROLLBACK;
END;
/
5.2 游标(Cursor)
sql
-- 显式游标处理多行数据
CREATE OR REPLACE PROCEDURE bulk_raise_salary (
p_dept_id IN NUMBER,
p_percent IN NUMBER
) IS
-- 声明游标
CURSOR emp_cursor IS
SELECT employee_id, salary FROM employees
WHERE department_id = p_dept_id
FOR UPDATE; -- 加锁防止并发修改
-- 记录类型
emp_rec emp_cursor%ROWTYPE;
BEGIN
OPEN emp_cursor;
LOOP
FETCH emp_cursor INTO emp_rec;
EXIT WHEN emp_cursor%NOTFOUND; -- 退出循环条件
-- 更新薪水
UPDATE employees
SET salary = emp_rec.salary * (1 + p_percent/100)
WHERE CURRENT OF emp_cursor; -- 定位当前游标行
DBMS_OUTPUT.PUT_LINE('员工 ' || emp_rec.employee_id || ' 已调整');
END LOOP;
CLOSE emp_cursor;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
CLOSE emp_cursor;
ROLLBACK;
RAISE;
END;
/
-- 游标 FOR 循环(简化)
CREATE OR REPLACE PROCEDURE process_high_earners IS
BEGIN
FOR emp_rec IN (SELECT employee_id, salary FROM employees WHERE salary > 10000)
LOOP
INSERT INTO high_earner_log VALUES (emp_rec.employee_id, emp_rec.salary, SYSDATE);
END LOOP;
COMMIT;
END;
/
5.3 自治事务(Autonomous Transaction)
sql
-- 日志记录不受主事务影响
CREATE OR REPLACE PROCEDURE log_message (
p_message IN VARCHAR2
) IS
PRAGMA AUTONOMOUS_TRANSACTION; -- 声明自治事务
BEGIN
INSERT INTO message_log (message, log_time) VALUES (p_message, SYSDATE);
COMMIT; -- 独立提交,不影响主事务
END;
/
-- 主事务回滚,但日志已提交
CREATE OR REPLACE PROCEDURE main_transaction IS
BEGIN
INSERT INTO orders VALUES (101, 5000);
log_message('订单 101 已创建'); -- 自治事务已提交
ROLLBACK; -- 订单被回滚,但日志保留
END;
/
5.4 动态 SQL(EXECUTE IMMEDIATE)
sql
-- 场景:动态表名查询
CREATE OR REPLACE FUNCTION dynamic_query (
p_table_name IN VARCHAR2,
p_id IN NUMBER
) RETURN VARCHAR2
IS
v_sql VARCHAR2(1000);
v_result VARCHAR2(100);
BEGIN
v_sql := 'SELECT name FROM ' || p_table_name || ' WHERE id = :id';
EXECUTE IMMEDIATE v_sql
INTO v_result
USING p_id; -- 绑定变量防止 SQL 注入
RETURN v_result;
EXCEPTION
WHEN OTHERS THEN
RETURN '查询失败:' || SQLERRM;
END;
/
-- 动态 DDL
CREATE OR REPLACE PROCEDURE create_log_table (p_table_name IN VARCHAR2) IS
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE ' || p_table_name || '_log (
id NUMBER GENERATED ALWAYS AS IDENTITY,
message VARCHAR2(200),
log_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)';
END;
/
六、包(Package)------代码封装
6.1 包的创建(规范 + 主体)
sql
-- 1. 包规范(接口定义)
CREATE OR REPLACE PACKAGE employee_mgmt AS
-- 常量
c_max_salary CONSTANT NUMBER := 50000;
-- 类型定义
TYPE emp_rec_type IS RECORD (
emp_id NUMBER,
emp_name VARCHAR2(100),
salary NUMBER
);
TYPE emp_tab_type IS TABLE OF emp_rec_type INDEX BY PLS_INTEGER;
-- 函数声明
FUNCTION calculate_bonus (p_emp_id IN NUMBER) RETURN NUMBER;
FUNCTION get_employee_info (p_emp_id IN NUMBER) RETURN emp_rec_type;
-- 过程声明
PROCEDURE hire_employee (
p_first_name IN VARCHAR2,
p_last_name IN VARCHAR2,
p_salary IN NUMBER
);
PROCEDURE fire_employee (p_emp_id IN NUMBER);
END employee_mgmt;
/
-- 2. 包主体(实现)
CREATE OR REPLACE PACKAGE BODY employee_mgmt AS
-- 私有函数(外部不可见)
FUNCTION validate_salary (p_salary IN NUMBER) RETURN BOOLEAN IS
BEGIN
RETURN p_salary BETWEEN 1000 AND c_max_salary;
END validate_salary;
-- 公有函数实现
FUNCTION calculate_bonus (p_emp_id IN NUMBER) RETURN NUMBER IS
v_salary employees.salary%TYPE;
BEGIN
SELECT salary INTO v_salary FROM employees WHERE employee_id = p_emp_id;
RETURN v_salary * 0.1; -- 奖金为薪水的10%
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN 0;
END calculate_bonus;
-- 存储过程实现
PROCEDURE hire_employee (
p_first_name IN VARCHAR2,
p_last_name IN VARCHAR2,
p_salary IN NUMBER
) IS
BEGIN
IF NOT validate_salary(p_salary) THEN
RAISE_APPLICATION_ERROR(-20003, '薪资超出范围');
END IF;
INSERT INTO employees (employee_id, first_name, last_name, salary, hire_date)
VALUES (emp_seq.NEXTVAL, p_first_name, p_last_name, p_salary, SYSDATE);
COMMIT;
END hire_employee;
END employee_mgmt;
/
6.2 调用包内程序
sql
-- 调用包函数
SELECT employee_mgmt.calculate_bonus(101) FROM dual;
-- 调用包过程
DECLARE
v_emp_info employee_mgmt.emp_rec_type;
BEGIN
v_emp_info := employee_mgmt.get_employee_info(102);
DBMS_OUTPUT.PUT_LINE('姓名:' || v_emp_info.emp_name);
employee_mgmt.hire_employee('Alice', 'Smith', 7500);
END;
/
6.3 包的优势
- 模块化:逻辑分组,代码组织清晰
- 性能:首次加载后常驻内存,后续调用更快
- 封装:公有/私有分离,隐藏实现细节
- 状态保持:包变量在会话中持续存在
- 重载:支持同名过程/函数(参数不同)
七、性能优化技巧
7.1 使用 BULK COLLECT 批量操作
sql
-- 错误:逐行处理(慢)
CREATE OR REPLACE PROCEDURE slow_update IS
BEGIN
FOR emp IN (SELECT employee_id, salary FROM employees WHERE department_id = 80)
LOOP
UPDATE employees SET salary = salary * 1.1 WHERE employee_id = emp.employee_id;
END LOOP;
COMMIT;
END;
-- 正确:批量处理
CREATE OR REPLACE PROCEDURE fast_update IS
TYPE num_tab IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
v_emp_ids num_tab;
v_salaries num_tab;
BEGIN
-- 批量获取
SELECT employee_id, salary
BULK COLLECT INTO v_emp_ids, v_salaries
FROM employees WHERE department_id = 80;
-- 批量更新
FORALL i IN 1..v_emp_ids.COUNT
UPDATE employees SET salary = v_salaries(i) * 1.1
WHERE employee_id = v_emp_ids(i);
COMMIT;
END;
7.2 使用 NOCOPY 提示(减少参数复制开销)
sql
-- 对于大集合,使用 NOCOPY 避免拷贝
CREATE OR REPLACE PROCEDURE process_large_collection (
p_collection IN OUT NOCOPY large_collection_type -- NOCOPY 提示
) IS
BEGIN
-- 直接操作原集合,不创建副本
FOR i IN 1..p_collection.COUNT LOOP
p_collection(i).status := 'PROCESSED';
END LOOP;
END;
7.3 避免上下文切换
sql
-- 错误:SQL 和 PL/SQL 频繁切换
CREATE OR REPLACE FUNCTION get_department_name (p_dept_id NUMBER) RETURN VARCHAR2 IS
v_name VARCHAR2(100);
BEGIN
SELECT department_name INTO v_name FROM departments WHERE department_id = p_dept_id;
RETURN v_name;
END;
-- 查询时使用
SELECT employee_id, get_department_name(department_id) FROM employees; -- 低效
-- 正确:纯 SQL 实现
SELECT e.employee_id, d.department_name
FROM employees e JOIN departments d ON e.department_id = d.department_id;
八、调试与监控
8.1 DBMS_OUTPUT 调试
sql
SET SERVEROUTPUT ON; -- 开启输出
CREATE OR REPLACE PROCEDURE debug_demo IS
v_counter NUMBER := 0;
BEGIN
FOR rec IN (SELECT employee_id FROM employees WHERE ROWNUM <= 5)
LOOP
v_counter := v_counter + 1;
DBMS_OUTPUT.PUT_LINE('处理第 ' || v_counter || ' 个员工:' || rec.employee_id);
END LOOP;
END;
/
8.2 使用 DBMS_APPLICATION_INFO
sql
-- 在 V$SESSION 中显示进度
CREATE OR REPLACE PROCEDURE long_running_task IS
v_total NUMBER;
BEGIN
SELECT COUNT(*) INTO v_total FROM employees;
FOR rec IN (SELECT employee_id FROM employees)
LOOP
DBMS_APPLICATION_INFO.SET_MODULE(
module_name => 'SALARY_UPDATE',
action_name => 'Processing ' || rec.employee_id
);
DBMS_APPLICATION_INFO.SET_SESSION_LONGOPS(
rindex => DBMS_APPLICATION_INFO.SET_SESSION_LONGOPS_NOHINT,
slno => 0,
op_name => 'Employee Processing',
sofar => rec.employee_id,
totalwork => v_total,
units => 'employees'
);
-- 业务逻辑
UPDATE employees SET salary = salary * 1.05 WHERE employee_id = rec.employee_id;
END LOOP;
END;
/
-- 监控查询
SELECT sid, serial#, module, action FROM v$session WHERE module = 'SALARY_UPDATE';
SELECT * FROM v$session_longops WHERE opname = 'Employee Processing';
8.3 依赖关系查询
sql
-- 查看存储过程依赖的表
SELECT referenced_owner, referenced_name, referenced_type
FROM all_dependencies
WHERE owner = 'HR'
AND name = 'ADJUST_EMPLOYEE_SALARY'
AND type = 'PROCEDURE'
ORDER BY referenced_type;
-- 查看哪些对象依赖该过程
SELECT name, type
FROM all_dependencies
WHERE referenced_owner = 'HR'
AND referenced_name = 'ADJUST_EMPLOYEE_SALARY';
九、最佳实践与规范
9.1 命名规范
sql
-- 前缀规范
- 存储过程:p_业务模块_操作(如 p_emp_update_salary)
- 函数:f_业务模块_计算(如 f_emp_calc_bonus)
- 包:pkg_业务模块(如 pkg_employee_mgmt)
- 参数:p_参数名(输入)、p_参数名_out(输出)、p_参数名_io(输入输出)
- 变量:v_变量名(局部)、g_变量名(全局包变量)
9.2 编码规范
sql
-- 1. 总是使用 AUTHID 明确权限
CREATE OR REPLACE PROCEDURE secure_proc(...) IS
AUTHID CURRENT_USER -- 调用者权限
IS
BEGIN
...
END;
/
-- 2. 参数使用 %TYPE 锚定
CREATE OR REPLACE PROCEDURE update_emp (
p_emp_id IN employees.employee_id%TYPE, -- 类型自动同步
p_salary IN employees.salary%TYPE
) IS ...
-- 3. 使用显式游标而非隐式
-- 错误:隐式游标无法处理 NO_DATA_FOUND
SELECT ... INTO ...; -- 不推荐
-- 正确:显式游标控制
DECLARE
CURSOR c_emp IS SELECT ...;
BEGIN
OPEN c_emp;
LOOP
FETCH c_emp INTO ...;
EXIT WHEN c_emp%NOTFOUND;
END LOOP;
CLOSE c_emp;
END;
-- 4. 异常处理精细化
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- 处理查询不到数据
WHEN DUP_VAL_ON_INDEX THEN
-- 处理唯一键冲突
WHEN OTHERS THEN
-- 记录错误日志后重新抛出
log_error(SQLCODE, SQLERRM);
RAISE; -- 重新抛出,让上层调用者处理
9.3 性能黄金法则
✅ 批量操作替代逐行处理 (FORALL)
✅ 避免游标循环中的 SQL (先 JOIN 再处理)
✅ 使用 NOCOPY 减少大集合拷贝
✅ SQL 能做的事不要放在 PL/SQL 中
✅ 使用 DBMS_PROFILER 定位性能瓶颈
❌ 避免在函数中执行 DML (SQL 调用时会导致上下文切换)
❌ 避免过度使用自治事务(破坏事务原子性)
十、总结对比
存储过程 vs 函数终极对比
| 维度 | 存储过程 | 函数 |
|---|---|---|
| 返回值 | 0 或多个(OUT 参数) | 必须 1 个(RETURN) |
| SQL 调用 | ❌ 不可 | ✅ 可在 SELECT/ WHERE 中调用 |
| 事务控制 | ✅ 可 COMMIT/ROLLBACK | ❌ 应避免(非确定性) |
| 副作用 | ✅ 可修改数据 | ⚠️ 应保持纯计算 |
| 性能 | 适合复杂业务逻辑 | 适合计算和转换 |
| 调试 | 较难(无 RETURN) | 较易(可单元测试) |
| 使用场景 | 批处理、ETL、API 封装 | 公式、验证、数据转换 |
选择原则:
- 需要返回多个值 → 存储过程
- 需要在 SQL 中使用 → 函数
- 需要修改数据 → 存储过程(函数也可但应避免)
- 纯计算逻辑 → 函数(保持确定性)
掌握存储过程和函数,是 Oracle 后端开发的核心技能。它们能将业务逻辑下沉到数据库层,提升性能、安全性和可维护性。