写点什么

openGauss 数据库源码解析系列文章——执行器解析(2.1)

作者:daydayup
  • 2023-07-24
    北京
  • 本文字数:7116 字

    阅读完需:约 23 分钟

openGauss 数据库源码解析系列文章——执行器解析(2.1)

四、表达式计算


表达式计算对应的代码源文件是“execQual.cpp”,openGauss 处理 SQL 语句中的函数调用、计算式和条件表达式时需要用到表达式计算。


表达式的表示方式和查询计划树的计划节点类似,通过生成表达式计划来对每个表达式节点进行计算。表达式继承层次中的公共根类为 Expr 节点,其他表达式节点都继承 Expr 节点。表达式状态的公共根类为 ExprState,记录了表达式的类型以及实现该表达式节点的函数指针。表达式内存上下文类为 ExprContext,ExprContext 充当了计划树节点中 Estate 的角色,表达式计算过程中的参数以及表达式所使用的内存上下文都会存放到此结构中。


表达式计算对应的主要结构体代码如下:


typedef struct Expr {    NodeTag type;              /*表达式节点类型*/} Expr;struct ExprState {    NodeTag type;    Expr* expr;                 /*关联的表达式节点*/    ExprStateEvalFunc evalfunc;   /*表达式运算的函数指针*/    VectorExprFun vecExprFun;    exprFakeCodeGenSig exprCodeGen; /*运行LLVM汇编函数的指针*/    ScalarVector tmpVector;    Oid resultType;};
复制代码


表达式计算的过程分为 3 个部分:初始化、执行和清理。初始化的过程使用统一接口 ExecInitExpr,根据表达式的类型选择不同的处理方式,生成表达式节点树。执行过程使用统一接口宏 ExecEvalExpr,执行过程类似于计划节点的递归方式。


4.1 初始化阶段


ExecInitExpr 函数的作用是在执行的初始化阶段,准备要执行的表达式树。根据传入的表达式 node tree,来创建并返回 ExprState tree。在真正的执行阶段会根据 ExprState tree 中记录的处理函数,递归地执行每个节点。ExecInitExpr 函数的核心代码如下:


if (node == NULL) {  /* 判断输入是否为空 */  gstrace_exit(GS_TRC_ID_ExecInitExpr);  return NULL;}switch (nodeTag(node)) {  /* 根据节点类型初始化节点内容 */        case T_Var:        case T_Const:case T_Param:        ……        case T_CaseTestExpr:        case T_Aggref:        ……case T_CurrentOfExpr:case T_TargetEntry: case T_List:        case T_Rownum:default:…… }return state;   /* 返回表达式节点树 */
复制代码


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 函数调用执行流程


执行阶段所有函数都共享此调用约定,相关代码如下:


输入:expression:需要计算的表达式状态树。econtext:评估上下文信息。输出:return value:Datum类型的返回值。*isNull:如果结果为NULL,则设置为TRUE(实际返回值无意义);如果结果非空,则设置为FALSE。*isDone:设置为set-result状态的指标。
复制代码


只能接受单例(非集合)结果的调用方应该传递 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 函数核心代码如下:


