【GaussDB】507内核新特性-PLSQL字节码(一)
这个字节码内容较多,一篇讲不完,这篇先讲基础概念和正常使用的情况
官方文档介绍
字节码和汇编指令类似,是为PL/SQL专门定义的一组用于实现PL/SQL语义的指令,通过将PL/SQL语句转化为字节码完成用户的指令。一个字节码对应一个原子化的操作,一条语句对应一个或多个字节码。通过逐条解析存储过程内的语句,将存储过程语句的执行转化为顺序执行字节码组成的数组。
字节码通过将传统存储过程基于AST(Abstract Syntax Tree,抽象语法树)的解释执行引擎转变为基于字节码的解释执行引擎,可以有效提升支持场景下存储过程的执行性能。通过调整GUC参数plsql_code_type,可以验证字节码框架对存储过程的性能提升。当GUC参数plsql_code_type设置为BYTECODE时,存储过程在支持字节码的场景下执行路径为字节码框架,不支持字节码的场景下执行路径为传统AST框架;设置为INTERPRETED时,存储过程执行路径为传统AST框架。
规格
- 通过GUC参数plsql_code_type进行控制,设置为INTERPRETED时,通过传统AST框架执行存储过程;设置为BYTECODE时,通过字节码框架执行存储过程,当存储过程存在不支持字节码的场景,则存储过程以传统引擎框架进行执行;不支持将GUC参数plsql_code_type设置为其他值。
- 该特性仅在A兼容模式数据库中生效,当A兼容性参数behavior_compat_options设置中包含compat_cursor、allow_procedure_compile_check、proc_outparam_override、dynamic_sql_compat、proc_outparam_transfer_length以及varray_compat参数选项时,支持执行字节码。
约束
- 目前不支持的DFX功能:PLDEBUGGER(打开PLDEBUGGER时,函数或存储过程将通过传统引擎执行),PG_STAT_USER_FUNCTIONS视图(仅统计通过传统引擎执行存储过程或函数的时间)。
- 不支持PACKAGE实例化。
- 不支持匿名块。
- 不支持TRIGGER。
- 不支持返回集合的函数。
- 不支持高级包以及系统函数。
- 不支持带有出参的FUNCTION在表达式中嵌套调用。
- 不支持RETURN复合类型的带有出参的FUNCTION。
- 不支持PROCEDURE显式指定RETURN值(正常用法PROCEDURE无需RETURN值)。
- 不支持子程序。
- 主事务不支持嵌套调用自治事务的表达式执行字节码(自治事务创建的新SESSION执行存储过程可以通过字节码框架执行)。
- 不支持SUBTYPE类型。
- 不支持游标循环语句SELECT返回的列数与语句中的游标变量列数不一致。
- 不支持RECORD变量的读取和赋值列值(如:va record; va.c1 := 1;)。
- 不支持PACKAGE中RECORD变量的读取和复制。
- 不支持REFCURSOR类型的出入参转换为其他类型(例如:REFCURSOR转换为VARCHAR2,或进行拼接运算操作等)。
- 不支持伪类型(具体请参见伪类型)作为函数或存储过程出入参。
- 不支持循环游标语句使用跨PACKAGE的CURSOR变量。
- 不支持包含对入参进行赋值的存储过程或函数。
- 不支持BULK COLLECT语句。
- 不支持FORALL批量查询语句带有SAVE EXCEPTIONS。
- 不支持函数或存储过程的参数默认值为PACKAGE变量。
- 不支持存储过程或函数OUT出参具有多个列或带有数组下标的复杂变量(如:type arrayInt is varray(16) of int; val arrayInt; val(1) = 1;)。
- 对于存储过程或函数INOUT参数,仅不包含运算的单一参数类型变量支持字节码框架。
注意约束中说的"不支持PACKAGE实例化",指的是一个字节码对象的维度,package中的存储过程和函数是可以支持字节码的,也就是说,字节码是按pg_proc中的oid来生成的。"不支持匿名块",但实际上存储过程里嵌套匿名块是支持的,这里指的是直接执行的匿名块是不支持的,因为它没有pg_proc的记录。其他的后面再看要不要举例,毕竟文档里缺了对于不支持场景的例子,写测试用例和验证挺麻烦的,而且我暂不确定GaussDB这些一句话的描述AI是否真的能真的看得懂。
关于GUC参数
光GUC参数就埋了个暗坑,GaussDB从506版本起,a_format_dev_version默认为s7,强制覆盖了behavior_compat_options的配置,而覆盖的这份配置,不包含字节码要求的allow_procedure_compile_check和varray_compat,因此需要手动把a_format_dev_version改为s6,然后再根据需要显式配置behavior_compat_options。
gs_guc reload -I all -N all -c "a_format_dev_version='s6'"
gs_guc reload -I all -N all -c "behavior_compat_options='aformat_null_test,aformat_regexp_match,allow_procedure_compile_check,bind_procedure_searchpath,compat_cursor,convert_string_digit_to_numeric,correct_to_number,current_sysdate,display_leading_zero,dynamic_sql_compat,enable_bpcharlikebpchar_compare,enable_case_when_alias,enable_crosstype_integer_operator,enable_ora_joinop_in_updatestmt,enable_use_ora_timestamptz,end_month_calculate,forbid_package_function_with_prefix,forbid_skip_tableof_empty_str_elem,forbid_update_multi_same_tables,plsql_rollback_keep_user,plsql_security_definer,plstmt_implicit_savepoint,proc_implicit_for_loop_variable,proc_outparam_override,proc_outparam_transfer_length,rownum_type_compat,show_full_error_lineno,sys_function_without_brackets,tableof_elem_constraints,time_constexpr_compact,truncate_numeric_tail_zero,unbind_divide_bound,varray_compat'"
gs_guc reload -I all -N all -c "plsql_code_type='BYTECODE'"
另外,字节码主要的控制参数为文档里字节码这个章节里提到的plsql_code_type
plsql_code_type
参数说明:控制存储过程在支持字节码的场景下是否按照基于字节码的执行框架进行执行。该参数可在PDB级别设置。
参数类型:枚举类型
参数单位:无
取值范围:
•INTERPRETED:表示存储过程按照基于AST的执行框架执行。
•BYTECODE:表示存储过程在支持字节码的场景下,将按照基于字节码的执行框架执行,否则按照基于传统AST的执行框架执行。
默认值:INTERPRETED。
设置方式:该参数属于USERSET类型参数,请参见表1中对应设置方法进行设置。
设置建议:推荐使用默认值。
设置不当的风险与影响:请在充分理解参数含义,并经过测试验证后进行修改。
但是搜索整个文档,发现还有个字节码相关的参数
expr_engine
参数说明:控制表达式的执行方式。该参数可在PDB级别设置。
参数类型:字符串
参数单位:无
取值范围:
•EXPR_TREE:表示使用表达式的递归引擎。
•EXPR_FLATTEN:表示使用表达式的展平引擎。
•EXPR_BYTECODE:表示使用表达式的字节码引擎。
默认值:EXPR_FLATTEN。在PDB场景内,若未设置该参数,则继承来自全局的设置。
设置方式:该参数属于USERSET类型参数,请参见表1中对应设置方法进行设置。
设置建议:推荐使用默认值。
设置不当的风险与影响:使用EXPR_FLATTEN或EXPR_BYTECODE会少量增加优化器运行开销。
在整个文档中搜索这个参数,除了这个参数本身的说明外,只能得到这么几个信息
- 分布式中不支持EXPR_BYTECODE/EXPR_FLATTEN。
- M模式不支持EXPR_BYTECODE/EXPR_FLATTEN。
- 部分执行计划相关演示之前手动配置了
SET expr_engine = EXPR_TREE;
第3点配置的目的是为了避免使用表达式展平,让输出的执行计划和SQL中的逻辑更匹配,否则实际执行计划和演示不一样,还得进行额外的解释。也就是说,文档中实际上未对expr_engine这个参数表达式的字节码做任何示例。
我粗略测了几个场景,未能观测到EXPR_BYTECODE是否生效,因此本文暂且不管expr_engine,仅针对plsql_code_type来进行测试。
如何识别是否支持字节码
判断一个存储过程是否支持字节码,有两种方式,但这两种方式都要求存储过程要实际执行一次。
- 方法一: 创建存储过程后,执行一次存储过程,然后使用
dump_plsql_bytecode(function_oid Oid)函数进行查询,输出存储过程oid,返回一行记录,其中字段bytecode显示为Not supported,则表示不支持,如果支持,则会显示具体的字节码信息。
[0] iconst0
[1] castfunction func: numeric
[14] storen_copy1 10
[19] exec_stmt_begin
[40] loopwhile
[45] exec_stmt_begin
...
- 方法二: 打开字节码的模块日志,然后执行一次存储过程,就会输出对应的日志
set logging_module = 'on(BYTECODE)';
set client_min_messages = log;
输出中包含LOG: Start ByteCode!就是支持(注意不一定会输出This function supports bytecode);
输出中包含bytecode doesn't support就是不支持。不支持时会具体说明由于什么原因不支持,部分不支持原因会具体到代码行。
与Global Plsql Cache的关系
507版本集中式默认开启Global Plsql Cache功能,只要一个会话执行过一个支持字节码的存储过程,其他会话是可以共享这个字节码的,此时通过dump_plsql_bytecode查询,pid,sessionid,global_sessionid 这三个字段是null。
测试字节码
功能测试
下面通过一个简单的例子来观察这个字节码的执行效果。
gaussdb=> set plsql_code_type=BYTECODE;
SET
gaussdb=> CREATE OR REPLACE PROCEDURE bc_loop(i int) as
gaussdb$> L_COUNT NUMBER:=0;
gaussdb$> begin
gaussdb$> LOOP
gaussdb$> IF L_COUNT > i THEN
gaussdb$> RAISE INFO 'count is %',L_COUNT;
gaussdb$> EXIT;
gaussdb$> END IF;
gaussdb$> L_COUNT:=L_COUNT+1;
gaussdb$> END LOOP;
gaussdb$> END;
gaussdb$> /
CREATE PROCEDURE
## 创建完后查不到字节码
gaussdb=> SELECT * FROM pg_catalog.dump_plsql_bytecode('bc_loop'::regproc);
(No rows)
gaussdb=> alter procedure bc_loop compile;
ALTER PROCEDURE
## 手动编译也查不到字节码
gaussdb=> SELECT * FROM pg_catalog.dump_plsql_bytecode('bc_loop'::regproc);
(No rows)
gaussdb=> call bc_loop(10);
INFO: count is 11
-[ RECORD 1 ]
bc_loop |
## 必须执行一次后才能查到字节码
gaussdb=> SELECT * FROM pg_catalog.dump_plsql_bytecode('bc_loop'::regproc);
-[ RECORD 1 ]----+---------------------------------------------------------------------------------------------
pid |
sessionid |
global_sessionid |
databaseid | 13764
pkg_id | 0
func_id | 16948
func_signature | name: bc_loop(integer), fn_collation: 0, search_path: addUser: false, schemas: 16577,11,2200
bytecode | [0] iconst0
| [1] castfunction func: numeric
| [14] storen_copy1 10
| [19] exec_stmt_begin
| [40] loopwhile
| [45] exec_stmt_begin
| [66] loadn 10
| [71] loadn 0
| [76] function func: numericint4gt
| [89] if_or_when 96
| [94] exec_stmt_begin
| [115] exec_stmt_treebased
| [128] exec_stmt_end
| [141] exec_stmt_begin
| [162] exec_stmt_end
| [175] goto 94
| [180] goto 5
| [185] exec_stmt_end
| [198] exec_stmt_begin
| [219] loadn 10
| [224] const64
| [233] function func: numeric_add
| [246] storen_copy1 10
| [251] exec_stmt_end
| [264] goto -224
| [269] exec_stmt_end
| [282] exec_stmt_begin
| [303] exec_stmt_end
| [316] return0 5
|
性能对比测试
进行5*2轮开关字节码的对比测试
gaussdb=> \timing on
Timing is on.
gaussdb=> set plsql_code_type=BYTECODE;
SET
Time: 0.377 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 5.299 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 8.448 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 5.042 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 4.980 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 6.513 ms
gaussdb=> set plsql_code_type=INTERPRETED;
SET
Time: 0.603 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 15.091 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 14.844 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 14.945 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 18.484 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 15.694 ms
gaussdb=> set plsql_code_type=BYTECODE;
SET
Time: 0.457 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 6.742 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 5.969 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 5.616 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 4.958 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 5.164 ms
gaussdb=> set plsql_code_type=INTERPRETED;
SET
Time: 0.462 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 15.028 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 14.992 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 15.332 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 14.408 ms
gaussdb=> call bc_loop(10000);
INFO: count is 10001
-[ RECORD 1 ]
bc_loop |
Time: 16.702 ms
gaussdb=>
测试明细
| 批次 | 第 1 次 (ms) | 第 2 次 (ms) | 第 3 次 (ms) | 第 4 次 (ms) | 第 5 次 (ms) |
|---|---|---|---|---|---|
| BYTECODE 首轮 | 5.299 | 8.448 | 5.042 | 4.980 | 6.513 |
| INTERPRETED 首轮 | 15.091 | 14.844 | 14.945 | 18.484 | 15.694 |
| BYTECODE 二轮 | 6.742 | 5.969 | 5.616 | 4.958 | 5.164 |
| INTERPRETED 二轮 | 15.028 | 14.992 | 15.332 | 14.408 | 16.702 |
汇总统计
| 编译模式 | 测试次数 | 平均耗时 (ms) | 性能提升 |
|---|---|---|---|
| INTERPRETED | 10 | 15.552 | — |
| BYTECODE | 10 | 5.873 | — |
| 对比 | +62.23% |
从测试结果来看,效果是很明显的,而且多轮测试也比较稳定
真实业务逻辑测试
简单测试验证通过了,接下来上真实业务逻辑。
正好我之前有用AI分析过一个"CRR (Cox-Ross-Rubinstein) 二叉树期权定价模型"在GaussDB的PLSQL算法,《美式期权二叉树算法 — 七语言实现与交叉验证报告》 ,可以说是AI已经用尽了浑身解数,最终得出结论:
claude opus 4.8:但要达到 Oracle 的 4ms,差距仍然是 GaussDB PL/SQL 引擎的解释执行效率问题——Oracle 对 PL/SQL 有深度优化(原生编译、内联、循环展开等)。如果 GaussDB 支持的话,可以查看是否有类似 Oracle 的
PLSQL_OPTIMIZE_LEVEL或PLSQL_CODE_TYPE=NATIVE的编译选项。
GaussDB的PLSQL_CODE_TYPE这不就来了嘛!
这里使用AI优化过的最终版本作为基准,再开启GaussDB的字节码特性,看看是否还能有性能提升
CREATE OR REPLACE FUNCTION binomial_amer_opt_v3_opt (
p_s0 float8,
p_k float8,
p_t float8,
p_r float8,
p_sigma float8,
p_n INTEGER,
p_is_call INTEGER, -- 1=看涨, 0=看跌
p_is_amer INTEGER -- 1=美式, 0=欧式
) RETURNS float8 AS $$
DECLARE
v_dt float8;
v_u float8;
v_d float8;
v_p float8;
v_disc float8;
v_dd float8;
v_1mp float8;
v_sign float8;
v_signK float8;
v_intrStep float8;
v_base float8;
v_intr float8;
v_cont float8;
v_opt float8[];
i INTEGER;
j INTEGER;
BEGIN
v_dt := p_t / p_n;
v_u := exp(p_sigma * sqrt(v_dt));
v_d := 1.0 / v_u;
v_p := (exp(p_r * v_dt) - v_d) / (v_u - v_d);
v_1mp := 1.0 - v_p;
v_disc := exp(-p_r * v_dt);
v_dd := v_d * v_d;
IF p_is_call = 1 THEN v_sign := 1.0; ELSE v_sign := -1.0; END IF;
v_signK := v_sign * p_k;
v_intrStep := v_signK * (v_dd - 1.0);
v_opt := array_fill(0.0, ARRAY[p_n + 2]);
FOR i IN 1 .. p_n + 1 LOOP
v_opt[i] := greatest(0.0, v_sign * (p_s0 * power(v_u, p_n) * power(v_dd, i - 1) - p_k));
END LOOP;
v_base := p_s0 * power(v_u, p_n) * v_d;
FOR i IN REVERSE p_n .. 1 LOOP
v_intr := v_sign * v_base - v_signK;
FOR j IN 1 .. i LOOP
v_cont := v_disc * (v_p * v_opt[j] + v_1mp * v_opt[j + 1]);
IF p_is_amer = 1 THEN v_opt[j] := greatest(v_cont, v_intr);
ELSE v_opt[j] := v_cont; END IF;
v_intr := v_intr * v_dd + v_intrStep;
END LOOP;
v_base := v_base * v_d;
END LOOP;
RETURN v_opt[1];
END;
$$ LANGUAGE plpgsql;
create or replace procedure test_benchmark(n int) is
declare
v_v1 number;
v_start timestamp;
begin
v_start := clock_timestamp();
for i in 1..n loop
v_v1 := binomial_amer_opt_v3_opt(100, 100, 1.0, 0.05, 0.2, 200, 0, 1);
end loop;
raise notice '%',clock_timestamp()-v_start;
end;
/
对比测试4轮,每轮调用算法函数100次
gaussdb=> set plsql_code_type=BYTECODE;
SET
gaussdb=> call test_benchmark(100);
NOTICE: 00:00:02.141342
test_benchmark
----------------
(1 row)
gaussdb=> call test_benchmark(100);
NOTICE: 00:00:01.99412
test_benchmark
----------------
(1 row)
gaussdb=> call test_benchmark(100);
NOTICE: 00:00:02.039277
test_benchmark
----------------
(1 row)
gaussdb=> call test_benchmark(100);
NOTICE: 00:00:02.019245
test_benchmark
----------------
(1 row)
gaussdb=> set plsql_code_type=INTERPRETED;
SET
gaussdb=> call test_benchmark(100);
NOTICE: 00:00:05.816567
test_benchmark
----------------
(1 row)
gaussdb=> call test_benchmark(100);
NOTICE: 00:00:05.824375
test_benchmark
----------------
(1 row)
gaussdb=> call test_benchmark(100);
NOTICE: 00:00:05.693762
test_benchmark
----------------
(1 row)
gaussdb=> call test_benchmark(100);
NOTICE: 00:00:05.794074
test_benchmark
----------------
(1 row)
补测一轮调用算法函数1000次的
gaussdb=> set plsql_code_type=BYTECODE;
SET
gaussdb=> call test_benchmark(1000);
NOTICE: 00:00:20.146643
test_benchmark
----------------
(1 row)
gaussdb=> set plsql_code_type=INTERPRETED;
SET
gaussdb=> call test_benchmark(1000);
NOTICE: 00:00:57.356876
test_benchmark
----------------
(1 row)
| plsql_code_type | 调用函数次数 | 第 1 次 (s) | 第 2 次 (s) | 第 3 次 (s) | 第 4 次 (s) |
|---|---|---|---|---|---|
| BYTECODE | 100 | 2.141 | 1.994 | 2.039 | 2.019 |
| INTERPRETED | 100 | 5.817 | 5.824 | 5.694 | 5.794 |
| BYTECODE | 1000 | 20.147 | |||
| INTERPRETED | 1000 | 57.357 |
通过以上多种测试,基本可以看出使用字节码后的耗时差不多是原本耗时的三分之一稍多一点。
字节码支持场景
官方文档有介绍字节码支持哪些场景,我就不搬过来了:
- 表达式 - https://doc.hcs.huawei.com/db/zh-cn/gaussdbqlh/26.861.0/rf-cent/gaussdb-38-0734.html
- 语句 - https://doc.hcs.huawei.com/db/zh-cn/gaussdbqlh/26.861.0/rf-cent/gaussdb-38-0735.html
下一篇讲讲字节码的使用约束。
未完待续~

