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

2023/04/21 17:58
阅读数 86

在上一篇openGauss数据库源码解析系列文章——执行器解析(二)中,介绍了执行器解析中“表达式计算”及“编译执行”的相关内容,本篇将介绍“向量化引擎”的精彩内容。

六、向量化引擎

传统的行执行引擎大多采用一次一元组的执行模式,这样在执行过程中CPU大部分时间并没有用来处理数据,更多的是在遍历执行树,就会导致CPU的有效利用率较低。而在面对OLAP场景巨量的函数调用次数,需要巨大的开销。为了解决这一问题,openGauss中增加了向量化引擎。向量化引擎使用了一次一批元组的执行模式,能够大大减少遍历执行节点的开销。一次一批元组的数据运载方式也为某些表达式计算的SIMD(single instruction, multiple data,单指令多数据)化提供了机会,SIMD化能够带来性能上的提升。同时向量化引擎还天然对接列存储,能够较为方便地在底层扫描节点装填向量化的列数据。

向量化引擎的执行算子类似于行执行引擎,包含控制算子、扫描算子、物化算子和连接算子。同样会使用节点表示,继承于行执行节点,执行流程采用递归方式。主要包含的节点有:CStoreScan(顺序扫描),CStoreIndexScan(索引扫描),CStoreIndexHeapScan(利用Bitmap获取元组),VecMaterial(物化),VecSort(排序),VecHashJoin(向量化哈希连接)等,下面将逐一介绍这些执行算子。


6.1 控制算子

1. VecResult算子

VecResult算子用于处理只有一个结果返回或WHERE过滤条件为常量的情况,对应的代码源文件是“vecresult.cpp”;对应的主要数据结构是VecResult,VecResult继承于BaseResult。VecResult算子相关的函数包括ExecInitVecResult(初始化节点)、ExecVecResult(执行节点)、ExecReScanVecResult(重置节点)、ExecEndVecResult(退出节点)。

ExecInitVecResult函数用于初始化VecResult执行算子。执行流程如图一所示,主要执行流程如下。

(1) 创建并初始化VecResult执行节点,并为节点创建表达式上下文。

(2)调用“ExecInitResultTupleSlot(estate, &res_state->ps)”函数分配存储投影结果的slot。

(3) 调用投影表达式初始化函数ExecInitVecExpr依次对ps.targetlist、ps.qual和resconstantqual进行初始化。

(4) 分别调用ExecAssignResultTypeFromTL函数和ExecAssignVectorForExprEval函数进行扫描描述符的初始化和投影结构的创建。

图28 ExecInitVecResult函数执行流程

ExecVecResult函数是执行VecResult的主体函数。执行流程如图29所示,主要执行流程如下。

(1) 检查是否需要计算常量表达式。

(2) 若需要则重新计算表达式,设置检查标识(如果常量计算表达式结果为false时,则设置约束检查标识位)。

(3) 获取结果元组。

图29  ExecVecResult函数执行流程

ExecReScanVecResult函数用于重新执行扫描计划。

ExecEndVecResult函数用于在执行结束时释放执行过程中申请的相关资源(包括存储空间等)。

2. VectorModifyTable算子

VecModifyTable算子用于处理INSERT、UPDATE、DELETE操作,对应的代码源文件是“vecmodifytable.cpp”;对应的主要数据结构是VecModifyTableState,VecModifyTableState继承于ModifyTableState。具体定义代码如下所示:

typedef struct VecModifyTableState : public ModifyTableState {    VectorBatch* m_pScanBatch;      /* 工作元组 */    VectorBatch* m_pCurrentBatch;  /* 输出元组 */} VecModifyTableState;

VecModifyTable算子相关的函数包括ExecInitVecModifyTable(初始化节点)、ExecVecModifyTable(执行节点)、ExecEndVecModifyTable(退出节点)。

ExecInitVecModifyTable函数用于初始化VecModifyTable算子,调用ExecInitModifyTable函数实现算子的初始化。

ExecVecModifyTable函数是执行VecModifyTable算子的主体函数,循环地从子计划中获取目标列并根据要求修改每一列,通过“switch(operation)”处理不同的修改操作,具体的修改操作包括CMD_INSERT(插入)、CMD_DELETE(删除)、CMD_UPDATE(更新)。

ExecEndVecModifyTable函数用于在执行VecModifyTable算子结束时调用ExecEndModifyTable函数清除相关资源。

3. VecAppend算子

VecAppend算子用于处理包含一个或多个子计划的链表,通过遍历子计划链表逐个执行子计划,对应的代码源文件是“vecappend.cpp”;对应的主要数据结构是VecAppendState,VecAppendState继承于AppendState。

VecAppend算子相关的函数包括ExecInitVecAppend(初始化节点)、ExecVecAppend(执行节点)、ExecReScanAppend(重置节点)、ExecEndVecAppend(退出节点)。

