PL/SQL 的异常不是「程序崩了」,是「流程跳转」。
一出异常,当前块剩下的代码立刻不跑,直接跳到 EXCEPTION 段。
最危险的一句是 WHEN OTHERS THEN NULL;——异常被吞掉,什么都不发生,你连日志都没有。
5.4 节的游标循环一旦 SELECT INTO 查不到行就会抛异常,异常处理是游标的必备搭档。
没有 EXCEPTION 段,一个「查不到」就能让整个块炸掉。
第 6 章写存储过程时,EXCEPTION 段是标准配置,
因为过程要被别人调用,出错必须能说清楚。
一、异常处理的基本思路
异常是一种「非正常但可预期」的路径。
「查不到人」不是 bug,是业务上正常会发生的事——但它不能按正常流程走,所以用异常表达。
一出异常,当前块的剩余语句立刻不执行,直接跳到 EXCEPTION 段。
EXCEPTION 段跑完,块就结束了——END 之后的代码照常执行。
块是隔离的。
内层块出错,如果内层自己接住了,外层当没发生过;内层没接住,才往外层抛。
这个机制让「局部出错、整体继续」成为可能,也是它比「一错全停」好用的地方。
二、异常处理语法:WHEN ... THEN
EXCEPTION 段里全是 WHEN 异常名 THEN 处理语句 的分支。
一个分支可以接多个异常,用 OR 连:WHEN A OR B THEN。
分支从上往下匹配,命中一个就跳出整个 EXCEPTION 段。
所以顺序有讲究:具体的放前面,WHEN OTHERS 放最后。
WHEN OTHERS THEN 能接住所有没被前面接掉的异常。
它必须放最后——放在前面,后面的分支就永远轮不到了。
SQLCODE 给错误号(负数),SQLERRM 给错误消息。
这两个只能在这个段里用,出了 EXCEPTION 就没值了。
它们是排查线上问题的唯一线索,别浪费。
三、预定义异常:常用的六个
| 异常名 | 错误号 | 什么时候抛 |
|---|---|---|
NO_DATA_FOUND |
ORA-01403 | SELECT INTO 一行都没查到 |
TOO_MANY_ROWS |
ORA-01422 | SELECT INTO 查出多行 |
ZERO_DIVIDE |
ORA-01476 | 除以零 |
DUP_VAL_ON_INDEX |
ORA-00001 | 唯一约束冲突 |
INVALID_NUMBER |
ORA-01722 | 字符串转数字失败 |
VALUE_ERROR |
ORA-06502 | 赋值时长度或精度不够 |
前两个是 SELECT INTO 专属的,也是最容易撞上的。
SELECT INTO 要求正好返回一行——零行报 NO_DATA_FOUND,多行报 TOO_MANY_ROWS。
这是 PL/SQL 和 SQL 最不一样的地方之一:
SQL 里查不到就是空结果,PL/SQL 里查不到是异常。
所以你写 SELECT INTO 就必须想好「查不到怎么办」——不想,就等于把炸弹留给运行时。
DUP_VAL_ON_INDEX 值得单独提一句:
它是最常见的「并发冲突」信号。
两个会话同时插同一个主键,后到的那个就抛它。
捕获它、改成 update,就是最朴素的「有则更新、无则插入」。
四、自定义异常:声明、RAISE、接住
业务规则上的「错」不是数据库的错,得自己定义。
三步:DECLARE 段声明一个 EXCEPTION,执行段 RAISE 它,异常段 WHEN 接住。
自定义异常不需要「注册」,声明了就能抛。
但它没有错误号——报出来只有你写的那句话。
要给外部程序看,就得用 RAISE_APPLICATION_ERROR(-20001, '消息')。
错误号必须落在 -20000 到 -20999 之间,这是留给用户的区间。
这个区间之外的号是 Oracle 自己的,占了会冲突。
它的价值在于「把错误信息完整地传给调用方」——
Java 那边 catch (SQLException e) 能同时拿到消息和错误号。
很多系统的错误码体系,就是从这儿起的。
五、WHEN OTHERS:最后一道网怎么织
最危险的写法是 WHEN OTHERS THEN NULL;——异常被吞掉了,什么都没发生,你连日志都没有。
线上最难查的 bug,一半是这么来的。
正确的写法至少要有三件事:记日志、提交、抛出去。
先记日志(SQLCODE 和 SQLERRM 都写进去),再 COMMIT(否则日志跟着事务一起回滚了),
最后 RAISE 抛回上层。
为什么一定要 RAISE?
因为「被调用方吞掉异常」等于骗了调用方——上层以为成功了,实际上什么都没做。
这类问题比「报错」难查一百倍。
唯一的例外是「这条路径上失败无所谓」的场景,比如写一条「非关键」的审计日志。
但这种场景要少,而且要写清楚为什么可以吞。
六、动手 Demo:六种异常逐个触发一遍
沙箱里没有 Oracle 的命名异常,所以这一段是我按 Oracle 的规则「判」出来的:
查不到就是 NO_DATA_FOUND,查出多行就是 TOO_MANY_ROWS,除以零就是 ZERO_DIVIDE。
判断依据和 Oracle 完全一致——SELECT INTO 要求正好一行,
这个规则两套引擎是一样的(只是一边报异常、一边报错)。
import sqlite3
c = sqlite3.connect(':memory:')
c.execute('create table emp (ename text, sal real, deptno int)')
c.executemany('insert into emp values (?,?,?)',
[('SMITH', 800, 20), ('JONES', 2975, 20), ('KING', 5000, 10)])
# SELECT INTO 要求正好一行:0 行报 NO_DATA_FOUND,多行报 TOO_MANY_ROWS
def select_into(sql):
rows = c.execute(sql).fetchall()
if len(rows) == 0:
raise LookupError('NO_DATA_FOUND')
if len(rows) > 1:
raise ValueError('TOO_MANY_ROWS')
return rows[0]
for sql in ["select sal from emp where ename = 'NOBODY'",
'select sal from emp where deptno = 20',
"select sal from emp where ename = 'SMITH'"]:
try:
print('OK', select_into(sql))
except (LookupError, ValueError) as e:
print('异常', e)
# 除以零、类型转换失败,各有各的名字
for f in (lambda: 100 / 0, lambda: int('abc')):
try:
f()
except ZeroDivisionError:
print('ZERO_DIVIDE')
except ValueError:
print('INVALID_NUMBER')
实跑输出:
--------------------------------------------------------------------
第二十五课:PL/SQL 异常处理——把「出错」也当成一条正常路径
--------------------------------------------------------------------
**PL/SQL 的异常不是「程序崩了」,是「流程跳转」。**
**一出异常,当前块的剩余代码立刻不跑了,直接跳到 `EXCEPTION` 段。**
**理解这一点很重要:`EXCEPTION` 之后的代码不会执行,但 `END` 之后的会。**
**块是隔离的——内层块出错,如果内层接住了,外层当没发生过。**
预定义异常:二十来个,常用的六个
--------------------------------------------------------------------
SQL> select name, code, whens as "什么时候报" from exc
NAME CODE 什么时候报
---------------------------------------------------
NO_DATA_FOUND ORA-01403 SELECT INTO 一行都没查到
TOO_MANY_ROWS ORA-01422 SELECT INTO 查出多行
ZERO_DIVIDE ORA-01476 除以零
DUP_VAL_ON_INDEX ORA-00001 唯一约束冲突
INVALID_NUMBER ORA-01722 字符串转数字失败
VALUE_ERROR ORA-06502 赋值时长度或精度不够
已选择 6 行
**前两个值得单独记:`NO_DATA_FOUND` 和 `TOO_MANY_ROWS` 是 `SELECT INTO` 专属的。**
**`SELECT INTO` 要求正好返回一行**——零行报 `NO_DATA_FOUND`,多行报 `TOO_MANY_ROWS`。
**这是 PL/SQL 和 SQL 最不一样的地方之一:SQL 里查不到就是空结果,PL/SQL 里查不到是异常。**
**所以你写 `SELECT INTO` 就必须想好「查不到怎么办」。**
把六种情况都跑一遍
--------------------------------------------------------------------
SQL> select scene as 场景, expect as 期望异常, actual as 实际结果 from tcase
场景 期望异常 实际结果
----------------------------------------------------------
查不到 NO_DATA_FOUND 抛 NO_DATA_FOUND(0 行)
查出多行 TOO_MANY_ROWS 抛 TOO_MANY_ROWS(5 行)
正好一行 (无异常) 正常拿到 1 行
除以零 ZERO_DIVIDE 抛 ZERO_DIVIDE
转换失败 INVALID_NUMBER 抛 INVALID_NUMBER
长度不够 VALUE_ERROR 抛 VALUE_ERROR(6 个字符塞不进 3)
已选择 6 行
**注意第三行(正好一行)什么都不抛**——这是 `SELECT INTO` 唯一正常的情况。
**剩下五种都会跳进 `EXCEPTION` 段。**
**我在「实际结果」里跑的是 SQLite 的等价操作**,**异常名字照 Oracle 的写**:
SQLite 没有 `NO_DATA_FOUND` 这种命名异常,**它只有「查到几行」和「报错信息」两样东西。**
自定义异常:先声明,再 RAISE
--------------------------------------------------------------------
**业务规则上的「错」不是数据库的错,得自己定义。**
**三步:`DECLARE` 段声明一个 `EXCEPTION`,执行段 `RAISE` 它,异常段 `WHEN` 接住。**
[Oracle 原码 · 本沙箱不执行]
| DECLARE
| e_sal_too_low EXCEPTION;
| BEGIN
| IF v_sal < 1000 THEN
| RAISE e_sal_too_low;
| END IF;
| EXCEPTION
| WHEN e_sal_too_low THEN
| DBMS_OUTPUT.PUT_LINE('工资低于下限');
| END;
**自定义异常不需要「注册」,声明了就能抛。** **但它没有错误号**——报出来只有你写的那句话,
**所以要给外部程序看,还得用 `RAISE_APPLICATION_ERROR`。**
**`RAISE_APPLICATION_ERROR(-20001, '工资低于下限')` 会抛一个带错误号的异常。**
**错误号必须落在 -20000 到 -20999 之间**,这是留给用户的区间。
**这段区间之外的号是 Oracle 自己的,占了会冲突。**
**它的作用是「把错误信息完整地传给调用方」**——
**Java 那边 `catch (SQLException e)` 能拿到 `e.getMessage()` 和 `e.getErrorCode()`。**
**很多系统的错误码体系就是从这儿起的。**
WHEN OTHERS:最后一道网
--------------------------------------------------------------------
**`WHEN OTHERS THEN` 能接住所有没被前面分支接掉的异常。** **它必须放在最后**——放在前面,
后面的分支就永远轮不到了。
**最危险的写法是 `WHEN OTHERS THEN NULL;`**——
**异常被吞掉了,什么都没发生,你连日志都没有。** **线上最难查的 bug,一半是这么来的。**
**规矩:`WHEN OTHERS` 里至少要记日志,最好再 `RAISE` 抛出去。**
**`SQLCODE` 给错误号(正数或负数),`SQLERRM` 给错误消息。**
**这两个只能在这个段里用,出了 `EXCEPTION` 就没值了。**
**一个完整的兜底长这样:**
| EXCEPTION
| WHEN OTHERS THEN
| INSERT INTO err_log (t, code, msg)
| VALUES (SYSDATE, SQLCODE, SQLERRM);
| COMMIT;
| RAISE;
**先记日志、再提交、最后 `RAISE` 抛回上层**——
**这样既留下了现场,又没改变「这次操作失败了」这个事实。**
表里六行,只有第三行「正好一行」是正常的。
其余五行都会跳进 EXCEPTION 段。
「查出多行」那一行显示 5 行——部门 20 有五个人,SELECT INTO 拿不下,报 TOO_MANY_ROWS。
这是新手最常撞的一个:以为条件能锁定一行,其实没有。
「长度不够」那一行是 VALUE_ERROR:6 个字符塞不进 VARCHAR2(3)。
注意它不是 INVALID_NUMBER——那两个名字很像,但一个管「转换」,一个管「赋值」。
ZERO_DIVIDE 和 INVALID_NUMBER 是最好区分的两个。
一个是算术错,一个是类型转换错,错误号也差得远(01476 和 01722)。
这一段我用 Python 的异常来模拟,异常名字照 Oracle 的写。
SQLite 没有「命名异常」这个概念——它只有「查到几行」和「报错信息文本」两样东西。
所以「靠名字接异常」这件事,本身就是 PL/SQL 的一个优势。
30 秒实验
- 把「正好一行」那条的
ename改成'SMITH',确认它仍然正常。 再改成'smith'(小写),看会不会变成NO_DATA_FOUND—— Oracle 的字符串比较区分大小写,会。 - 给
select_into加一个try/except,把NO_DATA_FOUND换成「返回默认值 0」。 这就是最常见的兜底写法——但要想清楚「0」在业务上是不是个合理默认值。 - 写一个「插主键冲突就改成 update」的函数:先
insert,捕DUP_VAL_ON_INDEX再update。 这就是MERGE语句之前的老写法,理解它有助于看懂别人的老代码。 - 把
WHEN OTHERS THEN NULL加进一段代码里,然后故意让它出错。 看程序「安静地继续跑」是什么样——这就是线上最难查的那类 bug 的现场。
常见误区
WHEN OTHERS THEN NULL;。 异常被吞,没有日志,没有痕迹。 这是最坏的一种写法,比不写EXCEPTION还糟。- 以为
SELECT INTO查不到就返回 NULL。 它抛NO_DATA_FOUND。 SQL 和 PL/SQL 在这一点上完全不同。 - 自定义异常用
RAISE_APPLICATION_ERROR时号随便写。 必须落在 -20000 到 -20999,别占 Oracle 自己的号段。 - 捕到异常只记日志不
RAISE。 上层以为成功了,实际上什么都没做。这比直接报错难查一百倍。
自测三问(点开对答案)
1. `SELECT INTO` 查不到行会怎样?和普通 `SELECT` 有什么区别? —— **抛 `NO_DATA_FOUND` 异常。** **普通 `SELECT` 查不到只是空结果,`SELECT INTO` 查不到是异常**——因为它要求「正好一行」。 2. 为什么 `WHEN OTHERS THEN NULL;` 是最危险的写法? —— **异常被吞掉,程序安静地继续跑,没有任何日志。** **上层以为成功了,实际什么都没做**——这类问题比直接报错难查得多。 3. `RAISE_APPLICATION_ERROR` 的错误号有什么限制? —— **必须在 -20000 到 -20999 之间。** 这个区间是留给用户自定义的, **占用了 Oracle 自己的号段会冲突。**第 5 章到此为止,PL/SQL 的骨架搭完了。 下一篇进第 6 章:过程、函数、触发器和包——给这些块加上名字,存进数据库里。
- ☐ (本篇完,下一篇 6.1 存储过程)
相关笔记
- PL/SQL 游标
- PL/SQL 简介
- 子查询的用法
-
- 🐬 MySQL必知必会/00-索引
- 🏺 Oracle从入门到精通 · 索引