fcinfo = &fcache->fcinfo_data;                           /* 声明fcinfo */InitFunctionCallInfoArgs(*fcinfo, list_length(fcache->args), 1); /*初始化fcinfo */ econtext->is_cursor = false;    foreach (arg, fcache->args) {                          /* 遍历获取参数值 */        ExprState* argstate = (ExprState*)lfirst(arg);        fcinfo->argTypes[i] = argstate->resultType;        fcinfo->arg[i] = ExecEvalExpr(argstate, econtext, &fcinfo->argnull[i], NULL);if (fcache->func.fn_strict)                   /* 判断参数是否存在空值 */…… result = FunctionCallInvoke(fcinfo);           /* 计算表达式结果 */return result;
复制代码


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 函数来计算结果。核心代码如下:


init_fcache<false>(func->funcid,func->inputcollid,fcache, econtext->ecxt_per_query_memory, true);                 /* 初始化fcache */if (fcache->func.fn_retset) {                           /* 判断返回结果类型 */    ……return ExecMakeFunctionResult<true, true, true>(fcache, econtext, isNull, isDone);} else if (expression_returns_set((Node*)func->args)) {……return ExecMakeFunctionResult<true, true, false>(fcache, econtext, isNull, isDone);} else {……return ExecMakeFunctionResultNoSets<true, true>(fcache, econtext, isNull, isDone);}
复制代码


ExecEvalFunc 函数的执行流程如下。


(1) 是通过 init_fcache 函数初始化 FuncExprState 节点,包括初始化参数、内存管理等等。


(2) 根据 FuncExprState 函数中的数据判断返回结果是否为 set 类型,并调用相应的函数计算结果。


ExecEvalFunc 函数执行流程如图 16 所示。



图 16 ExecEvalFunc 函数执行流程


ExecQual 函数的作用是检查 slot 结果是否满足表达式中的子表达式,如果子表达式为 false,则返回 false 否则返回 true,表示该结果符合预期,需要输出。核心代码如下:


foreach (l, qual) {        /* 遍历qual中的子表达式并计算 */expr_value = ExecEvalExpr(clause, econtext, &isNull, NULL);if (isNull) {  /* 判断计算结果 */if (resultForNull == false) {result = false; break;}        } else {            if (!DatumGetBool(expr_value)) {                  result = false; …… return result;   /* 返回结果是否满足表达式 */
复制代码


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。核心代码如下:


foreach (clause, clauses) {              /* 遍历子表达式 */        ExprState* clausestate = (ExprState*)lfirst(clause);        Datum clause_value;        clause_value = ExecEvalExpr(clausestate, econtext, isNull, NULL);  /* 执行表达式 */        /* 如果得到不空且ture的结果,直接返回结果 */if (*isNull) /* 记录存在空值 */            AnyNull = true;         else if (DatumGetBool(clause_value))/* 一次结果为true就返回 */            return clause_value;  /* 返回执行结果 */    }*isNull = AnyNull;return BoolGetDatum(false);
复制代码


ExecEvalOr 函数主要执行流程如下。


(1) 遍历子表达式 clauses。


(2) 通过 ExecEvalExpr 函数来调用 clause 中的表达式计算函数,计算出结果。


(3) 对结果进行判断,or 表达式中若有一个结果满足条件,就会跳出循环直接返回。


ExecEvalOr 函数的执行流程如图 18 所示。



图 18 ExecEvalOr 函数执行流程


ExecTargetList 函数的作用是根据给定的表达式上下文计算 targetlist 中的所有表达式,将计算结果存储到元组中。主要结构体代码如下:


typedef struct GenericExprState {    ExprState xprstate;    ExprState* arg; /*子节点的状态*/} GenericExprState;typedef struct TargetEntry {    Expr xpr;    Expr* expr;            /*要计算的表达式*/    AttrNumber resno;      /*属性号*/    char* resname;         /*列的名称*/    Index ressortgroupref;    /*如果被sort/group子句引用,则为非零*/    Oid resorigtbl;           /*列的源表的OID */    AttrNumber resorigcol;    /*源表中的列号*/    bool resjunk;            /*设置为true可从最终目标列表中删除该属性*/} TargetEntry;
复制代码


ExecTargetList 函数主要执行流程如下。(1) 遍历 targetlist 中的表达式。(2) 计算表达式结果。(3) 判断结果中 itemIsDone[resind]参数并生成最后的元组。ExecTargetList 函数的执行流程如图 19 所示。



图 19 ExecTargetList 函数执行流程


ExecProject 函数的作用是进行投影操作,投影操作是一种属性过滤过程,该操作将对元组的属性进行精简,把在上层计划节点中不需要用的属性从元组中去掉,从而构造一个精简版的元组。投影操作中被保留下来的那些属性被称为投影属性。主要结构体代码如下:


typedef struct ProjectionInfo {    NodeTag type;    List* pi_targetlist;            /*目标列表*/    ExprContext* pi_exprContext;  /*内存上下文*/    TupleTableSlot* pi_slot;       /*投影结果*/    ExprDoneCond* pi_itemIsDone; /*ExecProject的工作区数组*/    bool pi_directMap;    int pi_numSimpleVars;    /*在原始tlist(查询目标列表)中找到的简单变量数*/    int* pi_varSlotOffsets;    /*指示变量来自哪个slot(槽位)的数组*/    int* pi_varNumbers;     /*包含变量的输入属性数的数组*/    int* pi_varOutputCols;   /*包含变量的输出属性数的数组*/    int pi_lastInnerVar;      /*内部参数*/    int pi_lastOuterVar;     /*外部参数*/    int pi_lastScanVar;      /*扫描参数*/    List* pi_acessedVarNumbers;    List* pi_sysAttrList;    List* pi_lateAceessVarNumbers;    List* pi_maxOrmin;    /*列表优化,指示获取此列的最大值还是最小值*/    List* pi_PackTCopyVars;            /*记录需要移动的列*/    List* pi_PackLateAccessVarNumbers;  /*记录cstore(列存储)扫描中移动的内容的列*/    bool pi_const;    VectorBatch* pi_batch;    vectarget_func jitted_vectarget;      /* LLVM函数指针*/    VectorBatch* pi_setFuncBatch;} ProjectionInfo;
复制代码


ExecProject 函数的主要执行流程如下。


(1) 取 ProjectionInfo 需要投影的信息。按照执行的偏移获取原属性所在的元组,通过偏移量获取该属性,并通过目标属性的序号找到对应的新元组属性位置进行赋值。


(2) 对 pi_targetlist 进行运算,将结果赋值给对应元组中的属性。


(3)产生一个行记录结果,对 slot 做标记处理,slot 包含一个有效的虚拟元组。


ExecProject 函数的执行流程如图 20 所示。



图 20 ExecProject 函数执行流程


ExecEvalParamExec 函数的作用是获取并返回 PARAM_EXEC 类型的参数。PARAM_EXEC 参数是指内部执行器参数,是需要执行子计划来获取的结果,最后需要将结果返回到上层计划中。核心代码如下:


prm = &(econtext->ecxt_param_exec_vals[thisParamId]); /* 获取econtext中参数 */if (prm->execPlan != NULL) {                    /* 判断是否需要生成参数 */  /* 参数还未计算执行此函数*/  ExecSetParamPlan((SubPlanState*)prm->execPlan, econtext);  /*参数计算完计划重置为空*/  Assert(prm->execPlan == NULL);  prm->isConst = true;  prm->valueType = expression->paramtype;}*isNull = prm->isnull;prm->isChanged = true;return prm->value;
复制代码


ExecEvalParamExec 函数的主要执行流程如下。


(1) 获取 econtext 中的 ecxt_param_exec_vals 参数。


(2) 判断子计划是否为空,若不为空则调用 ExecSetParamPlan 函数执行子计划获取结果,并把计划置为空,当再次执行此函数时,不需要重新执行计划,直接返回已经获取过结果。


(3) 将结果 prm->value 返回。


ExecEvalParamExec 函数的执行流程如图 21 所示。



图 21 ExecEvalParamExec 函数执行流程


ExecEvalParamExtern 函数的作用是获取并返回 PARAM_EXTERN 类型的参数。该参数是指外部传入参数,例如在 PBE 执行时,PREPARE 的语句中的参数,在需要 execute 语句执行时传入。核心代码如下:


if (paramInfo && thisParamId > 0 && thisParamId <= paramInfo->numParams) {/* 判断参数 */ParamExternData* prm = &paramInfo->params[thisParamId - 1];  if (!OidIsValid(prm->ptype) && paramInfo->paramFetch != NULL)   /* 获取动态参数 */     (*paramInfo->paramFetch)(paramInfo, thisParamId);    if (OidIsValid(prm->ptype)) {                               /*检查参数并返回 */ if (prm->ptype != expression->paramtype)ereport(……);       *isNull = prm->isnull;       if (econtext->is_cursor && prm->ptype == REFCURSOROID) {         CopyCursorInfoData(&econtext->cursor_data, &prm->cursor_data);         econtext->dno = thisParamId - 1;       }       return prm->value;   }}  ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), errmsg("no value found for parameter %d", thisParamId)));  return (Datum)0;
复制代码


ExecEvalParamExtern 函数主要执行流程如下。


(1) 判断 PARAM_EXTERN 类型的参数否存在,若存在则从 ecxt_param_list_info 中获取该参数,否则直接报错。


(2) 判断参数是否是动态的,若是动态的则再次获取参数。


(3) 判断参数类型是否符合要求,若符合要求直接返回该参数。


ExecEvalParamExtern 函数的执行流程如图 22 所示。



图 22 ExecEvalParamExtern 函数执行流

用户头像

daydayup

关注

还未添加个人签名 2023-07-18 加入

还未添加个人简介

评论

发布
暂无评论
openGauss数据库源码解析系列文章——执行器解析(2.1)_opengauss_daydayup_InfoQ写作社区