引言
在国产化数据库替代浪潮中,KingbaseES凭借其与 Oracle PL/SQL 高兼容度,成为金融、政务等关键行业数据库迁移的首选。然而在实际迁移过程中,开发者常遇到"相同的PL/SQL代码在Oracle运行正常,在KingbaseES却报错"的困惑。

本文通过实践,梳理KingbaseES异常处理机制,从语法差异到运行时错误的完整解决方案,共同构建高可靠的国产数据库应用。
一、异常处理机制
异常(PL/SQL 运行时错误)可能来自设计错误、编码错误、硬件故障或其他来源。无法预先处理所有可能触发的异常,但可以编写异常处理程序,让程序在触发异常的情况下继续运行。
1.1 异常处理原理
KingbaseES三级异常处理架构
- 语法解析:通过扩展的SQL解析器识别Oracle PL/SQL特有语法
- 语义逻辑:将Oracle语义规则映射为KingbaseES执行逻辑
- 函数映射:内置同名函数实现无缝替换
当执行到错误语句时,控制流会跳转到对应的EXCEPTION块。系统在异常条件列表中匹配当前触发的异常,若匹配成功则执行对应的HandlerStatements,否则异常会向外层传播直至被捕获或终止程序。
1.2 封闭块
异常处理程序运行后,控制权转移到封闭块的下一条语句。如果没有封闭块会出现下面的情况
如果异常处理程序在子程序中,则将控制权返回给调用者调用之后的语句处
如果异常处理程序位于匿名块中,则控制权转移到主机环境
如果在没有异常处理程序的 PL/SQL 块中触发异常,则异常会传播。也就是说,异常会在连续的封闭块中向上抛出,直到一个 PL/SQL 块有一个异常处理程序或没有封闭块为止,如果没有异常处理程序,那么 PL/SQL 会向调用者或主机环境返回一个未处理的异常,这将决定最终的返回结果
1.3 异常种类
KingbaseES异常的种类分为:
系统预定义异常
系统预定义异常是 PL/SQL 已命名的异常,这些异常都有一个错误代码,且会在系统运行出错时隐式(自动)触发。
用户自定义异常
可以在任何 PL/SQL 匿名块、子程序或包的声明部分中声明自己的异常。例如,可以声明一个名为 invalid_number 的异常标记一个无效数字。 用户自定义异常必须显式的触发。
异常差异

下面将详细讲解两种异常
1.4 异常处理程序的优点
使用异常处理程序进行异常处理使程序更易于编写和理解,并降低了未处理异常的可能性。
如果没有异常处理程序,则必须检查所有可能触发的异常,并处理它。但这样很容易忽略可能出现的异常,尤其是在无法立即检测到异常的情况下(例如,在计算运行之前使用了错误数据可能无法检测到)。异常处理代码可以分散在整个程序中。
使用异常处理程序,不需要预先知道每个可能触发的异常或它可能发生的位置。只需在可能发生错误的每个块中包含一个异常处理模块。在异常处理模块中,用户可以编写处理特定错误或未知错误的异常处理程序。如果块中的任何地方(包括子块内)发生错误,则异常处理程序都会捕获并处理它。错误处理代码则会被隔离在块的异常处理部分。
一个存储过程使用单个异常处理程序来处理预定义异常 NO_DATA_FOUND,该异常可以出现在两个 SELECT INTO 语句中的任何一个中。
如果多个语句使用相同的异常处理程序,并且用户想知道哪个语句触发了异常,则可以使用变量辅助定位。
如果用户可以确定需要处理哪个异常,就可以为特定异常设置一个异常处理程序。用户还可以通过将语句放入具有自己的异常处理程序的块中来检查单个语句中的异常。
二、系统预定义异常
针对许多比较常见的异常以及系统运行时会触发的异常, PL/SQL 内部为其预定义了一个名称。例如:除零错误,对应的预定义异常名称为 DIVISION_BY_ZERO。当错误发生时,系统隐式(自动)抛出该异常。
PL/SQL 预定义异常'

