前文 《Postgresql源码(41)plpgsql函数编译执行流程分析》 《Postgresql源码(46)plpgsql中的变量类型及对应关系》 《Postgresql源码(49)plpgsql函数编译执行流程分析总结》
以一个带简单赋值、出入参、变量有默认值的普通函数为例,分析执行过程。触发器等其他函数的执行过程大同小异,核心流程基本不变,就是多了几个默认工具变量。相比《Postgresql源码(46)plpgsql中的变量类型及对应关系》这篇总结更清晰简单。
例子:
代码语言:javascript复制CREATE OR REPLACE FUNCTION sn(x int, y int, OUT sum int, OUT prod int) AS $$
DECLARE
a integer DEFAULT 32;
b CONSTANT integer := 10;
BEGIN
sum := x y 1;
prod := x * y * b;
raise notice '(%, %)', sum, prod;
END;
$$ LANGUAGE plpgsql;
select sn(2, 3);
整体流程理解总结
- src/pl/plpgsql下是plpgsql语言的功能模块。模块使用PG的language框架实现,pl与调用者部分解耦,SQL主流程通过FMT回调pl相关函数完成plpgsql的编译、运行。
- 例如使用psql创建一个函数,在进入pl代码时,一般情况下函数已经经过psql的语法解析(规则是见到
全部放过发到server这里解析主要是发现语句什么时候结束)、server的gram.y的语法解析(函数代码整理包装放到pg_proc系统表里面),在pl中要经历两大步骤:编译、执行
- 【pl编译】过程会重新把函数的代码从系统表中取出,用pl自己的pl_gram.y解析,识别语法结构中的各部分,包装成语法块,然后把语法块串在链表上返回链表头,后面执行的时候遍历链表即可;还有一部分功能是维护datums数组和ns_top链表,分别记录了运行时需要的变量和命名空间信息。
- 编译具体流程
- 系统表拿到源码;
- 初始化命名空间ns_top、变量空间datums;
- 函数参数、返回值构造进入ns_top、datums;
- 调用yacc解析语法树,并构造语法块list;
- 所有信息拷贝到function结构体中;
- function记录到htab中;
- 编译完成。
- 编译具体流程
- 【pl执行】执行前会给相关变量赋值,执行时会for循环遍历语法块链表,根据语法块类型走不同分支;执行中可能经常会递归进入语法块,因为大部分语法结构可以互相包含,比如函数中的循环结构中包含判断。
- 执行具体过程:
- 组装运行状态estate;
- 拷贝变量datums;
- func->fn_argvarnos找到入参在datums中的位置然后入参赋值;
- 然后进入exec_stmt_block:
- 初始化当前语法块的所有变量(使用block的n_initvars、initvarnos找到datums变量,然后赋值即可)。如果变量有默认值,使用exec_assign_expr把默认值当做SQL执行出结果,赋值给变量。
- 当前块有没有异常处理,没有的话直接执行;有的话需要走try/cache流程(使用block的body部分);
- 开始遍历body链表的第一个元素,赋值。这里的值都是使用PLpgSQL_expr表示的,因为值可以是一个语句
- 执行具体过程:
上面是整体流程的直观认识,下面做一些细节分析
编译:do_compile
总结:系统表拿到源码;初始化命名空间ns_top、变量空间datums;函数参数、返回值构造进入ns_top、datums;调用yacc解析语法树,并构造语法块list;所有信息拷贝到function结构体中;function记录到htab中;编译完成。
do_compile触发器的编译流程会有所差异,这里只分析普通函数的编译过程:
代码语言:javascript复制// 所有信息存入function,then add it to function hash table
do_compile
...
// 拿到源码
prosrcdatum = SysCacheGetAttr(PROCOID, procTup,Anum_pg_proc_prosrc, &isnull)
proc_source = TextDatumGetCString(prosrcdatum)
...
// 【初始化语法解析】
plpgsql_scanner_init(proc_source)
...
// 【初始化命名空间】
plpgsql_ns_init
// {itemtype = PLPGSQL_NSTYPE_LABEL, itemno = 0, prev = 0x0, name = 0x1c992c0 "sn"}
plpgsql_ns_push(NameStr(procStruct->proname), PLPGSQL_LABEL_BLOCK)
plpgsql_start_datums
...
switch (function->fn_is_trigger)
case PLPGSQL_NOT_TRIGGER
...
get_func_arg_info
// 【处理参数】
for (i = 0; i < numargs; i )
...
plpgsql_build_variable
...
// 处理结束
// 函数采纳数: (x int, y int, OUT sum int, OUT prod int)
// ns_top值: prod->$4->sum->$3->y->$2->x->$1->sn
// ns_top类型: | ---- PLPGSQL_NSTYPE_VAR ---- | ---- PLPGSQL_NSTYPE_LABEL ---- |
// datums有4个:
// {dtype = PLPGSQL_DTYPE_VAR, dno = 0, refname = 0x1c99360 "x" ...
// {dtype = PLPGSQL_DTYPE_VAR, dno = 1, refname = 0x1c99468 "y" ...
// {dtype = PLPGSQL_DTYPE_VAR, dno = 2, refname = 0x1c99608 "sum" ...
// {dtype = PLPGSQL_DTYPE_VAR, dno = 3, refname = 0x1c997a8 "prod" ...
// 如果多于一个Out,创建一个PLpgSQL_row类型组装所有返回值,所以datum在增加一个row
if (num_out_args > 1 || (num_out_args == 1 && function->fn_prokind == PROKIND_PROCEDURE))
...
// {dtype = PLPGSQL_DTYPE_ROW, dno = 4, refname = 0x7f3266610ea8 "(unnamed row)" ...
plpgsql_adddatum
...
// 【构造返回值】
// 增加一个found
// {itemtype = PLPGSQL_NSTYPE_VAR, itemno = 5, prev = 0x1c99800, name = 0x1c999f0 "found"}
// {dtype = PLPGSQL_DTYPE_VAR, dno = 5, refname = 0x1c999c0 "found" ...
plpgsql_build_variable
// 【开始语法解析】plpgsql_yyparse下一章展开
parse_rc = plpgsql_yyparse()
// 语法解析的所有语法块都串在plpgsql_parse_result上
function->action = plpgsql_parse_result
plpgsql_scanner_finish
// 没有return,给语法块list增加一个dummy return
add_dummy_return(function);
// plpgsql_Datums 拷贝到 function->datums
plpgsql_finish_datums
// 所有信息存入hash
plpgsql_HashTableInsert(function, hashkey);
// 编译完成
return function;
执行:plpgsql_exec_function
总结:
组装运行状态estate;拷贝变量datums;func->fn_argvarnos找到入参在datums中的位置然后入参赋值;
然后进入exec_stmt_block:
1、初始化当前语法块的所有变量(使用block的n_initvars、initvarnos找到datums变量,然后赋值即可)。如果变量有默认值,使用exec_assign_expr把默认值当做SQL执行出结果,赋值给变量。
2、当前块有没有异常处理,没有的话直接执行;有的话需要走try/cache流程(使用block的body部分);
3、开始遍历body链表的第一个元素,赋值。这里的值都是使用PLpgSQL_expr表示的,因为值可以是一个语句
其他:
- estate和function是什么关系:function在编译时已经包含了函数的所有静态信息,这里estate包含function并从之中解析出一些运行需要的信息放到estate中)
- block->initvarnos:block初始化的时候找变量;func->fn_argvarnos:参数初始化的时候找变量;两个数组记录的都是datums数组的位置,指向一个变量
- 所有的数值都用PLpgSQL_expr表示,expr->query可能是一个数也可能是一个SQL,expr可以做到通用表示一切可能的值。
plpgsql_exec_function
...
// 组装estate
plpgsql_estate_setup
...
// 变量信息拷贝:func->datums到estate->datums
copy_plpgsql_datums
// 入参赋值
for (i = 0; i < func->fn_nargs; i )
// 拿到入参位置
int n = func->fn_argvarnos[i]
switch (estate.datums[n]->dtype)
// 普通变量
case PLPGSQL_DTYPE_VAR:
PLpgSQL_var *var = (PLpgSQL_var *) estate.datums[n]
assign_simple_var(&estate, var, fcinfo->args[i].value, fcinfo->args[i].isnull, false)
// 赋值后 x=2 y=3
// var = {dtype = PLPGSQL_DTYPE_VAR, dno = 0, refname = 0x1c99360 "x",
// ... value = 2, isnull = false, freeval = false, promise = PLPGSQL_PROMISE_NONE}
// 给found赋值false
exec_set_found(&estate, false)
// 开始执行
rc = exec_toplevel_block(&estate, func->action)
exec_stmt_block(estate, block)
// 【第一步】初始化当前语法块的所有变量(使用block的n_initvars、initvarnos找到datums变量,然后赋值即可)
for (i = 0; i < block->n_initvars; i )
int n = block->initvarnos[i];
PLpgSQL_datum *datum = estate->datums[n]
// 有默认值需要赋值
if (var->default_val == NULL)
...
else
// 【第一步】变量有默认值,使用exec_assign_expr把默认值当做SQL执行出结果,赋值给变量
// var->default_val是一个expr,expr相当于一个SQL语句,需要调用SQL引擎执行一遍
exec_assign_expr(estate, (PLpgSQL_datum *) var, var->default_val)
// 第一次跑生成执行计划
exec_prepare_plan
// 执行SQL
value = exec_eval_expr
// 赋值
exec_assign_value
...
// 【第二步】当前块有没有异常处理,没有的话直接执行;有的话需要走try/cache流程;(使用block的body部分)
if (block->exceptions)
...
else
// 【第二步】调用exec_stmts开始执行语法块。body应该是一个4个元素的list,包含三句函数体中写的赋值和一句后加的return
rc = exec_stmts(estate, block->body);
// stmts == block->body,开始遍历
foreach(s, stmts)
switch (stmt->cmd_type)
PLPGSQL_STMT_ASSIGN
// 【第三步】开始遍历body链表的第一个元素,赋值
rc = exec_stmt_assign(estate, (PLpgSQL_stmt_assign *) stmt)
// 【第三步】这里的值都是使用PLpgSQL_expr表示的,因为值可以是一个语句。
// 这样看这个函数就比较好理解了,第一个参数是运行时变量;第二个是变量;第三个是值。
exec_assign_expr(estate, estate->datums[stmt->varno], stmt->expr)
...
return PLPGSQL_RC_OK;