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

上一篇openGauss数据库源码解析系列文章——执行器解析(一)介绍了“ 执行器整体架构及代码概览”、“执行流程”及“执行算子”的相关内容,本篇将介绍“表达式计算”及“编译执行”的精彩内容。

四、表达式计算

表达式计算对应的代码源文件是“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; };

ExecEvalFunc和ExecEvalOper这两个函数的功能类似。通过调用结果处理函数来获取结果。如果函数本身或者它的任何输入参数都可以返回一个集合,那么就会调ExecMakeFunctionResult函数来计算结果,否则调用ExecMakeFunctionResultNoSets函数来计算结果。核心代码如下:

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

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;   /* 返回结果是否满足表达式 */

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);

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;

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;

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; /* 返回生成的参数 */

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

if (paramInfo && thisParamId > 0 && thisParamId 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;

五、编译执行

为了提高SQL的执行速度,解决传统数据处理引擎条件逻辑冗余的问题,openGauss为执行表达式引入了CodeGen技术,其核心思想是为具体的查询生成定制化的机器码代替通用的函数实现,并尽可能地将数据存储在CPU寄存器中。openGauss通过LLVM编译框架来实现CodeGen,LLVM是“Low Level Virtual Machine”的缩写,开发之初是想作为一个底层虚拟机,但随着开发,以及功能的逐渐完善,慢慢变成一个模块化的编译系统,并能支持多种语言。LLVM的系统架构如图23所示。

图23 LLVM系统架构

LLVM大体上可以分成3个部分。

(1) 支持多种语言的前端。

(2) 优化器。

(3) 支持多种CPU架构的后端(X86、Aarch64)。

LLVM与GCC一样,都是常用的编译系统,但是LLVM更加模块化,从而可以免去每使用一套语言换一套优化器的工作,开发者只要设计相应的前端,并针对各个目标平台做后端优化。

考虑如下SQL语句。

SELECT * FROM dataTable WHRER (x + 2) * 3 > 4;

此类表达式的执行代码是一套通用的函数实现,每次递归都有很多冗余判断,需要依赖上一步的输出作为当前的输入,实现如下代码逻辑:

void MaterializeTuple(char * tuple) { for (int I = 0; i < num_slots_; i++) {    char* slot = tuple + offsets_[i];    switch(types_[i]) {        case BOOLEAN: *slot = ParseBoolean(); break; case INT: *slot = ParseInt(); Break; case FLOAT: …      case STRING: … … … } } }