ExecInitVecAppend函数用于初始化VecAppend算子。执行执行流程如图30所示,主要执行流程如下。

(1) 创建并初始化执行节点VecAppend。

(2) 分配存储投影结果的slot。

(3) 循环初始化子计划链表。

(4) 初始化扫描描述符并设置初始迭代。

图30  ExecInitVecAppend函数执行流程

ExecVecAppend函数是执行VecAppend算子的主体函数。执行流程如图31所示,每次从子计划中获取一条元组,当取回全部元组时,移动到下一个子计划,直到执行全部子计划。

图31  ExecVecAppend执行流程

ExecEndVecAppend函数用于在执行结束时清理VecAppend算子,释放相应的子计划。

6.2  扫描算子

1. CStoreScan算子

CStoreScan算子用于扫描基础表,按顺序扫描基础表,对应的代码源文件是“veccstore.cpp”;CStoreScan算子对应的主要数据结构是CStoreScanState,CStoreScanState继承于ScanState。具体定义代码如下:

typedef struct CStoreScanState : ScanState {    Relation ss_currentDeltaRelation;    Relation ss_partition_parent;    TableScanDesc ss_currentDeltaScanDesc;    bool ss_deltaScan;    bool ss_deltaScanEnd;    VectorBatch* m_pScanBatch;    VectorBatch* m_pCurrentBatch;    CStoreScanRunTimeKeyInfo* m_pScanRunTimeKeys;    int m_ScanRunTimeKeysNum;    bool m_ScanRunTimeKeysReady;    CStore* m_CStore;    CStoreScanKey csss_ScanKeys;    int csss_NumScanKeys;    bool m_fSimpleMap;    bool m_fUseColumnRef;    vecqual_func jitted_vecqual;    bool m_isReplicaTable; /*复制表标记符*/} CStoreScanState;

CStoreScan算子的相关函数包括:ExecInitCStoreScan(初始化节点)、ExecCStoreScan(执行节点)、ExecEndCStoreScan(退出节点)、ExecReScanCStoreScan(重置节点)。

ExecInitCStoreScan函数用于初始化CStoreScan算子。主要执行流程如下。

(1) 创建并初始化CStoreScan算子,为节点创建表达式上下文。

(2) 调用ExecAssignVectorForExprEval函数进行投影表达式的初始化。

(3) 调用ExecInitResultTupleSlot函数和ExecInitScanTupleSlot函数分别初始化用于投影结果和用于扫描的slot。

(4) 打开扫描表,调用ExecAssignResultTypeFromTL函数和ExecBuildVecProjectionInfo函数分别初始化结果扫描描述符和创建投影结构。

ExecCStoreScan函数是CStoreScan算子的主体函数,通过迭代的方式获取全部结果元组。

ExecEndCStoreScan函数用于在算子执行结束后清理CStoreScan算子。主要执行流程是:首先获取节点信息(包括Relation、ScanDesc),之后释放表达式上下文、元组,最后关闭相应的partition、relation。

ExecReScanCStoreScan函数用于重新执行扫描计划。主要执行流程是:首先重置runtime关键词,关闭当前节点partition信息,初始化接下来的partition信息,最后重置CStoreScan算子。

2. CStoreIndexScan算子

CStoreIndexScan算子用于使用索引对表进行扫描,如果过滤条件中涉及索引,可以使用该算子加速元组获取,对应的代码源文件是“veccstoreindexscan.cpp”。CStoreIndexScan算子对应的主要数据结构是CStoreIndexScanState,CStoreIndexScanState继承于CStoreScanState。具体定义代码如下:

typedef struct CStoreIndexScanState : CStoreScanState {    CStoreScanState* m_indexScan;    CBTreeScanState* m_btreeIndexScan;    CBTreeOnlyScanState* m_btreeIndexOnlyScan;    List* m_deltaQual;    bool index_only_scan;    /* 扫描索引并从基表中得到以下信息 */    int* m_indexOutBaseTabAttr;    int* m_idxInTargetList;    int m_indexOutAttrNo;    cstoreIndexScanFunc m_cstoreIndexScanFunc;} CStoreIndexScanState;

CStoreIndexScan算子的相关函数包括:ExecInitCStoreIndexScan(初始化节点)、ExecCStoreIndexScanT(执行节点)、ExecEndCStoreIndexScan(退出节点)、ExecReScanCStoreIndexScan(重置节点)。

ExecInitCStoreIndexScan函数用于初始化CStoreIndexScan算子。主要执行流程是:首先创建CStoreScan执行节点scanstate,之后根据scanstate创建CStoreIndexScanState节点。最后打开相关的relation和index。

