Oracle 19c入门学习教程,从入门到精通,PL/SQL 编程详解:语法、使用方法与综合案例(6)

PL/SQL 编程详解:语法、使用方法与综合案例

PL/SQL(Procedural Language/SQL)是 Oracle 数据库专有的过程化编程语言,它在 SQL 的基础上增加了变量、控制结构、异常处理等程序设计能力,适用于编写存储过程、函数、触发器等数据库逻辑。


一、PL/SQL 简介

1. 什么是 PL/SQL?

  • 过程化扩展:在标准 SQL 基础上加入条件判断、循环、变量等。
  • 块结构:程序由一个或多个"块"(Block)组成。
  • 高效执行:可在数据库服务器端运行,减少网络传输。
  • 强类型语言:变量需声明数据类型。

2. PL/SQL 优势

  • 支持事务控制(COMMIT/ROLLBACK)
  • 异常处理机制完善
  • 可调用 SQL 函数和系统包(如 DBMS_OUTPUT)
  • 与 Oracle 深度集成

二、安装与运行环境准备

前提:已安装 Oracle Database(如 19c/21c)或 Oracle Express Edition (XE)

1. 启用 SCOTT 用户(用于练习)

sql 复制代码
-- 以 sysdba 登录
CONNECT / AS SYSDBA

-- 解锁并设置密码
ALTER USER scott ACCOUNT UNLOCK IDENTIFIED BY tiger;

-- 连接 scott
CONNECT scott/tiger

2. 启用 DBMS_OUTPUT(用于调试输出)

sql 复制代码
-- 在 SQL*Plus 或 SQL Developer 中执行
SET SERVEROUTPUT ON;

✅ 推荐工具:

  • SQL*Plus(命令行)
  • Oracle SQL Developer(图形界面,免费)
  • VS Code + Oracle 插件

三、PL/SQL 块结构

PL/SQL 程序由 匿名块命名块(过程、函数)组成。

1. 匿名块语法

plsql 复制代码
[DECLARE
  -- 声明部分:变量、常量、游标等
]
BEGIN
  -- 执行部分:SQL 和 PL/SQL 语句
[EXCEPTION
  -- 异常处理部分
]
END;
/

⚠️ 注意:匿名块必须以 / 结尾(在 SQL*Plus 中)才能执行。

2. 示例:最简单的 PL/SQL 块

plsql 复制代码
-- 输出 "Hello, PL/SQL!"
BEGIN
  DBMS_OUTPUT.PUT_LINE('Hello, PL/SQL!');
END;
/

DBMS_OUTPUT.PUT_LINE 是 Oracle 内置过程,用于向控制台输出信息。


四、代码注释和标识符

1. 注释方式

plsql 复制代码
-- 单行注释

/*
  多行注释
  可跨越多行
*/

2. 标识符规则

  • 以字母开头
  • 可包含字母、数字、下划线 _、美元符 $、井号 #
  • 最大长度 30 字符(Oracle 12c 之前),128 字符(12c+)
  • 不区分大小写(但建议统一风格)

✅ 推荐命名规范:

  • 变量:v_employee_id
  • 常量:c_max_salary
  • 游标:cur_emp

五、文本(字符串字面量)

  • 用单引号 ' 包围
  • 单引号本身用两个单引号表示:'It''s OK'
plsql 复制代码
DECLARE
  v_msg VARCHAR2(100) := 'He said: ''Hello!''';
BEGIN
  DBMS_OUTPUT.PUT_LINE(v_msg);  -- 输出:He said: 'Hello!'
END;
/

六、数据类型、变量和常量

1. 基本数据类型

类型 说明 示例
NUMBER(p,s) 数值 NUMBER(10,2)
VARCHAR2(n) 可变长字符串 VARCHAR2(50)
CHAR(n) 定长字符串(空格填充) CHAR(10)
DATE 日期时间 SYSDATE
BOOLEAN 布尔值(仅 PL/SQL 内部使用) TRUE/FALSE

2. 特殊数据类型

类型 说明
%TYPE 引用表列的数据类型
%ROWTYPE 引用整行记录的结构
RECORD 自定义复合类型
TABLE PL/SQL 表(类似数组)

3. 定义变量和常量

plsql 复制代码
DECLARE
  -- 基本变量
  v_emp_id    NUMBER(4) := 7369;
  v_name      VARCHAR2(20);
  v_hire_date DATE := SYSDATE;

  -- 使用 %TYPE(推荐!避免硬编码类型)
  v_salary    emp.sal%TYPE;  -- 自动匹配 emp 表 sal 列类型

  -- 常量(必须初始化,且不能修改)
  c_pi        CONSTANT NUMBER := 3.14159;
  c_company   CONSTANT VARCHAR2(20) := 'Oracle Corp';

  -- 布尔变量
  v_is_manager BOOLEAN := FALSE;
