前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Postgresql源码(49)plpgsql函数编译执行流程分析总结

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

作者头像
mingjie
发布2022-05-25 10:54:08
1.2K0
发布2022-05-25 10:54:08
举报
文章被收录于专栏:Postgresql源码分析

前文 《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;    
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-05-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 整体流程理解总结
  • 编译:do_compile
  • 执行:plpgsql_exec_function
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档