Postgresql源码(49)plpgsql函数编译执行流程分析总结

2022-05-25 10:54:08 浏览数 (1)

前文 《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可以做到通用表示一切可能的值。
代码语言:javascript复制
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;    

0 人点赞