预定义异常信息可通过数据库系统函数 SYS_GET_PREDEFINED_EXCEPTION_DETAIL 进行查询
2.1 匿名块中处理 DIVISION_BY_ZERO
sql
set serverout on
\set SQLTERM /
DECLARE
stock_price NUMBER := 9.73;
net_earnings NUMBER := 0;
pe_ratio NUMBER;
BEGIN
pe_ratio := stock_price / net_earnings; -- raises ZERO_DIVIDE exception
DBMS_OUTPUT.PUT_LINE('Price/earnings ratio = ' || pe_ratio);
EXCEPTION
WHEN ZERO_DIVIDE THEN
DBMS_OUTPUT.PUT_LINE('Company had zero earnings.');
pe_ratio := NULL;
END;
/
\set SQLTERM ;
结果:
sql
Company had zero earnings.
2.2 匿名块中避免 DIVISION_BY_ZERO
sql
\set SQLTERM /
DECLARE
stock_price NUMBER := 9.73;
net_earnings NUMBER := 0;
pe_ratio NUMBER;
BEGIN
pe_ratio :=
CASE net_earnings
WHEN 0 THEN NULL
ELSE stock_price / net_earnings
END;
END;
/
\set SQLTERM ;
三、用户自定义异常
用户可以在任何 PL/SQL 块,函数、存储过程或者包中声明一个异常。
语法格式:
sql
exception_name EXCEPTION ;
用户自定义异常必须被显式触发,有关异常触发的详细信息
用户自定义异常可以与一个错误码进行绑定,具体语法为:
sql
PRAGMA EXCEPTION_INIT (exception, error_code) ;
其中 exception 是用户自定义的异常, error_code 是大于 -1000000 且小于 0 的整数,error_code 可以是系统预定义异常的错误码。
四、重新声明预定义的异常
建议不要重新声明系统预定义的异常,即声明用户定义的异常名称与系统预定义的异常名称同名
如果重新声明了系统预定义的异常,那么本地声明将覆盖STANDARD 包的全局声明。此时只能使用 STANDARD包名来限定全局异常名称,否则异常处理程序无法处理全局异常。
重新声明预定义异常:
sql
DROP TABLE IF EXISTS t CASCADE;
CREATE TABLE t (c NUMBER(2,1));
在下面的块中,INSERT 语句隐式引发了 VALUE_ERROR 异常,异常处理程序捕获并处理了这个异常。
sql
set serverout on
\set SQLTERM /
DECLARE
default_number NUMBER := 0;
BEGIN
INSERT INTO t VALUES(TO_NUMBER('100.001', '9G999'));
EXCEPTION
WHEN VALUE_ERROR THEN
DBMS_OUTPUT.PUT_LINE('Substituting default value for invalid number.');
INSERT INTO t VALUES(default_number);
END;
/
\set SQLTERM ;
结果:
sql
Substituting default value for invalid number.
以下块重新声明了预定义的异常VALUE_ERROR。当 INSERT 语句隐式触发预定义异常 VALUE_ERROR 时,异常处理程序不会处理它。
sql
set serverout on
\set SQLTERM /
DECLARE
default_number NUMBER := 0;
value_error EXCEPTION; -- redeclare predefined exception
BEGIN
INSERT INTO t VALUES(TO_NUMBER('100.001', '9G999'));
EXCEPTION
WHEN VALUE_ERROR THEN
DBMS_OUTPUT.PUT_LINE('Substituting default value for invalid number.');
INSERT INTO t VALUES(default_number);
END;
/
\set SQLTERM ;
结果:
sql
ERROR: numeric field overflow
DETAIL: A field with precision 2, scale 1 must round to an absolute
value less than 10^1.
CONTEXT: SQL statement "INSERT INTO t VALUES(TO_NUMBER('100.001', '9G999'))"
PL/SQL function inline_code_block line 5 at SQL statement
如果在异常处理程序中使用包名 STANDARD 限定异常名称,则上述块中的异常处理程序将处理预定义的异常 VALUE_ERROR:
sql
set serverout on
\set SQLTERM /
DECLARE
default_number NUMBER := 0;
value_error EXCEPTION; -- redeclare predefined exception
BEGIN
INSERT INTO t VALUES(TO_NUMBER('100.001', '9G999'));
EXCEPTION
WHEN STANDARD.VALUE_ERROR THEN
DBMS_OUTPUT.PUT_LINE('Substituting default value for invalid number.');
INSERT INTO t VALUES(default_number);
END;
/
\set SQLTERM ;
结果:
sql
Substituting default value for invalid number.
五、显式触发异常
要显式触发常,使用 RAISE 语句或 存储过程RAISE_APPLICATION_ERROR。
5.1 RAISE 语句
RAISE 语句可以显式的触发一个异常。在异常处理程序之外,RAISE 语句必须指定异常名称。如果在异常处理程序内部,且省略了异常名称,那么该 RAISE 语句将重新引发当前正在处理的异常。
语法格式:
sql
RAISE [ exception ];
其中 exception 可以是已定义的用户自定义异常,也可以是系统预定义异常。
省略 exception 的 RAISE 子句仅可在异常处理模块中使用。
5.2 使用 RAISE 语句触发用户自定义的异常
存储过程声明了一个名为 past_due 的异常,使用 RAISE 语句显式触发它,并使用异常处理程序处理它。
声明、触发和处理用户定义的异常
sql
\set SQLTERM /
CREATE PROCEDURE account_status (
due_date DATE,
today DATE)
AUTHID DEFINER IS
past_due EXCEPTION; -- declare exception
BEGIN
IF due_date < today THEN
RAISE past_due; -- explicitly raise exception
END IF;
EXCEPTION
WHEN past_due THEN -- handle exception
RAISE NOTICE 'Account past due.';
END;
/
\set SQLTERM ;
调用存储过程:
sql
\set SQLTERM /
BEGIN
account_status (TO_DATE('01-JUL-2010', 'DD-MON-YYYY'),
TO_DATE('09-JUL-2010', 'DD-MON-YYYY'));
END;
/
\set SQLTERM ;
输出结果:
sql
NOTICE: Account past due.
5.3 使用 RAISE 语句触发系统预定义的异常
系统预定义异常通常由系统运行时隐式触发,但也可以使用 RAISE 语句显式地触发它们。当一个预定义异常拥有对应的异常处理程序时,无论是显式触发还是隐式触发,都会触发异常处理程序对相应的异常进行处理。
存储过程不论显式或是隐式触发预定义异常 VALUE_ERROR,异常处理程序始终都会处理它。
触发系统预定义异常
sql
DROP TABLE IF EXISTS t CASCADE;
CREATE TABLE t (c NUMBER(2,1));
\set SQLTERM /
CREATE PROCEDURE p (n NUMBER) AUTHID DEFINER IS
default_number NUMBER := 0;
BEGIN
IF n < 0 THEN
RAISE VALUE_ERROR; -- 显式触发
ELSE
INSERT INTO t VALUES(TO_NUMBER('100.001', '9G999')); -- 隐式触发
END IF;
EXCEPTION
WHEN VALUE_ERROR THEN
RAISE NOTICE 'Substituting default value for invalid number.';
INSERT INTO t VALUES(default_number);
END;
/
\set SQLTERM ;
调用存储过程:
sql
\set SQLTERM /
BEGIN
p(-1);
END;
/
\set SQLTERM ;
输出结果:
sql
NOTICE: Substituting default value for invalid number.
再次调用存储过程:
sql
\set SQLTERM /
BEGIN
p(1);
END;
/
\set SQLTERM ;
输出结果:
sql
NOTICE: Substituting default value for invalid number.
六、总结与展望
KingbaseES通过完善的异常处理机制,实现了从语法兼容到运行时行为的全面Oracle兼容。其三级异常处理架构、丰富的预定义异常、灵活的自定义异常能力,配合智能诊断工具,构建了完整的异常处理生态体系。