知识库✳

教程与手册

全部教程
  1. 01Oracle 函数手册
  2. 02性能优化手册
  3. 03Linux Shell
  4. 04Oracle 学习与演练
  5. 05网络基础学习
  6. 06数据治理与管理
返回个人网站
甲Oracle 学习教程从 SQL 到 PL/SQL 与运维
首页课程目录搜索演练场
知识库

已完成 0 / 22 章

模块一 · 入门与准备

  • 01Oracle 数据库体系结构
  • 02环境安装与连接方式
  • 03SQL*Plus 与常用工具

模块二 · SQL 查询基础

  • 04SELECT 查询基础
  • 05过滤与排序
  • 06单行函数
  • 07多表连接
  • 08分组聚合与 HAVING
  • 09子查询

模块三 · 数据与对象

  • 10DML 与事务控制
  • 11表与数据类型
  • 12约束
  • 13索引与执行计划
  • 14视图、序列与同义词

模块四 · PL/SQL 编程

  • 15PL/SQL 块与变量
  • 16流程控制与游标
  • 17存储过程与函数
  • 18触发器
  • 19异常处理

模块五 · 运维与进阶

  • 20用户与权限管理
  • 21表空间与存储管理
  • 22备份与恢复
❖

Oracle 学习教程

返回个人网站知识库

示例代码基于 Oracle AI Database 26ai / 23ai,查询示例使用官方 HR 示例方案。

以 Next.js 16 · Tailwind CSS v4 · HeroUI v3 装帧

17
  1. 首页
  2. 课程目录
  3. 存储过程与函数

第 17 章◆进阶◆建议 50 分钟◆模块四 · PL/SQL 编程

存储过程与函数

把可复用的逻辑封装成命名程序单元,掌握参数模式、函数与包的差异。

学完这一章,你应该能

  • 会创建存储过程与函数并调用
  • 分清 IN / OUT / IN OUT 参数
  • 理解函数与过程的选用标准
  • 会组织包(PACKAGE)来管理相关程序

17.1存储过程

带输出参数的调薪过程
CREATE OR REPLACE PROCEDURE raise_salary (
  p_dept_id  IN     NUMBER,
  p_percent  IN     NUMBER,
  p_count    OUT    NUMBER,
  p_total    OUT    NUMBER
) IS
BEGIN
  IF p_percent <= 0 OR p_percent > 0.5 THEN
    RAISE_APPLICATION_ERROR(-20001, '调薪比例必须在 0 到 0.5 之间');
  END IF;

  UPDATE employees
  SET    salary = salary * (1 + p_percent)
  WHERE  department_id = p_dept_id;

  p_count := SQL%ROWCOUNT;

  SELECT SUM(salary) INTO p_total
  FROM   employees
  WHERE  department_id = p_dept_id;
END raise_salary;
/

只作示例阅读:5 行写入限制下只开放单独创建普通空表;CTAS、删表、清空表、改结构、触发器和存储程序不允许执行。请在独立学习数据库中练习。

两种调用方式
-- 方式一:PL/SQL 块
DECLARE
  v_cnt NUMBER;
  v_sum NUMBER;
BEGIN
  raise_salary(60, 0.05, v_cnt, v_sum);
  DBMS_OUTPUT.PUT_LINE('调整 ' || v_cnt || ' 人,调整后部门工资总额 ' || v_sum);
  ROLLBACK;
END;
/

-- 方式二:SQL*Plus 绑定变量
VARIABLE b_cnt NUMBER
VARIABLE b_sum NUMBER
EXEC raise_salary(60, 0.05, :b_cnt, :b_sum)
PRINT b_cnt b_sum
ROLLBACK

17.2函数

函数必须有返回值,可以在 SQL 语句里直接调用(过程不行)。但 SQL 中调用的函数不能读写本条语句正在操作的表、不能改包状态;若确实要写库,必须声明自治事务(PRAGMA AUTONOMOUS_TRANSACTION),否则运行期报 ORA-14551。

计算年资的函数
CREATE OR REPLACE FUNCTION years_of_service (
  p_hire_date IN DATE
) RETURN NUMBER IS
  v_years NUMBER;
BEGIN
  IF p_hire_date IS NULL THEN
    RETURN NULL;
  END IF;

  v_years := TRUNC(MONTHS_BETWEEN(SYSDATE, p_hire_date) / 12);
  RETURN v_years;
END years_of_service;
/

只作示例阅读:5 行写入限制下只开放单独创建普通空表;CTAS、删表、清空表、改结构、触发器和存储程序不允许执行。请在独立学习数据库中练习。

在 SQL 里调用函数
SELECT employee_id,
       last_name,
       hire_date,
       years_of_service(hire_date) AS years
FROM   employees
WHERE  years_of_service(hire_date) >= 15
ORDER  BY years DESC;

