六、向量化引擎
传统的行执行引擎大多采用一次一元组的执行模式,这样在执行过程中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场景需求,同编译执行相结合提供高效执行效率。