1. 从一段“看起来没问题”的代码说起
很多人学 PL/SQL 的时候,前面变量声明、数据类型、%TYPE、%ROWTYPE 都学得挺顺,一到执行语句这块就开始出问题。我见过太多这样的情况:语法背得滚瓜烂熟,真到写一个稍微复杂点的业务逻辑,要么是循环里少写了个EXIT WHEN,要么是SELECT INTO没处理NO_DATA_FOUND,跑起来直接报错,排查半天找不到原因。
这篇笔记专门聊 PL/SQL 的执行语句部分。所谓执行语句,就是BEGIN ... END块里真正干活的那些代码——赋值、条件判断、循环、SQL 语句嵌入、异常处理,这些构成了 PL/SQL 程序的主体。变量声明得再漂亮,执行语句写不好,程序照样跑不起来。
适合谁看?如果你已经写过一些简单的 PL/SQL 块,知道DECLARE和BEGIN的区别,但对条件分支的写法、循环的控制、游标的使用还不太有把握,那这篇内容就是给你准备的。如果你是完全零基础,建议先把变量声明和基本语法过一遍再来看,不然有些地方可能会觉得跳跃。
下面我会按照实际写代码时的思路来展开:先讲整体结构怎么设计,再拆解每一类执行语句的细节和坑,然后给一个完整的实操案例,最后把常见报错和排查方法整理出来。内容比较长,建议收藏着慢慢看,遇到具体问题的时候翻出来对照。
2. 执行语句的整体设计与思路拆解
2.1 为什么执行语句是 PL/SQL 的核心
PL/SQL 的程序结构分三部分:声明部分(DECLARE)、执行部分(BEGIN ... END)、异常处理部分(EXCEPTION)。声明部分定义变量和类型,异常处理部分兜底错误,而执行部分才是真正实现业务逻辑的地方。
打个比方,声明部分像是你出门前准备的工具箱,异常处理像是保险绳,执行部分才是你真正干活的过程。工具箱再齐全、保险绳再结实,活干得不对,结果还是白搭。
执行语句主要包含这几类:
- 赋值语句:给变量赋值,包括直接赋值和
SELECT INTO赋值 - 条件控制语句:
IF-THEN-ELSIF-ELSE和CASE - 循环语句:
LOOP、WHILE LOOP、FOR LOOP - SQL 语句:
INSERT、UPDATE、DELETE、SELECT INTO、游标操作 - 顺序控制语句:
GOTO、NULL - 动态 SQL:
EXECUTE IMMEDIATE
这几类语句的组合方式决定了程序的逻辑走向。写执行语句的时候,脑子里要清楚三件事:数据从哪来、经过什么处理、结果到哪去。想清楚这三点,代码结构就不会乱。
2.2 方案选型:什么时候用什么语句
很多初学者容易犯的一个毛病是“手里有锤子,看什么都像钉子”。学了FOR LOOP就到处用循环,学了游标就什么查询都上游标。实际上不同的执行语句有各自最适合的场景,选错了虽然功能能实现,但性能和可维护性会差很多。
我整理了一个选型对照表,方便你快速判断:
| 场景 | 推荐方案 | 不推荐方案 | 原因 |
|---|---|---|---|
| 单行单列查询 | SELECT INTO | 显式游标 | 代码简洁,性能好 |
| 多行查询逐行处理 | 显式游标 +FOR LOOP | 隐式游标 | 可控性强,能批量提交 |
| 固定次数循环 | FOR LOOP(数值型) | WHILE LOOP | 不需要手动维护计数器 |
| 条件不确定的循环 | WHILE LOOP | FOR LOOP | 条件驱动更自然 |
| 至少执行一次 | 基础LOOP | WHILE LOOP | 先执行后判断 |
| 多分支等值判断 | CASE | 多层IF | 可读性更好 |
| 范围判断 | IF-ELSIF | CASE | CASE不适合范围条件 |
| 批量 DML | FORALL | 逐行 DML | 性能差距巨大 |
这个表不是死规矩,但大部分情况下按这个来选不会错。核心原则是:能用 SQL 解决的不要用 PL/SQL 循环解决,能用简单语句的不要用复杂语句。PL/SQL 的循环性能远不如原生 SQL,这是很多人踩过的坑。
2.3 执行语句的书写规范
在展开具体语法之前,先说几个书写规范。这些不是语法强制要求,但按这个来写,代码可读性和可维护性会好很多。
缩进:BEGIN ... END内部的语句统一缩进两个或四个空格,嵌套的IF、LOOP再往里缩进。不要用 Tab 和空格混着来,不同编辑器显示效果不一样。
命名:变量用v_前缀,常量用c_前缀,游标用cur_前缀,参数用p_前缀。这样一眼就能看出标识符的类型,不用翻到声明部分去确认。
注释:单行注释用--,多行注释用/* */。每个逻辑块前面加一行注释说明意图,不要注释“这行是赋值”这种废话,要注释“为什么这么赋值”。
大小写:关键字统一大写(BEGIN、END、IF、LOOP),标识符统一小写。这样视觉上容易区分。
这些规范看起来琐碎,但实际项目中,代码是给人看的,机器只是顺便执行一下。规范写出来的代码,三个月后自己回头看还能快速理解。
3. 核心执行语句的细节解析与实操要点
3.1 赋值语句:不只是等号那么简单
赋值语句是执行语句里最基础的一类,但坑也不少。PL/SQL 里的赋值用:=,不是=。=在 PL/SQL 里是比较运算符,用在IF条件里。这个区别初学者经常搞混。
DECLARE v_name VARCHAR2(50); v_count NUMBER := 0; -- 声明时赋初值 BEGIN v_name := '张三'; -- 执行部分赋值 v_count := v_count + 1; END;除了直接赋值,还有两种特殊的赋值方式需要重点掌握。
SELECT INTO 赋值:从数据库查询结果直接赋给变量。这个用起来方便,但有两个必须处理的异常:NO_DATA_FOUND(查询没结果)和TOO_MANY_ROWS(查询结果超过一行)。
BEGIN SELECT employee_name INTO v_name FROM employees WHERE employee_id = 100; EXCEPTION WHEN NO_DATA_FOUND THEN v_name := '未知'; WHEN TOO_MANY_ROWS THEN DBMS_OUTPUT.PUT_LINE('查询结果不唯一'); END;注意:
SELECT INTO必须返回且只返回一行。如果业务上可能返回多行,要么加ROWNUM = 1限制,要么改用游标。不加异常处理的SELECT INTO是生产事故的高发区。
游标赋值:通过FETCH把游标当前行的数据赋给变量。这个后面讲游标的时候详细说。
赋值语句还有一个容易忽略的点:空值传播。任何包含NULL的算术运算结果都是NULL。比如v_count := NULL + 1,结果还是NULL,不是 1。所以变量声明时最好给个初值,避免空值参与运算。
3.2 条件控制:IF 和 CASE 怎么选
条件控制语句决定程序的走向。PL/SQL 提供两种:IF和CASE。
IF 语句有三种形式:
-- 形式一:单分支 IF condition THEN statements; END IF; -- 形式二:双分支 IF condition THEN statements; ELSE statements; END IF; -- 形式三:多分支 IF condition1 THEN statements; ELSIF condition2 THEN statements; ELSE statements; END IF;注意ELSIF的拼写,不是ELSEIF,也不是ELSE IF。这个拼写错误编译器会直接报错,但初学者经常打错。
CASE 语句有两种形式:简单 CASE 和搜索 CASE。
-- 简单 CASE:基于一个表达式的值做等值判断 CASE v_grade WHEN 'A' THEN DBMS_OUTPUT.PUT_LINE('优秀'); WHEN 'B' THEN DBMS_OUTPUT.PUT_LINE('良好'); WHEN 'C' THEN DBMS_OUTPUT.PUT_LINE('及格'); ELSE DBMS_OUTPUT.PUT_LINE('不及格'); END CASE; -- 搜索 CASE:基于多个条件表达式 CASE WHEN v_score >= 90 THEN DBMS_OUTPUT.PUT_LINE('优秀'); WHEN v_score >= 80 THEN DBMS_OUTPUT.PUT_LINE('良好'); WHEN v_score >= 60 THEN DBMS_OUTPUT.PUT_LINE('及格'); ELSE DBMS_OUTPUT.PUT_LINE('不及格'); END CASE;选型建议:等值判断用简单 CASE,范围判断用搜索 CASE。如果分支超过三个,CASE 通常比嵌套 IF 更清晰。
注意:CASE 语句里如果没有匹配的
WHEN且没有ELSE,会抛出CASE_NOT_FOUND异常。所以要么保证覆盖所有情况,要么加ELSE兜底。这一点和 IF 不同,IF 没有匹配就是什么都不做,不会报错。
3.3 循环语句:三种循环的适用场景
PL/SQL 有三种循环:基础LOOP、WHILE LOOP、FOR LOOP。它们的区别在于循环条件的判断时机和循环变量的管理方式。
基础 LOOP:先执行循环体,再判断是否退出。至少执行一次。
LOOP statements; EXIT WHEN condition; END LOOP;WHILE LOOP:先判断条件,条件为真才执行循环体。可能一次都不执行。
WHILE condition LOOP statements; END LOOP;FOR LOOP:循环变量自动管理,从下界到上界自动递增或递减。
FOR i IN 1..10 LOOP DBMS_OUTPUT.PUT_LINE(i); END LOOP; -- 递减循环 FOR i IN REVERSE 10..1 LOOP DBMS_OUTPUT.PUT_LINE(i); END LOOP;FOR LOOP 的循环变量不需要在声明部分定义,它是循环内部自动声明的。循环结束后这个变量就不存在了,不能在循环外面引用。
循环控制有两个关键字:EXIT和CONTINUE。
EXIT:跳出整个循环EXIT WHEN condition:条件满足时跳出CONTINUE:跳过本次循环剩余部分,进入下一次循环CONTINUE WHEN condition:条件满足时跳过
FOR i IN 1..10 LOOP CONTINUE WHEN MOD(i, 2) = 0; -- 跳过偶数 DBMS_OUTPUT.PUT_LINE(i); -- 只输出奇数 END LOOP;注意:循环里一定要有退出条件,否则就是死循环。基础 LOOP 最容易写出死循环,因为退出条件写在循环体内部,如果条件永远不满足,程序就卡住了。写基础 LOOP 的时候,先写
EXIT WHEN,再写业务逻辑,这样不容易忘。
3.4 游标:处理多行查询的正确姿势
游标是 PL/SQL 里处理多行查询结果的核心工具。分显式游标和隐式游标两种。
显式游标的使用分四步:声明、打开、提取、关闭。
DECLARE CURSOR cur_emp IS SELECT employee_id, employee_name FROM employees WHERE department_id = 10; v_id employees.employee_id%TYPE; v_name employees.employee_name%TYPE; BEGIN OPEN cur_emp; LOOP FETCH cur_emp INTO v_id, v_name; EXIT WHEN cur_emp%NOTFOUND; DBMS_OUTPUT.PUT_LINE(v_id || ' - ' || v_name); END LOOP; CLOSE cur_emp; END;游标有四个属性:
| 属性 | 含义 | 使用时机 |
|---|---|---|
%FOUND | 最近一次 FETCH 是否取到数据 | FETCH 之后 |
%NOTFOUND | 最近一次 FETCH 是否没取到数据 | FETCH 之后 |
%ROWCOUNT | 到目前为止取到的行数 | FETCH 之后 |
%ISOPEN | 游标是否处于打开状态 | OPEN 之后 |
游标 FOR 循环是更简洁的写法,不需要手动 OPEN、FETCH、CLOSE,Oracle 自动管理。
BEGIN FOR rec IN (SELECT employee_id, employee_name FROM employees WHERE department_id = 10) LOOP DBMS_OUTPUT.PUT_LINE(rec.employee_id || ' - ' || rec.employee_name); END LOOP; END;游标 FOR 循环的rec是记录类型变量,自动匹配查询的列结构。循环结束后自动关闭游标,不用手动处理。实际开发中,除非有特殊需求(比如需要中途退出后重新打开),否则优先用游标 FOR 循环。
隐式游标是 Oracle 自动创建的,用于SELECT INTO、INSERT、UPDATE、DELETE等语句。通过SQL%FOUND、SQL%ROWCOUNT等属性访问。
BEGIN UPDATE employees SET salary = salary * 1.1 WHERE department_id = 10; IF SQL%ROWCOUNT > 0 THEN DBMS_OUTPUT.PUT_LINE('更新了 ' || SQL%ROWCOUNT || ' 行'); END IF; END;注意:隐式游标的属性只在最近一次 SQL 语句执行后有效。如果中间执行了其他 SQL,属性值会被覆盖。所以要在 SQL 执行后立即读取属性值。
3.5 动态 SQL:EXECUTE IMMEDIATE 的使用
有时候 SQL 语句在编译时不确定,需要运行时动态拼接,这时候用EXECUTE IMMEDIATE。
DECLARE v_table_name VARCHAR2(30) := 'employees'; v_sql VARCHAR2(500); v_count NUMBER; BEGIN v_sql := 'SELECT COUNT(*) FROM ' || v_table_name || ' WHERE department_id = :1'; EXECUTE IMMEDIATE v_sql INTO v_count USING 10; DBMS_OUTPUT.PUT_LINE('记录数:' || v_count); END;动态 SQL 的要点:
INTO子句接收查询结果,只适用于返回单行的查询USING子句传入绑定变量,避免 SQL 注入- 动态 SQL 在运行时才解析,编译时不会检查语法错误
- 性能比静态 SQL 差,因为每次执行都要解析
注意:动态 SQL 里拼接表名、列名的时候要特别小心,这些不能作为绑定变量传入,只能字符串拼接。如果来源不可信,必须做白名单校验,否则有 SQL 注入风险。绑定变量只能用于值,不能用于标识符。
4. 完整实操案例:从需求到实现
4.1 需求描述
假设有一个员工表employees,包含employee_id、employee_name、department_id、salary、hire_date几个字段。现在需要写一个 PL/SQL 块,实现以下功能:
- 遍历部门 10 的所有员工
- 根据入职年限调整薪资:满 5 年涨 10%,满 3 年涨 5%,其他涨 2%
- 如果调整后薪资超过 20000,只涨到 20000 为止
- 把调整结果写入日志表
salary_log - 处理可能出现的异常
4.2 实现思路拆解
这个需求涉及几个关键点:
- 多行查询:用游标 FOR 循环遍历
- 条件判断:用搜索 CASE 或 IF-ELSIF
- 上限控制:用
LEAST函数或 IF 判断 - 数据写入:用
INSERT - 异常处理:捕获
OTHERS并记录
先想清楚数据流:从employees读数据,计算新薪资,写入salary_log。读和写是分开的,中间用变量传递。
4.3 完整代码实现
DECLARE v_new_salary NUMBER; v_raise_rate NUMBER; v_years NUMBER; BEGIN FOR rec IN (SELECT employee_id, employee_name, salary, hire_date FROM employees WHERE department_id = 10) LOOP -- 计算入职年限 v_years := TRUNC(MONTHS_BETWEEN(SYSDATE, rec.hire_date) / 12); -- 根据年限确定涨幅 CASE WHEN v_years >= 5 THEN v_raise_rate := 0.10; WHEN v_years >= 3 THEN v_raise_rate := 0.05; ELSE v_raise_rate := 0.02; END CASE; -- 计算新薪资,上限 20000 v_new_salary := LEAST(rec.salary * (1 + v_raise_rate), 20000); -- 写入日志表 INSERT INTO salary_log (employee_id, old_salary, new_salary, log_date) VALUES (rec.employee_id, rec.salary, v_new_salary, SYSDATE); DBMS_OUTPUT.PUT_LINE(rec.employee_name || ':' || rec.salary || ' -> ' || v_new_salary); END LOOP; COMMIT; DBMS_OUTPUT.PUT_LINE('薪资调整完成'); EXCEPTION WHEN OTHERS THEN ROLLBACK; DBMS_OUTPUT.PUT_LINE('发生错误:' || SQLERRM); RAISE; END;4.4 关键步骤说明
游标 FOR 循环的选择:这里用游标 FOR 循环而不是显式游标,因为不需要中途退出或重新打开,自动管理更省心。rec自动匹配查询列,直接用rec.employee_id访问。
年限计算:MONTHS_BETWEEN(SYSDATE, hire_date)返回两个日期之间的月数,除以 12 再TRUNC取整得到年数。注意MONTHS_BETWEEN返回的是带小数的数值,直接比较会有精度问题,所以先取整。
涨幅确定:用搜索 CASE 而不是 IF-ELSIF,因为条件是范围判断,CASE 写起来更整齐。注意WHEN的顺序,从大到小排,这样第一个匹配的就是最终结果。
上限控制:用LEAST函数取较小值,比写 IF 判断简洁。LEAST(a, b)返回 a 和 b 中较小的那个。
异常处理:捕获OTHERS后先ROLLBACK再输出错误信息,最后RAISE重新抛出异常。这样既保证了数据一致性,又不会吞掉错误。实际项目中,异常信息应该写入日志表而不是只输出到控制台。
提交时机:循环结束后统一COMMIT,而不是每行提交。批量提交性能更好,但要注意如果循环中途出错,前面的操作会回滚。如果数据量大,可以每 1000 行提交一次,平衡性能和回滚代价。
4.5 参数计算过程
以一个月薪 8000、入职 4 年的员工为例:
- 年限计算:
MONTHS_BETWEEN(SYSDATE, hire_date)假设返回 48.5,除以 12 得 4.04,TRUNC后为 4 - 涨幅确定:4 年满足
>= 3但不满足>= 5,所以涨幅 5% - 新薪资计算:
8000 * 1.05 = 8400,LEAST(8400, 20000) = 8400 - 结果:薪资从 8000 调整为 8400
再以一个月薪 19000、入职 6 年的员工为例:
- 年限计算:6 年
- 涨幅确定:满足
>= 5,涨幅 10% - 新薪资计算:
19000 * 1.10 = 20900,LEAST(20900, 20000) = 20000 - 结果:薪资从 19000 调整为 20000,被上限截断
这个计算逻辑清晰,每一步都可验证。实际写代码的时候,建议先用几个典型值手动算一遍,确认逻辑正确再写进程序。
5. 常见问题与排查技巧实录
5.1 执行语句常见报错速查
| 报错信息 | 原因 | 解决方法 |
|---|---|---|
ORA-06550: line X, column Y | 语法错误 | 检查对应行列的语法,常见是漏分号、拼写错误 |
ORA-01403: no data found | SELECT INTO没查到数据 | 加NO_DATA_FOUND异常处理 |
ORA-01422: exact fetch returns more than requested number of rows | SELECT INTO返回多行 | 加ROWNUM = 1或改用游标 |
ORA-06502: numeric or value error | 变量长度不够或类型不匹配 | 检查变量声明长度和赋值内容 |
ORA-06531: Reference to uninitialized collection | 集合未初始化 | 使用前先调用构造函数初始化 |
ORA-01000: maximum open cursors exceeded | 游标打开太多没关闭 | 检查游标是否都有关闭,或增大OPEN_CURSORS参数 |
ORA-06508: PL/SQL: could not find program unit being called | 调用的存储过程不存在或无效 | 检查对象是否存在,重新编译 |
ORA-04063: package body has errors | 包体有编译错误 | 查看编译错误详情,修复后重新编译 |
5.2 排查思路:从报错到定位
遇到报错不要慌,按这个顺序排查:
第一步:看报错信息。Oracle 的报错信息通常包含错误码、错误描述、出错的行号。行号是最关键的线索,直接定位到具体哪一行。
第二步:检查该行的语法。常见问题:漏分号、括号不匹配、关键字拼写错误、变量名写错。这些低级错误占了报错的一半以上。
第三步:检查变量和类型。如果语法没问题,看涉及的变量是否声明、类型是否匹配、长度是否够。VARCHAR2(10)的变量赋一个 20 字符的字符串就会报ORA-06502。
第四步:检查数据。如果语法和变量都没问题,看查询的数据是否符合预期。SELECT INTO返回多行或零行都会报错,但语法本身没问题。
第五步:加调试输出。在关键位置加DBMS_OUTPUT.PUT_LINE,输出变量值和执行进度,看程序走到哪一步出错。这是最笨但最有效的方法。
5.3 独家避坑技巧
技巧一:先写异常处理框架。写任何 PL/SQL 块,先把EXCEPTION WHEN OTHERS THEN框架搭好,再填业务逻辑。这样即使出错也能看到错误信息,不会一头雾水。
技巧二:循环里慎用 COMMIT。循环里每行提交会导致频繁的 I/O,性能差。而且如果循环中途出错,前面提交的数据无法回滚,数据一致性难保证。建议循环外统一提交,或者每 N 行提交一次。
技巧三:游标用完必须关。显式游标不关会占用数据库资源,长时间运行的程序可能报ORA-01000。用游标 FOR 循环可以避免这个问题,因为自动关闭。
技巧四:动态 SQL 用绑定变量。拼接 SQL 的时候,值用USING传,不要直接拼字符串。直接拼字符串不仅有 SQL 注入风险,还会导致每次执行都重新解析,性能差。
技巧五:注意 NULL 的比较。NULL = NULL的结果是NULL,不是TRUE。判断 NULL 要用IS NULL或IS NOT NULL。条件里涉及可能为 NULL 的变量时要特别小心。
技巧六:FOR LOOP 的边界。FOR i IN 1..10包含 1 和 10,共 10 次。FOR i IN REVERSE 10..1从 10 递减到 1。如果下界大于上界且不用 REVERSE,循环体一次都不执行,不会报错。这个特性有时候会导致逻辑静默失败。
技巧七:CASE 的 ELSE 不是可有可无。CASE 语句没有匹配且没有 ELSE 会抛CASE_NOT_FOUND异常。即使你认为覆盖了所有情况,加个 ELSE 兜底也更安全。
技巧八:变量命名避免关键字。比如date、number、value这些词虽然不全是保留字,但用作变量名容易引起混淆。加前缀v_可以有效避免。
5.4 性能优化要点
执行语句写对了只是第一步,写得快才是进阶要求。几个关键优化点:
减少循环内的 SQL。循环里每执行一次 SQL 就是一次网络往返和解析。如果循环 1000 次,就是 1000 次 SQL 执行。能改成批量操作的尽量批量。比如循环里INSERT,可以改成FORALL批量插入。
用 BULK COLLECT 批量提取。游标逐行 FETCH 性能差,用BULK COLLECT INTO一次性提取到集合里,再在内存中处理。
DECLARE TYPE t_emp IS TABLE OF employees%ROWTYPE; v_emps t_emp; BEGIN SELECT * BULK COLLECT INTO v_emps FROM employees WHERE department_id = 10; FOR i IN 1..v_emps.COUNT LOOP -- 处理 v_emps(i) END LOOP; END;避免在循环条件里做查询。WHILE (SELECT COUNT(*) FROM ...) > 0 LOOP这种写法每次循环都查一次数据库,性能极差。应该先查出来存到变量里,循环里用变量判断。
用 EXISTS 代替 COUNT(*)。判断记录是否存在,EXISTS比COUNT(*)快,因为找到一条就返回,不用统计全部。
这些优化技巧在实际项目中能带来数量级的性能提升,尤其是数据量大的时候。我见过一个程序,优化前跑 20 分钟,优化后 30 秒,差别就在这些细节上。
6. 执行语句的进阶用法与扩展思路
6.1 嵌套块与作用域
PL/SQL 允许在BEGIN ... END里再嵌套BEGIN ... END,形成嵌套块。嵌套块里可以声明自己的变量,作用域仅限于该块内部。
DECLARE v_outer VARCHAR2(10) := 'outer'; BEGIN DECLARE v_inner VARCHAR2(10) := 'inner'; BEGIN DBMS_OUTPUT.PUT_LINE(v_outer || ' ' || v_inner); -- 都能访问 END; -- DBMS_OUTPUT.PUT_LINE(v_inner); -- 报错,v_inner 超出作用域 END;嵌套块的主要用途是隔离异常处理。内层块出错,如果不处理会传播到外层块。如果内层块自己处理了,外层块感知不到。
BEGIN BEGIN -- 可能出错的代码 NULL; EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('内层处理了错误'); END; DBMS_OUTPUT.PUT_LINE('外层继续执行'); END;这个特性在批量处理的时候很有用:单条记录出错不影响其他记录继续处理。
6.2 GOTO 和 NULL 语句
GOTO语句无条件跳转到指定标签。实际开发中强烈不建议使用,因为它破坏程序的结构化流程,让代码难以理解和维护。几乎所有GOTO能实现的功能,都能用条件判断和循环更好地实现。
BEGIN GOTO label1; DBMS_OUTPUT.PUT_LINE('这行不会执行'); <<label1>> DBMS_OUTPUT.PUT_LINE('跳转到这里'); END;NULL语句是一个空操作,什么都不做。主要用途是占位,让语法完整。比如IF分支里暂时不想写逻辑,但不能留空,就写个NULL。
IF condition THEN NULL; -- 暂时不处理 ELSE DBMS_OUTPUT.PUT_LINE('处理其他情况'); END IF;6.3 执行语句的调试方法
PL/SQL 没有断点调试那么方便的工具,主要靠输出调试。DBMS_OUTPUT.PUT_LINE是最常用的手段,但要注意几点:
- 需要先执行
SET SERVEROUTPUT ON才能看到输出 - 输出缓冲区有大小限制,输出太多会报
ORA-20000 - 生产环境不要留调试输出,影响性能
更专业的做法是建一个日志表,把调试信息写进去,带时间戳和模块名,方便事后分析。
CREATE TABLE debug_log ( log_id NUMBER GENERATED ALWAYS AS IDENTITY, log_time TIMESTAMP DEFAULT SYSTIMESTAMP, module VARCHAR2(100), message VARCHAR2(4000) ); -- 写日志 INSERT INTO debug_log (module, message) VALUES ('PROC_001', '开始处理');这种方式比DBMS_OUTPUT灵活,可以随时查询,不受会话限制。
6.4 事务控制与执行语句的配合
执行语句里的 DML 操作涉及事务控制。COMMIT提交事务,ROLLBACK回滚事务,SAVEPOINT设置保存点。
BEGIN INSERT INTO t1 VALUES (1); SAVEPOINT sp1; INSERT INTO t2 VALUES (2); ROLLBACK TO sp1; -- 回滚到 sp1,t2 的插入被撤销,t1 的插入保留 COMMIT; END;事务控制的关键原则:
- 事务要尽量短,长时间不提交会占用锁资源
- 循环里批量 DML 时,定期提交,避免回滚段过大
- 异常处理里记得
ROLLBACK,保证数据一致性 - DDL 语句(
CREATE、ALTER、DROP)会自动提交事务,不能回滚
注意:
COMMIT之后不能再ROLLBACK。如果程序逻辑需要回滚,确保在COMMIT之前完成所有判断。这个顺序搞反了会导致数据无法回滚。
7. 写在最后的一些实操体会
PL/SQL 的执行语句看起来简单,真写好需要大量练习。我的建议是:不要光看语法,一定要动手写。找一个实际的业务场景,从简单的开始,逐步增加复杂度。写完之后自己 review 一遍,看看有没有漏掉的异常处理、有没有可以优化的地方。
另外,养成看执行计划的习惯。很多性能问题在写代码的时候就能避免,关键是脑子里要有“这条语句会怎么执行”的概念。比如循环里查数据库,你知道每次循环都是一次网络往返,自然就会想办法改成批量操作。
最后分享一个我自己的习惯:写 PL/SQL 的时候,先把异常处理框架和日志输出写好,再填业务逻辑。这样即使逻辑写错了,也能通过日志快速定位问题。这个习惯帮我省了很多排查时间,尤其是在处理复杂业务逻辑的时候。
执行语句这块内容比较多,一篇写不完。后面有机会再聊聊存储过程、函数、触发器的执行语句有什么不同,以及包(PACKAGE)里的执行语句怎么组织。先把这篇的内容消化掉,动手写几个例子,比看十篇文章都管用。