BEGIN
  -- 从表中查询赋值
  SELECT ename, sal INTO v_name, v_salary
  FROM emp
  WHERE empno = v_emp_id;

  DBMS_OUTPUT.PUT_LINE('Employee: ' || v_name || ', Salary: ' || v_salary);
END;
/

✅ 优势:使用 %TYPE 可使代码随表结构自动适应,提高可维护性。


七、PL/SQL 表达式

支持算术、比较、逻辑、字符串连接等操作。

plsql 复制代码
DECLARE
  v_a NUMBER := 10;
  v_b NUMBER := 3;
  v_result NUMBER;
BEGIN
  v_result := v_a + v_b * 2;        -- 16
  v_result := MOD(v_a, v_b);        -- 1
  v_result := POWER(v_a, 2);        -- 100

  IF v_a > v_b AND v_b != 0 THEN
    DBMS_OUTPUT.PUT_LINE('Valid calculation');
  END IF;
END;
/

八、流程控制语句

1. 选择语句

(1) IF-THEN
plsql 复制代码
IF condition THEN
  statements;
END IF;
(2) IF-THEN-ELSE
plsql 复制代码
IF salary > 5000 THEN
  bonus := 1000;
ELSE
  bonus := 500;
END IF;
(3) IF-THEN-ELSIF-ELSE
plsql 复制代码
DECLARE
  v_grade CHAR(1) := 'B';
BEGIN
  IF v_grade = 'A' THEN
    DBMS_OUTPUT.PUT_LINE('Excellent');
  ELSIF v_grade = 'B' THEN
    DBMS_OUTPUT.PUT_LINE('Good');
  ELSIF v_grade = 'C' THEN
    DBMS_OUTPUT.PUT_LINE('Fair');
  ELSE
    DBMS_OUTPUT.PUT_LINE('Unknown grade');
  END IF;
END;
/
(4) CASE 语句(Oracle 9i+)
plsql 复制代码
CASE v_deptno
  WHEN 10 THEN DBMS_OUTPUT.PUT_LINE('Accounting');
  WHEN 20 THEN DBMS_OUTPUT.PUT_LINE('Research');
  WHEN 30 THEN DBMS_OUTPUT.PUT_LINE('Sales');
  ELSE DBMS_OUTPUT.PUT_LINE('Other Dept');
END CASE;

2. 循环语句

(1) LOOP ... EXIT
plsql 复制代码
DECLARE
  i NUMBER := 1;
BEGIN
  LOOP
    DBMS_OUTPUT.PUT_LINE('Count: ' || i);
    i := i + 1;
    EXIT WHEN i > 5;
  END LOOP;
END;
/
(2) WHILE LOOP
plsql 复制代码
DECLARE
  i NUMBER := 1;
BEGIN
  WHILE i <= 5 LOOP
    DBMS_OUTPUT.PUT_LINE('While: ' || i);
    i := i + 1;
  END LOOP;
END;
/
(3) FOR LOOP(最常用)
plsql 复制代码
BEGIN
  FOR i IN 1..5 LOOP
    DBMS_OUTPUT.PUT_LINE('For loop: ' || i);
  END LOOP;

  -- 反向循环
  FOR i IN REVERSE 1..3 LOOP
    DBMS_OUTPUT.PUT_LINE('Reverse: ' || i);
  END LOOP;
END;
/

九、PL/SQL 游标(Cursor)

游标用于处理多行查询结果。

1. 基本原理

  • 隐式游标:Oracle 自动为 DML 语句(INSERT/UPDATE/DELETE)创建。
  • 显式游标:程序员手动声明,用于 SELECT 多行。

2. 显式游标(四步法:声明 → 打开 → 获取 → 关闭)

plsql 复制代码
DECLARE
  -- 1. 声明游标
  CURSOR cur_emp IS
    SELECT empno, ename, sal FROM emp WHERE deptno = 20;

  -- 声明记录变量
  v_emp_rec cur_emp%ROWTYPE;
BEGIN
  -- 2. 打开游标
  OPEN cur_emp;

  -- 3. 循环获取数据
  LOOP
    FETCH cur_emp INTO v_emp_rec;
    EXIT WHEN cur_emp%NOTFOUND;  -- 无更多数据时退出

    DBMS_OUTPUT.PUT_LINE(
      'ID: ' || v_emp_rec.empno ||
      ', Name: ' || v_emp_rec.ename ||
      ', Salary: ' || v_emp_rec.sal
    );
  END LOOP;

  -- 4. 关闭游标
  CLOSE cur_emp;