ExecCStoreIndexScanT函数是CStoreIndexScan算子的主体函数。主要执行流程是:首先会从计划节点中获取Btree的相关信息,设置runtime扫描关键词,最后循环地获取结果集,直到执行结束。

ExecEndCStoreIndexScan函数用于在执行结束时清理CStoreIndexScan算子。主要执行流程是:首先清理相应的CStoreScan算子,之后关闭相应的index、relation。

ExecReScanCStoreIndexScan函数用于重新执行扫描计划。主要执行流程是:首先重新扫描m_indexscan,之后重新扫描相关节点信息。

3. CStoreIndexHeapScan算子

CStoreIndexHeapScan算子用于对属性上的索引进行扫描,返回结果为一个位图,其中标记了满足条件的元组在页面中的偏移量,对应的代码源文件是veccstoreindexheapscan.cpp;CStoreIndexHeapScan算子对应的主要数据结构是CStoreIndexHeapScanState,继承于CStoreIndexScanState。其中包含的核心函数有:ExecCstoreInitIndexHeapScan(初始化节点)、ExecCstoreIndexHeapScan(执行节点)、ExecReScanCstoreIndexHeapScan(重置节点)、ExecEndCstoreIndexHeapScan(退出节点)。

ExecCstoreInitIndexHeapScan函数是用于初始化CStoreIndexHeapScan算子。主要执行流程是:首先将计划节点转换为执行节点CStoreScan,之后复制CStoreScan算子、计划节点信息完成CStoreIndexHeapScan算子的初始化。

ExecCstoreIndexHeapScan函数是CStoreIndexHeapScan算子的主体函数。主要执行流程是:首先更新timing标记,之后迭代地获取目标迭代批次,直到获取全部结果。

ExecReScanCstoreIndexHeapScan函数用于重新执行扫描计划,通过调用VecExecReScan函数、ExecReScanCStoreScan函数实现重新扫描。

ExecEndCstoreIndexHeapScan函数用于在执行结束后清理CStoreIndexHeapScan算子占用的资源,通过调用ExecEndNode函数和ExecEndCStoreScan函数清理计划节点和执行节点。

4. VecSubqueryScan算子

VecSubqueryScan算子将子计划作为扫描对象,实际执行中会转换为调用子节点计划,对应的代码源文件是vecsubqueryscan.cpp;VecSubqueryScan算子对应的主要数据结构是VecSubqueryScanState,继承于SubqueryScanState。包含的核心函数有:ExecInitVecSubqueryScan(初始化节点)、ExecVecSubqueryScan(执行节点)、ExecEndVecSubqueryScan(退出节点)、ExecReScanVecSubqueryScan(重置节点)。

ExecInitVecSubqueryScan函数是用于初始化VecSubqueryScan算子。主要执行流程是:首先初始化VecSubqueryScan执行算子,并为节点创建表达式上下文,接着初始化子查询计划,最后初始化元组和投影信息。

ExecVecSubqueryScan函数是VecSubqueryScan算子的主体函数。主要执行流程是:调用ExecVecScan执行算子,得到查询结果。

ExecReScanVecSubqueryScan函数用于重新执行扫描计划,通过调用ExecScanReScan函数重新扫描。

ExecEndVecSubqueryScan函数用于在执行结束后清理VecSubqueryScan算子占用的资源,通过调用ExecEndSubqueryScan函数进行清理。

5. VecForeignScan算子

VecForeignScan算子对应的代码源文件是“vecforeignscan.cpp”。VecForeignScan算子对应的主要数据结构是VecForeignScanState,继承于ForeignScanState。该算子包含的核心函数有:ExecInitVecForeignScan(初始化节点)、ExecVecForeignScan(执行节点)、ExecEndVecForeignScan(退出节点)、ExecReScanVecForeignScan(重置节点)。

ExecInitVecForeignScan函数是用于初始化VecForeignScan算子。主要执行流程如下。

(1) 创建VecForeignScanState执行节点。

(2) 设置表达式上下文。

(3) 调用ExecInitVecExpr函数依次为“ss.ps.targetlist”和“ss.ps.qual”初始化表达式。

(4) 调用ExecBuildVecProjectionInfo函数创建投影结构。

ExecVecForeignScan函数是VecForeignScan算子的主体函数,通过调用ExecVecScan执行算子,得到查询结果。

ExecReScanVecForeignScan函数用于重新执行扫描计划,通过调用ExecScanReScan函数实现重新扫描。

ExecEndVecForeignScan函数用于在执行结束后清理VecForeignScan算子占用的资源,通过调用MemoryContextDelete函数清除上下文和ExecEndForeignScan函数清除执行节点。

6.3  物化算子

1. VecMaterial算子

VecMaterial算子能够缓存需要多次重复扫描的子节点结果,有助于减少执行中的扫描代价。

