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, '自定义消息')抛出带编号的错误。
十一、综合性实战案例
案例:批量调整员工工资并记录日志
需求:
- 为每个部门工资最低的员工加薪 10%
- 记录调整前后的工资到日志表
- 处理可能的异常(如无员工、加薪后超上限)
- 使用游标遍历部门
步骤 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 的关键一步。后续可深入学习 存储过程、函数、包、触发器 等高级特性。