END;
/

3. 隐式游标(SQL 游标)

Oracle 自动维护,可通过属性访问:

属性 说明
SQL%FOUND DML 影响至少一行?
SQL%NOTFOUND 未影响任何行?
SQL%ROWCOUNT 影响的行数
SQL%ISOPEN 总为 FALSE(隐式游标自动关闭)
plsql 复制代码
BEGIN
  UPDATE emp SET sal = sal * 1.1 WHERE deptno = 50;

  IF SQL%FOUND THEN
    DBMS_OUTPUT.PUT_LINE('Updated ' || SQL%ROWCOUNT || ' rows.');
  ELSE
    DBMS_OUTPUT.PUT_LINE('No employees in dept 50.');
  END IF;
END;
/

4. 游标属性总结

游标类型 %FOUND %NOTFOUND %ROWCOUNT %ISOPEN
显式
隐式 否(始终 FALSE)

5. 游标变量(REF CURSOR)

用于动态 SQL 或跨过程传递结果集(高级用法,略)。

6. 通过 FOR 语句循环游标(推荐!自动管理打开/关闭)

plsql 复制代码
DECLARE
  CURSOR cur_high_sal IS
    SELECT ename, sal FROM emp WHERE sal > 2500;
BEGIN
  -- 自动 OPEN, FETCH, CLOSE
  FOR rec IN cur_high_sal LOOP
    DBMS_OUTPUT.PUT_LINE(rec.ename || ' earns $' || rec.sal);
  END LOOP;
END;
/

✅ 优点:代码简洁,不易出错。


十、PL/SQL 异常处理

1. 异常处理方法

  • 捕获运行时错误(如除零、无数据、重复主键等)
  • 防止程序崩溃,提供友好提示

2. 异常处理语法

plsql 复制代码
BEGIN
  -- 可能出错的代码
EXCEPTION
  WHEN exception_name THEN
    -- 处理特定异常
  WHEN OTHERS THEN
    -- 处理所有其他异常
END;
/

3. 预定义异常(常用)

异常名 触发条件
NO_DATA_FOUND SELECT INTO 未返回行
TOO_MANY_ROWS SELECT INTO 返回多行
ZERO_DIVIDE 除零错误
DUP_VAL_ON_INDEX 唯一约束冲突
INVALID_NUMBER 字符串转数字失败
plsql 复制代码
DECLARE
  v_ename emp.ename%TYPE;
BEGIN
  SELECT ename INTO v_ename
  FROM emp
  WHERE empno = 9999;  -- 不存在的员工

EXCEPTION
  WHEN NO_DATA_FOUND THEN
    DBMS_OUTPUT.PUT_LINE('Error: Employee not found!');
  WHEN TOO_MANY_ROWS THEN
    DBMS_OUTPUT.PUT_LINE('Error: Multiple employees found!');
  WHEN OTHERS THEN
    DBMS_OUTPUT.PUT_LINE('Unexpected error: ' || SQLERRM);
END;
/

SQLERRM 返回错误消息文本。

4. 自定义异常

plsql 复制代码
DECLARE
  -- 声明自定义异常
  e_invalid_salary EXCEPTION;

  v_sal emp.sal%TYPE := 100;
BEGIN
  IF v_sal < 500 THEN
    -- 抛出自定义异常
    RAISE e_invalid_salary;
  END IF;

EXCEPTION
  WHEN e_invalid_salary THEN
    DBMS_OUTPUT.PUT_LINE('Error: Salary too low!');
  WHEN OTHERS THEN
    DBMS_OUTPUT.PUT_LINE('System error: ' || SQLERRM);
END;
/

也可使用 RAISE_APPLICATION_ERROR(-20001, '自定义消息') 抛出带编号的错误。


十一、综合性实战案例

案例:批量调整员工工资并记录日志

需求

  1. 为每个部门工资最低的员工加薪 10%
  2. 记录调整前后的工资到日志表
  3. 处理可能的异常(如无员工、加薪后超上限)
  4. 使用游标遍历部门
