写点什么

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

作者:openGauss
  • 2023-04-25
    广东
  • 本文字数:14149 字

    阅读完需:约 46 分钟

在上一篇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

关注

还未添加个人签名 2020-11-13 加入

openGauss是一款高性能、高安全、高可靠的企业级开源关系型数据库。

评论

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