openGauss数据库源码解析系列文章——执行器解析(2.1)
  lYE0sTgD5uUi 2023年11月02日 55 0

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所示。

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

图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所示。

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

图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 表达式计算的主要函数

主要函数

说明

ExecMakeFunctionResultNoSets

表达式计算(非集合)

ExecMakeFunctionResult

表达式计算(集合)

ExecEvalFunc/ExecEvalOper

调用表达式计算函数

ExecQual

检查条件表达式

ExecEvalOr

处理or表达式

ExecTargetList

计算targetlist中的所有表达式

ExecProject

计算投影信息

ExecEvalParamExec

获取Exec类型参数

ExecEvalParamExtern

获取Extern类型参数

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所示。

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

图14 “ExecMakeFunctionResultNoSets”函数执行流程

ExecMakeFunctionResult函数的执行流程如图7-15所示。

(1) 判断funcResultStore是否存在,如果存在则从中获取结果返回(注:如果下文(3)中的模式是SFRM_Materialize,则会直接跳到此处)。

(2) 计算出参数值存入到fcinfo中。

(3) 把参数传入到表达式函数中计算表达式,首先判断参数args是否存在空,然后判断返回集合的函数的返回模式,SFRM_ValuePerCall模式是每次调用返回一个值,SFRM_Materialize模式是在Tuplestore中实例化的结果集。

(4) 根据不同的模式进行计算并返回结果。

openGauss数据库源码解析系列文章——执行器解析(2.1)_表达式计算_04

图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所示。

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

图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所示。

openGauss数据库源码解析系列文章——执行器解析(2.1)_表达式计算_06

图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所示。

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

图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所示。

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

图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所示。

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

图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所示。

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

图21 ExecEvalParamExec函数执行流程

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

if (paramInfo && thisParamId > 0 && thisParamId <= paramInfo->numParams) {/* 判断参数 */
ParamExternData* prm = ¶mInfo->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所示。

openGauss数据库源码解析系列文章——执行器解析(2.1)_表达式计算_11

图22 ExecEvalParamExtern函数执行流

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  gBkHYLY8jvYd   2023年12月06日   50   0   0 #includecii++
  gBkHYLY8jvYd   2023年11月19日   30   0   0 #includecic++
  gBkHYLY8jvYd   2023年12月09日   30   0   0 cii++数据
  lh6O4DgR0ZQ8   2023年11月19日   29   0   0 解包ci插槽
  gBkHYLY8jvYd   2023年12月06日   24   0   0 cii++依赖关系
  lh6O4DgR0ZQ8   2023年11月24日   18   0   0 cii++c++
  gBkHYLY8jvYd   2023年11月22日   23   0   0 ioscii++
  gBkHYLY8jvYd   2023年11月19日   27   0   0 #include数组ci
  gBkHYLY8jvYd   2023年11月19日   24   0   0 cifor循环字符串
  gBkHYLY8jvYd   2023年12月08日   21   0   0 #includecii++
  gBkHYLY8jvYd   2023年11月19日   26   0   0 #includeiosci
  gBkHYLY8jvYd   2023年12月11日   20   0   0 cic++最小值
  gBkHYLY8jvYd   2023年11月19日   29   0   0 测试点cic++
  gBkHYLY8jvYd   2023年11月22日   26   0   0 #includeiosci
lYE0sTgD5uUi