VecMaterial算子对应的代码源文件是“vecmaterial.cpp”。VecMaterial算子对应的主要数据结构是VecMaterialState,继承于MaterialState。相关代码如下:

typedef struct VecMaterialState : public MaterialState {    VectorBatch* m_pCurrentBatch;    BatchStore* batchstorestate;    bool from_memory;} VecMaterialState;

VecMaterial算子的相关函数包括:ExecInitVecMaterial(初始化节点)、ExecVecMaterial(执行节点)、ExecEndVecMaterial(退出节点)、ExecReScanVecMaterial(重置节点)。

ExecInitVecMaterial函数用于初始化VecMaterial算子。主要执行流程如下。

(1) 创建并初始化VecMaterialState执行节点。

(2) 分别调用“ExecInitResultTupleSlot(estate, &matstate->ss.ps)”函数和“ExecInitScanTupleSlot(estate, &matstate->ss)”函数分配用于存储投影结果和用于扫描的slot。

(3) 调用“ExecAssignScanTypeFromOuterPlan(&matstate->ss)”函数初始化元组类型,调用“ExecAssignResultTypeFromTL(&matstate->ss.ps,matstate->ss.ss_ScanTupleSlot->tts_tupleDescriptor->tdTableAmType)”函数初始化结果扫描描述符。

(4) 最后如果当前VecMaterial节点处于子计划中并且物化Stream数据,需要将其添加到estate->es_material_of_subplan中。

ExecVecMaterial函数是VecMaterial算子的主体函数,根据materalAll判断是否需要一次性物化所有元组,通过分别调用exec_vec_material_all函数、exec_vec_material_one函数完成算子的执行。其中exec_vec_material_all函数会一次性物化全部元组,之后根据需要返回部分元组,而exec_vec_material_one函数则会逐个物化元组。

ExecEndVecMaterial函数用于清理VecMaterial算子执行过程中使用的资源主要执行流程是:首先调用“ExecClearTuple(node->ss.ss_ScanTupleSlot)”函数清理tuple table,之后调用batchstore_end释放用于存储元组的资源,最后调用“ExecEndNode(outerPlanState(node)) ”函数清理子计划节点。

ExecReScanVecMaterial函数用于重新执行扫描计划,流程如图7-32所示。主要执行流程是:

(1) 根据eflags判断tuplestore是否进行其他操作(如REWIND、BACKWARD、RESTORE)。

(2) 若未进行其他操作,则直接调用“VecExecReScan(node->ss.ps.lefttree)”函数进行重新扫描,反之则需要进一步判断是否已执行了物化操作。

(3) 若未进行物化操作,则同样直接调用“VecExecReScan(node->ss.ps.lefttree)”函数进行重新扫描;若已产生物化结果,则需要根据进行操作的不同进行不同处理。

(4) 已产生物化结果可以分为2种情况:REWIND操作和其他操作。如果进行了REWIND操作,则需要调用batchstore_end函数释放已经存储的结果,之后调用“VecExecReScan(node->ss.ps.lefttree)”函数重新扫描。对于其他操作,需要判断当前计划是否为partition-wise join并且是否需要切换分区。

(5) 如是则同样需要调用batchstore_end和VecExecReScan函数重新扫描计划;反之只需要调用“batchstore_rescan(node->batchstorestate)”函数。

图32  ExecReScanVecMaterial函数执行流程

2. VecSort算子

VecSort算子用于缓存下层节点返回的所有结果元组并进行排序,对于结果元组较多时,会使用临时文件进行存储,并使用外排序进行排序操作。

VecSort算子对应的代码源文件是“vecsort.cpp”,VecSort算子对应的主要数据结构是VecSortState,继承于SortState。相应代码如下:

typedef struct VecSortState : public SortState {    VectorBatch* m_pCurrentBatch;char* jitted_CompareMultiColumn;         char* jitted_CompareMultiColumn_TOPN; } VecSortState;

VecSort算子的相应函数有:ExecInitVecSort(初始化节点)、ExecVecSort(执行节点)、ExecEndVecSort(退出节点)、ExecReScanVecSort(重置节点)。

ExecInitVecSort函数用于初始化VecSort算子。主要执行流程如下。

(1) 创建并初始化VecSortState执行节点。

(2) 分别调用“ExecInitResultTupleSlot(estate, &sort_state->ss.ps)”函数用于存储投影结果和“ExecInitScanTupleSlot(estate, &sort_state->ss)”函数用于初始化扫描元组槽。

(3) 调用“ExecAssignScanTypeFromOuterPlan(&sort_state->ss)”函数初始化元组类型,调用“ExecAssignResultTypeFromTL(&sort_stat->ss.ps,sort_stat->ss.ss_ScanTupleSlot->tts_tupleDescriptor->tdTableAmType)”函数初始化结果扫描描述符。

