openGauss 数据库源码解析系列文章——执行器解析(2.1)
openGauss 数据库源码解析系列文章——执行器解析(2.1)
四、表达式计算
表达式计算对应的代码源文件是“execQual.cpp”,openGauss 处理 SQL 语句中的函数调用、计算式和条件表达式时需要用到表达式计算。
表达式的表示方式和查询计划树的计划节点类似,通过生成表达式计划来对每个表达式节点进行计算。表达式继承层次中的公共根类为 Expr 节点,其他表达式节点都继承 Expr 节点。表达式状态的公共根类为 ExprState,记录了表达式的类型以及实现该表达式节点的函数指针。表达式内存上下文类为 ExprContext,ExprContext 充当了计划树节点中 Estate 的角色,表达式计算过程中的参数以及表达式所使用的内存上下文都会存放到此结构中。
表达式计算对应的主要结构体代码如下:
表达式计算的过程分为 3 个部分:初始化、执行和清理。初始化的过程使用统一接口 ExecInitExpr,根据表达式的类型选择不同的处理方式,生成表达式节点树。执行过程使用统一接口宏 ExecEvalExpr,执行过程类似于计划节点的递归方式。
4.1 初始化阶段
ExecInitExpr 函数的作用是在执行的初始化阶段,准备要执行的表达式树。根据传入的表达式 node tree,来创建并返回 ExprState tree。在真正的执行阶段会根据 ExprState tree 中记录的处理函数,递归地执行每个节点。ExecInitExpr 函数的核心代码如下:
ExecInitExpr 函数主要执行流程如下。
(1) 判断输入的 node 节点是否为空,若为空,则直接返回 NULL,表示没有表达式限制。
(2) 根据输入的 node 节点的类型初始化变量 evalfunc 即 node 节点对应的执行函数,若节点存在参数或者表达式,则递归调用 ExecInitExpr 函数,最后生成 ExprState tree。
(3) 返回 ExprState tree,在执行表达式的时候会根据 ExprState tree 来递归执行。
ExecInitExpr 函数流程如图 12 所示。
图 12 ExecInitExpr 函数执行流程
4.2 执行阶段
执行阶段主要是根据宏定义 ExecEvalExpr 递归调用执行函数。在计算时的核心函数包括 ExecMakeFunctionResult 和 ExecMakeFunctionResultNoSets,通过这两个函数计算出表达式的结果并返回。其他的表达式计算函数还包括 ExecEvalFunc、ExecEvalOper、ExecEvalScalarVar、ExecEvalConst、ExecQual、ExecProject 等,这些函数分别对应不同的表达式的类型或者参数类型,通过不同的逻辑来处理获取的计算结果。
执行过程就是上层函数调用下层函数。首先下层函数根据参数类型获取相应的数据,然后上层函数通过处理数据得到最后的结果,最后根据表达式逻辑返回结果。
通过一个简单的 SQL 语句介绍一下表达式计算的函数调用过程,每种 SQL 语句的执行流程不完全一致,此示例仅供参考。例句:“SELECT * FROM s WHERE s.a<3 or s.b<3;”。具体流程如下。
(1) 根据表达式“s.a<3 or s.b<3”确认第一步调用 ExecQual 函数。
(2) 由于本次表达式是 or 语句,所以需要将表达式传入到 ExecEvalOr 函数计算,在 ExecEvalOr 函数中采用 for 循环依次对子表达式“s.a<3”和“s.b<3”计算,将子表达式传入到下一层函数中。
(3) ExecEvalOper 函数根据子表达式的返回值是否为 set 集来调用下一层函数,计算子表达式的结果。
(4) ExecMakeFunctionResultNoSets 函数中获取子表达式中的参数的值,“s.a”和“3”分别通过 ExecEvalScalarVar 函数和 ExecEvalConst 函数来获取,获取到参数之后计算表达式结果,若 s.a<3 本次计算返回 true,否则返回 false,并依次向上层返回结果。
函数调用流程图如图 13 所示。
图 13 函数调用执行流程
执行阶段所有函数都共享此调用约定,相关代码如下:
只能接受单例(非集合)结果的调用方应该传递 isDone 为 NULL,如果表达式计算得到集合结果(set-result),则返回错误将通过 ereport 报告。如果调用者传递的 isDone 指针不为空,需要将*isDone 设置为以下 3 种状态之一:
(1) ExprSingleResult 单例结果(非集合)。
(2) ExprMultipleResult 返回值是集合的一个元素。
(3) ExprEndResult 集合中没有其他元素。
当返回 ExprMultipleResult 时,调用者应该重复调用并执行 ExecEvalExpr 函数,直到返回 ExprEndResult。
表 4-1 中列举代码“execQual.cpp”文件中的部分主要函数,下面将依次详细介绍每个函数的功能、核心代码和执行流程。
表 4-1 表达式计算的主要函数
ExecMakeFunctionResult 函数和 ExecMakeFunctionResultNoS 函数是表达式计算的核心函数,主要作用是通过获取表达式的参数来计算出表达式结果。ExecMakeFunctionResultNoSets 函数是 ExecMakeFunctionResult 函数的简化版,只能处理返回值是非集合情况。ExecMakeFunctionResult 函数核心代码如下:
ExecMakeFunctionResultNoSets 函数的执行流程如下。
(1) 声明 fcinfo 来存储表达式需要的参数信息,通过 InitFunctionCallInfoArgs 函数初始化 fcinfo 中的字段。
(2) 遍历表达式中的参数 args,通过 ExecEvalExpr 宏调用接口获取每一个参数的值,存储到“fcinfo->arg[i]”中。
(3) 根据 func.fn_strict 函数来判断是否需要检查参数空值情况。如果不需要检查,则通过“FunctionCalllv-oke”宏将参数传入表达式并计算出表达式的结果。否则进行判空处理,若存在空值则直接返回空,若不存在空值则通过 FunctionCalllvoke 宏计算表达式结果。
(4) 返回计算结果。
流程如图 14 所示。
图 14 “ExecMakeFunctionResultNoSets”函数执行流程
ExecMakeFunctionResult 函数的执行流程如图 7-15 所示。
(1) 判断 funcResultStore 是否存在,如果存在则从中获取结果返回(注:如果下文(3)中的模式是 SFRM_Materialize,则会直接跳到此处)。
(2) 计算出参数值存入到 fcinfo 中。
(3) 把参数传入到表达式函数中计算表达式,首先判断参数 args 是否存在空,然后判断返回集合的函数的返回模式,SFRM_ValuePerCall 模式是每次调用返回一个值,SFRM_Materialize 模式是在 Tuplestore 中实例化的结果集。
(4) 根据不同的模式进行计算并返回结果。
图 15 ExecMakeFunctionResult 函数执行流程
ExecEvalFunc 和 ExecEvalOper 这两个函数的功能类似。通过调用结果处理函数来获取结果。如果函数本身或者它的任何输入参数都可以返回一个集合,那么就会调 ExecMakeFunctionResult 函数来计算结果,否则调用 ExecMakeFunctionResultNoSets 函数来计算结果。核心代码如下:
ExecEvalFunc 函数的执行流程如下。
(1) 是通过 init_fcache 函数初始化 FuncExprState 节点,包括初始化参数、内存管理等等。
(2) 根据 FuncExprState 函数中的数据判断返回结果是否为 set 类型,并调用相应的函数计算结果。
ExecEvalFunc 函数执行流程如图 16 所示。
图 16 ExecEvalFunc 函数执行流程
ExecQual 函数的作用是检查 slot 结果是否满足表达式中的子表达式,如果子表达式为 false,则返回 false 否则返回 true,表示该结果符合预期,需要输出。核心代码如下:
ExecQual 函数的主要执行流程如下。
(1) 遍历 qual 中的子表达式,根据 ExecEvalExpr 函数计算结果是否满足该子表达式,若满足则 expr_value 为 1,否则为 0。
(2) 判断结果是否为空,若为空,则根据 resultForNull 参数得到返回值信息。若不为空,则根据 expr_value 判断返回 true 或者 false。
(3) 返回 result。
ExecQual 函数的执行流程如图 17 所示。
图 17 ExecQual 函数执行流程
ExecEvalOr 函数的作用是计算通过 or 连接的 bool 表达式(布尔表达式,最终只有 true(真)和 false(假)两个取值),检查 slot 结果是否满足表达式中的 or 表达式。如果结果符合 or 表达式中的任何一个子表达式,则直接返回 true,否则返回 false。如果获取的结果为 null,则记录 isNull 为 true。核心代码如下:
ExecEvalOr 函数主要执行流程如下。
(1) 遍历子表达式 clauses。
(2) 通过 ExecEvalExpr 函数来调用 clause 中的表达式计算函数,计算出结果。
(3) 对结果进行判断,or 表达式中若有一个结果满足条件,就会跳出循环直接返回。
ExecEvalOr 函数的执行流程如图 18 所示。
图 18 ExecEvalOr 函数执行流程
ExecTargetList 函数的作用是根据给定的表达式上下文计算 targetlist 中的所有表达式,将计算结果存储到元组中。主要结构体代码如下:
ExecTargetList 函数主要执行流程如下。(1) 遍历 targetlist 中的表达式。(2) 计算表达式结果。(3) 判断结果中 itemIsDone[resind]参数并生成最后的元组。ExecTargetList 函数的执行流程如图 19 所示。
图 19 ExecTargetList 函数执行流程
ExecProject 函数的作用是进行投影操作,投影操作是一种属性过滤过程,该操作将对元组的属性进行精简,把在上层计划节点中不需要用的属性从元组中去掉,从而构造一个精简版的元组。投影操作中被保留下来的那些属性被称为投影属性。主要结构体代码如下:
ExecProject 函数的主要执行流程如下。
(1) 取 ProjectionInfo 需要投影的信息。按照执行的偏移获取原属性所在的元组,通过偏移量获取该属性,并通过目标属性的序号找到对应的新元组属性位置进行赋值。
(2) 对 pi_targetlist 进行运算,将结果赋值给对应元组中的属性。
(3)产生一个行记录结果,对 slot 做标记处理,slot 包含一个有效的虚拟元组。
ExecProject 函数的执行流程如图 20 所示。
图 20 ExecProject 函数执行流程
ExecEvalParamExec 函数的作用是获取并返回 PARAM_EXEC 类型的参数。PARAM_EXEC 参数是指内部执行器参数,是需要执行子计划来获取的结果,最后需要将结果返回到上层计划中。核心代码如下:
ExecEvalParamExec 函数的主要执行流程如下。
(1) 获取 econtext 中的 ecxt_param_exec_vals 参数。
(2) 判断子计划是否为空,若不为空则调用 ExecSetParamPlan 函数执行子计划获取结果,并把计划置为空,当再次执行此函数时,不需要重新执行计划,直接返回已经获取过结果。
(3) 将结果 prm->value 返回。
ExecEvalParamExec 函数的执行流程如图 21 所示。
图 21 ExecEvalParamExec 函数执行流程
ExecEvalParamExtern 函数的作用是获取并返回 PARAM_EXTERN 类型的参数。该参数是指外部传入参数,例如在 PBE 执行时,PREPARE 的语句中的参数,在需要 execute 语句执行时传入。核心代码如下:
ExecEvalParamExtern 函数主要执行流程如下。
(1) 判断 PARAM_EXTERN 类型的参数否存在,若存在则从 ecxt_param_list_info 中获取该参数,否则直接报错。
(2) 判断参数是否是动态的,若是动态的则再次获取参数。
(3) 判断参数类型是否符合要求,若符合要求直接返回该参数。
ExecEvalParamExtern 函数的执行流程如图 22 所示。
图 22 ExecEvalParamExtern 函数执行流
评论