步骤 1:创建日志表
sql 复制代码
CREATE TABLE emp_salary_log (
  log_id     NUMBER GENERATED ALWAYS AS IDENTITY,
  empno      NUMBER(4),
  old_sal    NUMBER(7,2),
  new_sal    NUMBER(7,2),
  update_time DATE DEFAULT SYSDATE
);
步骤 2:PL/SQL 程序
plsql 复制代码
DECLARE
  -- 自定义异常
  e_salary_too_high EXCEPTION;
  PRAGMA EXCEPTION_INIT(e_salary_too_high, -20001);

  -- 游标:每个部门最低工资员工
  CURSOR cur_min_sal_dept IS
    SELECT e.empno, e.ename, e.sal, e.deptno, d.dname
    FROM emp e
    JOIN dept d ON e.deptno = d.deptno
    WHERE e.sal = (
      SELECT MIN(sal)
      FROM emp e2
      WHERE e2.deptno = e.deptno
    );

  v_new_sal emp.sal%TYPE;
  v_max_allowed CONSTANT NUMBER := 10000;
BEGIN
  DBMS_OUTPUT.PUT_LINE('Starting salary adjustment...');

  FOR rec IN cur_min_sal_dept LOOP
    BEGIN
      v_new_sal := rec.sal * 1.1;

      -- 检查是否超过上限
      IF v_new_sal > v_max_allowed THEN
        RAISE e_salary_too_high;
      END IF;

      -- 更新工资
      UPDATE emp
      SET sal = v_new_sal
      WHERE empno = rec.empno;

      -- 记录日志
      INSERT INTO emp_salary_log (empno, old_sal, new_sal)
      VALUES (rec.empno, rec.sal, v_new_sal);

      DBMS_OUTPUT.PUT_LINE(
        'Adjusted: ' || rec.ename ||
        ' (Dept: ' || rec.dname || ') from ' ||
        rec.sal || ' to ' || v_new_sal
      );

    EXCEPTION
      WHEN e_salary_too_high THEN
        DBMS_OUTPUT.PUT_LINE(
          'Warning: ' || rec.ename || '''s new salary exceeds limit. Skipped.'
        );
      WHEN DUP_VAL_ON_INDEX THEN
        DBMS_OUTPUT.PUT_LINE('Log entry already exists for ' || rec.empno);
      WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error for ' || rec.ename || ': ' || SQLERRM);
    END;
  END LOOP;

  COMMIT;  -- 提交事务
  DBMS_OUTPUT.PUT_LINE('Adjustment completed.');

EXCEPTION
  WHEN OTHERS THEN
    ROLLBACK;
    DBMS_OUTPUT.PUT_LINE('Transaction rolled back due to error: ' || SQLERRM);
END;
/
输出示例:
复制代码
Starting salary adjustment...
Adjusted: SMITH (Dept: RESEARCH) from 800 to 880
Adjusted: JAMES (Dept: SALES) from 950 to 1045
Adjusted: MILLER (Dept: ACCOUNTING) from 1300 to 1430
Adjustment completed.

十二、总结

模块 核心知识点
块结构 DECLARE/BEGIN/EXCEPTION/END
变量/常量 %TYPE, %ROWTYPE, CONSTANT
控制结构 IF, CASE, LOOP, FOR
游标 显式/隐式,FOR 循环游标
异常处理 预定义异常、自定义异常、SQLERRM
最佳实践 使用绑定变量、避免硬编码、合理使用 COMMIT/ROLLBACK

💡 学习建议

  • 从简单匿名块开始练习
  • 多使用 DBMS_OUTPUT.PUT_LINE 调试
  • 优先使用 FOR 循环游标
  • 异常处理要覆盖 OTHERS

掌握 PL/SQL 是成为 Oracle 开发者或 DBA 的关键一步。后续可深入学习 存储过程、函数、包、触发器 等高级特性。

相关推荐
richxu202510012 小时前
嵌入式学习之路->stm32篇-->(8)I2C通讯(上)
stm32·嵌入式硬件·学习
行业探路者2 小时前
如何利用二维码提升富媒体展示的效果?
大数据·人工智能·学习·产品运营·软件工程
生擒小朵拉2 小时前
ROS1学习笔记(一)
笔记·学习
丝斯20112 小时前
AI学习笔记整理(52)——大模型之Agent 智能体
人工智能·笔记·学习
小范馆2 小时前
ESP8266-01S学习笔记
笔记·stm32·学习·esp8266-01s
山峰哥2 小时前
SQL调优实战:让查询效率飙升10倍的降本密码
服务器·前端·数据库·sql·编辑器·深度优先
一个响当当的名号3 小时前
lectrue2 高级SQL
数据库·oracle
keep_moving_20233 小时前
uvm_mem_mam介绍
学习
再卷还是菜3 小时前
网安渗透学习小结--PHP源码调试方法,文件包含漏洞,PHP伪协议以及ctf题目解答
学习