ExecVecSort函数是VecSort算子的主体函数。主要执行流程是:初次执行时,首先调用batchsort_begin_heap函数初始化元组缓存结构,之后循环执行从下层节点获取元组;调用sort_putbatch将获取的元组存放到缓存中;获取全部元组之后,调用batchsort_performsort进行排序;后续对VecSort算子的执行,调用batchsort_getbatch直接从缓存中获取一个元组。

ExecEndVecSort函数用于清理VecSort算子执行过程中使用的资源。主要执行流程是:首先调用ExecClearTuple函数依次清理“node->ss.ss_ScanTupleSlot”元组缓存和“node->ss.ps_ResultTupleSlot”排序后的元组缓存,之后调用batchsort_end函数释放用于元组排序的资源,最后调用“ExecEndNode(outerPlanState(node)) ”函数清理子计划节点。


ExecReScanVecSorts函数用于重新执行扫描计划,执行流程图如图33所示。主要执行流程是:

(1) 判断是否已经进行过排序;如没有执行过,则调用VecExecReScan函数重新扫描执行节点即可,反之则先判断子节点是否已经重新扫描。

(2) 如果子节点已经重新扫描,则调用ExecClearTuple函数清理已经排序的结果元组,调用batchsort_end函数清理“node->tuplesortstate”,调用VecExecReScan函数重新扫描执行节点;如果没有,则判断当前计划是否是“partition-wise join”并且需要切换分区。

(3)如果当前计划是“partition-wise join”并且需要切换分区,则同样需要调用batchstore_end函数和VecExecReScan函数进行重新扫描计划;否则只需要调用“batchstore_rescan(node->tuplesortstate)”。

ExecReScanVecSorts函数执行流程如图33所示。

图33  ExecReScanVecSort函数执行流程

3. VecLimit算子

VecLimit算子用于处理Limit子句,对应的代码源文件是“veclimit.cpp”。VecLimit算子对应的主要数据结构是VecLimitState,VecLimitState继承于LimitState。具体定义代码如下:

struct VecLimitState : public LimitState {    VectorBatch* subBatch;};

VecLimit算子的相关函数包括ExecInitVecLimit(初始化节点)、ExecVecLimit(执行节点)、ExecReScanVecLimit(重置节点)、ExecEndVecLimit(退出节点)。

ExecInitVecLimit函数用于初始化VecLimit算子,将VecLimit计划节点转换为VecLimit执行节点。主要执行流程如下。

(1) 创建VecLimit执行节点,创建表达式上下文,分别初始化limitOffset(调用“ExecInitExpr((Expr)node->limitOffset, (PlanState)limit_state))” 函数和limitCount(调用“ExecInitExpr((Expr)node->limitCount, (PlanState)limit_state)”函数)表达式。

(2) 调用“ExecInitResultTupleSlot(estate, &limit_state->ps) ”函数进行初始化元组。

(3) 调用“ExecInitNode(outer_plan, estate, eflags) ”函数进行初始化外部计划。最后置空投影结构。

ExecVecLimit函数是VecLimit算子的主体函数。函数中通过switch来处理VecLimit算子中存在的多种状态,node->Istate存在的状态有LIMIT_INITIAL、LIMIT_RESCAN、LIMIT_EMPTY、LIMIT_INWINDOW、LIMIT_SUBPLANEOF、LIMIT_WINDOWEND、LIMIT_WINDOWSTART。其中LIMIT_INITIAL表示处理Limit算子初始化,LIMIT_RESCAN表示重新执行子节点计划,LIMIT_EMPTY表示Limit算子是空集,LIMIT_INWINDOW表示处理窗口函数(在窗口函数内前向和后向移动),LIMIT_SUBPLANEOF表示处理子节点计划(移动到子节点计划尾部),LIMIT_WINDOWEND表示在窗口结尾部分结束,LIMIT_WINDOWSTART表示在窗口开始部分结束。

ExecEndVecLimit函数用于在执行VecLimit算子结束时释放相关资源,通过依次调用“ExecFreeExprContext(&node->ps)”函数和“ExecEndNode(outerPlanState(node))”函数释放表达式上下文和节点相关信息。

ExecReScanVecLimit函数用于重新执行扫描计划。在参数发生改变时,通过调用“recompute_limits(node)”函数完成执行节点的重新扫描和VecLimit状态机的重置。在chgParam为空时,还需要调用VecExecReScan函数重新扫描计划节点。

4. VecGroup算子

VecGroup算子用于处理SQL语句中的“GROUP BY”子句,对满足条件的元组做分组处理。

VecGroup算子对应的代码源文件是“vecgroup.cpp”。VecGroup算子对应的主要数据结构是VecGroupState,继承于GroupState。相关代码如下:

struct VecGroupState : public GroupState {    void** container;    void* cap;    uint16 idx;    int cellSize;    bool keySimple;    FmgrInfo* buildFunc;    FmgrInfo* buildScanFunc;    VectorBatch* scanBatch;    VarBuf* currentBuf;    VarBuf* bckBuf;    vecqual_func jitted_vecqual; };

VecGroup算子的相应函数有:ExecInitVecGroup(初始化节点)、ExecVecGroup(执行节点)、ExecEndVecGroup(退出节点)、ExecReScanVecGroup(重置节点)。

ExecInitVecGroup函数用于初始化VecGroup算子,主要执行流程如下。

(1) 创建并初始化VecGroupState执行节点,并为节点创建表达式上下文。

(2) 调用“ExecInitResultTupleSlot(estate,&grp_state->ss.ps);”函数分配存储投影结果的slot。

(3) 调用投影表达式初始化函数ExecInitVecExpr依次对plan.targetlist和plan.qual进行初始化。

(4) 调用ExecInitNode函数初始化子节点。

(5) 调用ExecAssignResultTypeFromTL函数初始化结果扫描描述符和调用ExecAssignVectorForExprEval函数创建投影结构。

ExecVecGroup函数是VecGroup算子的主体函数。主要执行流程如下。

(1) 获取下层元组中符合having子句条件的第1个元组。

(2) 依次获取组内的所有元组,直到获取到分组属性不同的元组,此时表示当前分组获取结束;如果获取到空元组,则表示完成分组操作,设置grp_done字段为true并结束执行。

(3) 扫描下一个符合having条件的元组,将缓存的元组作为分组的开始,并返回新元组。

(4) 重复(2)、(3)直到结束。

ExecEndVecGroup函数用于清理VecGroup算子执行过程中使用的资源。主要执行流程是:首先调用ExecFreeExprContext函数清理表达式上下文,最后调用“ExecEndNode(outerPlanState(node))”函数清理子计划节点。

ExecReScanVecGroup函数用于重新执行扫描计划,通过调用VecExecReScan函数实现重新扫描。

5. VecAggregation算子

VecAggregation算子用于处理含有聚集函数的操作,将同一分组下的多个元组合并成一个聚集结果元组。

VecAggregation算子对应的代码源文件是“vecagg.cpp”。VecAggregation算子对应的主要数据结构是VecAggState,继承于AggState。相应代码如下:

typedef struct VecAggState : public AggState {    void* aggRun;    VecAggInfo* aggInfo;    char* jitted_hashing;    char* jitted_sglhashing;    char* jitted_batchagg;    char* jitted_sonicbatchagg;    char* jitted_SortAggMatchKey;} VecAggState;

VecAggregation算子对应的核心函数有:ExecInitVecAggregation(初始化节点)、ExecVecAggregation(执行节点)、ExecEndVecAggregation(退出节点)、ExecReScanVecAggregation(重置节点)。

ExecInitVecAggregation函数用于初始化VecAggregation算子。主要执行流程如下。

(1) 创建并初始化VecAggState执行节点,并调用ExecAssignExprContext函数为节点创建表达式上下文。

(2) 调用ExecInitScanTupleSlot函数分配用于扫描的slot,调用ExecInitResultTupleSlot函数分配存储投影结果的slot,调用ExecInitExtraTupleSlot函数为sort_slot进行初始化。

(3) 调用投影表达式初始化函数ExecInitVecExpr依次对“plan.targetlist”和“plan.qual”进行初始化。

(4) 调用ExecInitNode函数初始化子节点,获取其中的Aggref节点。

(5) 使用每个Aggref节点中包含的聚集函数信息进行初始化,构造出对应的AggStatePerAgg。

(6) 最后根据策略类型,初始化相应的状态信息。

ExecVecAggregation函数是VecAggregation算子的主体函数。根据策略类型的不同(hash、plain、sort),调用不同的Runner函数执行。

ExecEndVecAggregation函数用于清理VecAggregation算子执行过程中使用的资源。主要执行流程是:依据选择的策略Hash、sort、plain分别调用freeMemoryContext函数、endSortAgg函数、endPlainAgg函数清理节点信息,之后分别调用ExecFreeExprContext函数和ExecClearTuple函数对表达式上下文和元组缓存进行清理。

ExecReScanVecAggregation函数用于重新执行扫描计划。主要执行流程是:根据策略类型分别调用相应的ResetNecessary函数重置相应执行节点,最后调用VecExecReScan函数实现重新扫描。

6. VecWindowAgg算子

VecWindowAgg算子用于处理窗口函数的聚集操作。不同于Agg算子,窗口函数不会将同一分组中的元组合并为一个,这样就需要对每个元组都产生一个结果元组,其中包含对应的聚集计算结果。

VecWindowAgg算子对应的代码源文件是“vecwindowagg.cpp”。VecWindowAgg算子对应的主要数据结构是VecWindowAggState,继承于WindowAggState。相关代码如下:

typedef struct VecWindowAggState : public WindowAggState {    void* VecWinAggRuntime;    VecAggInfo* windowAggInfo;} VecWindowAggState;

VecWindowAgg算子中对应的核心函数有:ExecInitVecWindowAgg(初始化节点)、ExecVecWindowAgg(执行节点)、ExecEndVecWindowAgg(退出节点)、ExecReScanVecWindowAgg(重置节点)。

ExecInitVecWindowAgg函数用于初始化VecWindowAgg算子,主要执行流程如下。

(1) 创建并初始化VecWindowAgg执行节点,并调用ExecAssignExprContext函数为节点创建表达式上下文。

(2) 调用ExecInitResultTupleSlot函数分配存储投影结果的slot,调用ExecInitScanTupleSlot函数分配用于扫描的slot。

(3) 调用ExecInitVecExpr函数为ps.targetlist初始化投影表达式。

(4) 初始化分区判断函数和排序属性是否相同的操作函数,保存在partEqfunctions、ordEqfunctions中。

(5) 初始化funcs指向的表达式树,构造相关调用信息并存放在perfunc中。

ExecVecWindowAgg函数是VecWindowAgg算子的主体函数,通过调用getBatch执行算子,得到窗口函数的投影结果。

ExecEndVecWindowAgg函数用于清理VecWindowAgg算子执行过程中使用的资源,通过调用batchstore_end函数清理元组缓存,通过调用ExecEndNode函数清理执行节点。

ExecReScanVecWindowAgg函数用于重新执行扫描计划,通过调用ResetNecessary函数重置相应执行节点,通过调用VecExecReScan函数实现重新扫描。

7. VecSetOp算子

VecSetOp算子用于处理EXECEPT和INTERSECT集合操作。一般一个VecSetOp算子中只能处理两个集合之间的集合操作,对于多个集合之间的集合操作,需要多个SetOp实现。

VecSetOp算子对应的代码源文件是“vecsetop.cpp”。VecSetOp算子对应的主要数据结构是VecSetOpState,继承于SetOpState。相关代码如下:

typedef struct VecSetOpState : public SetOpState {    void* vecSetOpInfo;} VecSetOpState;

VecSetOp算子中对应的核心函数有:ExecInitVecSetOp(初始化节点)、ExecVecSetOp(执行节点)、ExecEndVecSetOp(退出节点)、ExecReScanVecSetOp(重置节点)。

ExecInitVecSetOp函数用于初始化VecSetOp算子。主要执行流程如下。

(1) 创建并初始化VecSetOpState执行节点。

(2) 调用ExecInitResultTupleSlot函数分配存储投影结果的slot。

(3) 调用ExecInitnode函数初始化子节点。

(4) 调用ExecAssignResultTypeFromTL函数初始化结果扫描描述符。

ExecVecSetOp函数是VecSetOp算子的主体函数,通过执行VecSetOp算子状态机,产生resultBatch。

ExecEndVecSetOp函数用于清理VecSetOp算子执行过程中使用的资源。通过调用freeMemoryContext函数释放内存上下文,通过调用ExecClearTuple函数清理元组缓存,通过调用ExecEndNode函数清理执行节点。

ExecReScanVecSetOp函数用于重新执行扫描计划,通过调用ExecClearTuple函数清理元组结果缓存,通过调用ResetNecessary函数重置相应执行节点。

6.4  连接算子

1. VecNestLoop算子

VecNestLoop算子对应的主要数据结构是VecNestLoopState,VecNestLoopState继承于NestLoopState。具体定义代码如下:

struct VecNestLoopState : public NestLoopState {    void* vecNestLoopRuntime;    vecqual_func jitted_vecqual;    vecqual_func jitted_joinqual;};

VecNestLoop算子的相关函数包括:ExecInitVecNestLoop(初始化节点)、ExecVecNestLoop(执行节点)、ExecEndVecNestLoop(退出节点)、ExecReScanVecNestLoop(重置节点)。

ExecInitVecNestLoop函数用于初始化VecNestLoop执行算子。主要执行流程如下。

(1) 初始化VecNestLoop执行算子。

(2) 为节点创建表达式上下文,分别处理左右子树,得到外执行计划节点和内执行计划节点。

(3) 初始化元组和投影信息。

ExecVecNestLoop函数是执行VecNestLoop的主体函数,通过执行VecNestLoop状态机,并获得结果元组。

ExecEndVecNestLoop函数用于在执行结束时清理VecNestLoop算子。主要执行流程是:首先释放表达式上下文,之后清空元组,最后清空子计划节点。

ExecReScanVecNestLoop函数用于重新执行扫描计划。主要执行流程是:首先把VecNestLoop计划节点转换成外计划执行节点,之后判断外计划执行节点的chgParam是否为空,若chgParam为空,则重新扫描节点。