注意:在 WHERE 里对列调用自定义函数会让该列上的索引失效(除非建函数索引)。数据量大时慎用。

只作示例阅读:受限模式不允许调用「YEARS_OF_SERVICE」:用户函数或程序可能在查询中写入数据。请在独立学习数据库中练习。

17.3过程与函数怎么选

  • 需要在 SQL 语句中调用 → 只能用函数
  • 需要返回多个值 → 用过程加 OUT 参数,或返回游标
  • 需要修改数据 → 优先用过程;函数要在 SQL 中写库必须用自治事务,并自行承担一致性后果
  • 纯计算、无副作用 → 用函数,更容易复用和测试
  • 逻辑复杂、多个程序共享 → 用包组织

17.4包:把相关程序组织起来

  • 包规范先编译,包体后编译;改包体不影响依赖对象的有效性
  • 包内的私有过程/函数只在包体里声明,外部无法调用
  • 包级变量在会话内保持状态,可用于缓存,但要注意会话隔离
包规范与包体
-- 包规范:对外接口
CREATE OR REPLACE PACKAGE pkg_emp AS
  g_min_salary CONSTANT NUMBER := 3000;       -- 公共常量

  FUNCTION  get_avg_salary (p_dept_id IN NUMBER) RETURN NUMBER;
  PROCEDURE adjust_salary  (p_dept_id IN NUMBER, p_percent IN NUMBER);
END pkg_emp;
/

-- 包体:具体实现
CREATE OR REPLACE PACKAGE BODY pkg_emp AS

  FUNCTION get_avg_salary (p_dept_id IN NUMBER) RETURN NUMBER IS
    v_avg NUMBER;
  BEGIN
    SELECT ROUND(AVG(salary), 2) INTO v_avg
    FROM   employees WHERE department_id = p_dept_id;
    RETURN NVL(v_avg, 0);
  END get_avg_salary;

  PROCEDURE adjust_salary (p_dept_id IN NUMBER, p_percent IN NUMBER) IS
  BEGIN
    UPDATE employees
    SET    salary = GREATEST(salary * (1 + p_percent), g_min_salary)
    WHERE  department_id = p_dept_id;
  END adjust_salary;

END pkg_emp;
/

-- 调用
SELECT pkg_emp.get_avg_salary(60) FROM dual;

只作示例阅读:5 行写入限制下只开放单独创建普通空表;CTAS、删表、清空表、改结构、触发器和存储程序不允许执行。请在独立学习数据库中练习。

练习

3 题 · 先自己写,再展开对照

参考答案

必须用函数:需要在 SQL 语句里直接调用时(SELECT 列里、WHERE 里)。
过程无法出现在 SQL 语句中。

应该用过程:
- 需要返回多个值(用多个 OUT 参数)或返回结果集
- 需要修改数据(函数在 SQL 中改数据受很大限制,见下一题)
- 逻辑主要是「做一件事」而不是「算一个值」

实际判断标准很简单:它是不是要产生一个值给 SQL 用。
是就用函数,不是就用过程。

自评要点

  • SQL 中调用只能用函数
  • 多返回值 / 改数据优先过程

参考答案

不能直接改。在 SQL 语句中调用的函数如果执行 DML,运行期会报:

ORA-14551: cannot perform a DML operation inside a query

而且在 SQL 中调用的函数还不能读写本条语句正在操作的表、不能改包状态。

确实需要写库时,要声明自治事务:
  PRAGMA AUTONOMOUS_TRANSACTION;
并在函数内显式 COMMIT / ROLLBACK。自治事务独立于主事务提交,
所以要自己承担一致性后果(典型用途是记日志,且不希望日志随主事务回滚)。

补充:老资料里「函数必须满足纯度规则(RNDS/WNDS 等)」是 9i 及更早的
说法,11g 起默认按实际行为判断,不要被它误导。

自评要点

  • 知道会报 ORA-14551
  • 知道自治事务是唯一出路
  • 知道纯度规则是过时说法

参考答案

1. 接口与实现分离:包规范(PACKAGE)只暴露对外接口,
   包体(PACKAGE BODY)里的私有过程和函数外部看不到。

2. 改包体不影响依赖对象的有效性:只修改包体实现时,
   引用这个包的存储过程、视图不会变成 INVALID,减少级联重编译。

3. 便于组织:相关逻辑集中在一处,命名不会互相冲突。

4. 可以定义包级变量和常量,在会话内保持状态(注意会话隔离)。

代价:包级变量会让会话持有状态,长时间存在的连接里要小心读到自己的旧状态。

自评要点

  • 接口/实现分离,可隐藏私有成员
  • 改包体不使依赖对象失效
← 上一章

流程控制与游标

下一章 →

触发器