2. VecMergeJoin算子

VecMergeJoin算子对应的主要数据结构是VecMergeJoinState,VecMergeJoinState继承于MergeJoinState。具体定义代码如下:

struct VecMergeJoinState : public MergeJoinShared {    /* 向量化执行支持 */    VecMergeJoinClause mj_Clauses;    MJBatchOffset mj_OuterOffset;    MJBatchOffset mj_InnerOffset;    ExprContext* mj_OuterEContext;    ExprContext* mj_InnerEContext;    MJBatchOffset mj_MarkedOffset;    VectorBatch* mj_MarkedBatch;    BatchAccessor m_inputs[2];    MJBatchOffset m_prevInnerOffset;    bool m_prevInnerQualified;    MJBatchOffset m_prevOuterOffset;    bool m_prevOuterQualified;    bool m_fDone;    VectorBatch* m_pInnerMatch;    MJBatchOffset* m_pInnerOffset;    VectorBatch* m_pOuterMatch;    MJBatchOffset* m_pOuterOffset;    VectorBatch* m_pCurrentBatch;    VectorBatch* m_pReturnBatch;vecqual_func jitted_joinqual; };

VecMergeJoin算子的相关函数包括:ExecInitVecMergeJoin(初始化节点)、ExecVecMergeJoinT(执行节点)、ExecEndVecMergeJoin(退出节点)、ExecReScanVecMergeJoin(重置节点)。

ExecInitVecMergeJoin函数用于初始化VecMergeJoin执行算子。主要执行流程如下。

(1) 初始化VecMergeJoin执行算子。

(2) 为节点创建表达式上下文,分别处理左右子树,得到外执行计划节点和内执行计划节点。

(3) 初始化元组和投影信息。

ExecVecMergeJoinT函数是执行VecMergeJoin的主体函数,执行VecMergeJoin状态机,并根据join类型,获取结果元组。

ExecEndVecMergeJoin函数用于在执行结束时清理VecMergeJoin算子。首先释放表达式上下文,之后清空元组,最后清空左右子树节点。

ExecReScanVecMergeJoin函数用于重新执行扫描计划。主要执行流程是:首先重置节点相关参数,之后判断左右子树的chgParam是否为空;若chgParam为空时,则重新扫描节点。

3. VecHashJoin算子

VecHashJoin算子对应的主要数据结构是VecHashJoinState,VecHashJoinState继承于HashJoinState。具体定义如下:

typedef struct VecHashJoinState : public HashJoinState {    int joinState;    void* hashTbl;    FmgrInfo* eqfunctions;vecqual_func jitted_joinqual;vecqual_func jitted_hashclause;     char* jitted_innerjoin;    char* jitted_matchkey;    char* jitted_buildHashTable;    char* jitted_probeHashTable;    int enable_fast_keyMatch;    BloomFilterRuntime bf_runtime;    char* jitted_hashjoin_bfaddLong;    char* jitted_hashjoin_bfincLong;    char* jitted_buildHashTable_NeedCopy;} VecHashJoinState;

VecHashJoin算子的相关函数包括:ExecInitVecHashJoin(初始化节点)、ExecVecHashJoin(执行节点)、ExecEndVecHashJoin(退出节点)、ExecReScanVecHashJoin(重置节点)。

ExecInitVecHashJoin函数用于初始化Vechash join执行算子,并把VecHashJoin计划节点转换成计划执行节点。主要执行流程是:首先处理左子树,得到外执行计划节点;再处理右子树,得到内执行计划节点;最后初始化元组和投影信息。

ExecVecHashJoin函数是执行VecHashJoin的主体函数,执行VecHashJoin状态机。

ExecEndVecHashJoin函数用于在执行结束时清理VecHashJoin算子。主要执行流程是:首先释放内存上下文,之后释放表达式,清空左右子树。流程如图34所示。

图34  ExecEndVecHashJoin函数执行流程

ExecReScanVecHashJoin函数用于重新执行扫描计划。主要执行流程是:首先判断状态信息,如哈希表为空时,只需要重新扫描左子树计划,否则需要重新构建哈希表。

七、小结

本章节主要介绍了执行器的总体框架、执行器算子、向量化引擎;向量化引擎通过编译执行模块实现执行加速。执行器接收Plan(优化器输出),对Plan做转换处理,生成状态树,状态树的节点对应执行算子(这些算子利用存储和索引提供的接口,实现数据读写);执行器是SQL语句同存储交互的中介。这些执行算子有统一的接口以及相似的执行流程(初始化、迭代执行、清理3个过程)。向量化引擎面向OLAP场景需求,同编译执行相结合提供高效执行效率。

往期回顾

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

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


本文分享自微信公众号 - openGauss(openGauss)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部