写点什么

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

作者:daydayup
  • 2023-07-24
    北京
  • 本文字数:13801 字

    阅读完需:约 45 分钟

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

三、执行算子


执行算子模块包含多种计划执行算子,算子类型如表 7-4 所示,是计划执行的独立单元,用于实现具体的计划动作。执行计划包含 4 类算子,分别是控制算子、扫描算子、物化算子和连接算子,如表 3-1 所示。这些算子统一使用节点(node)表示,具有统一的接口,执行流程采用递归模式。整体执行流程是:首先根据计划节点的类型初始化状态节点(函数名为“ExecInit+算子名”),然后再回调执行函数(函数名为“Exec+算子名”),最后是清理状态节点(函数名为“ExecEnd+算子名”)。本节主要介绍行执行算子,面向列存储的算子在后续章节(向量化引擎)介绍。


表 3-1 执行算子类型



3.1 控制算子


控制算子主要用于执行特殊流程,这类流程通常包含两个以上输入,如 Union 操作,需要把多个子结果(输入),合并成一个。控制算子有多种,如表 3-2 所示。


表 3-2 控制算子



1. Result 算子


Result 算子对应的代码源文件是“nodeResult.cpp”,用于处理只有一个结果(如通过 SELECT 调用可执行函数或表达式,或者 INSERT 语句只包含 Values 字句)或者 WHERE 表达式中的结果是常量(如“SELECT * FROM emp WHERE 2 > 1”,过滤条件“2 > 1”是常量只需要计算一次即可)的流程。由于 openGauss 没有提供单独的投影算子(Projection)和选择算子(Selection),Result 算子也可以起到类似的作用。


Result 算子提供的主要函数如表 3-3 所示。


表 3-3 Result 算子主要函数



ExecInitResult 函数初始化 Result 状态节点,主要执行流程如下。


(1) 构造状态节点,构造 ResultState 状态结构。


(2) 初始化元组表。


(3) 初始化子节点表达式(生成目标列表的表达式、过滤表达式和常量表达式)。


(4) 初始化左子节点(右子节点是空)。


(5) 初始化元组类型和投影信息。


ExecResult 函数迭代输出元组,流程图如图 6 所示,主要执行流程如下。


(1) 检查是否需要做常量表达式计算,如果之前没有做常量表达式计算则需要计算表达式并设置检查标识(如果常量表达式计算结果为 false,则设置约束检查标识位)。


(2) 判断是否需要做投影处理,如果返回结果是集合,则把投影结果直接返回。


(3) 执行元组获取。



图 6 Result 算子执行流程


ExecEndResult 函数是在执行计划执行结束时调用,用于释放执行过程申请的资源(存储空间)。


2. Append 算子


Append 算子对应的代码源文件是“nodeAppend.cpp”,用于处理包含一个或多个子计划的链表。Append 遍历子计划链表逐个执行子计划,当子计划返回全部结果后,迭代执行下一个子计划。Append 算子通常用于 SQL 中的集合操作中,例如多个 Union All 操作,可以对多个子查询的结果取并集;另外 Append 算子还可以用来实现继承表的查询功能。


Append 算子提供的主要函数如表 3-4 所示。


表 3-4 Append 算子主要函数



ExecInitAppend 函数初始化 Append 状态节点,主要执行流程如下。


(1) 初始化 Append 执行状态节点(AppendState)。


(2) 迭代初始化子计划链表(初始化每一个子计划)。


(3) 设置初始迭代子计划。


ExecAppend 函数迭代输出元组,是 Append 算子主体函数。每次从子计划中获取一条元组,直到返回元组为空,则移到下一个子计划(使用 as_whichplan 标记),直至所有子计划都全部执行完。执行流程如图 7 所示。



图 7 Append 算子执行流程


ExecEndAppend 函数负责 Append 节点清理,遍历子计划数组,逐一释放子计划对应的资源。


3. BitmapAnd 算子


BitmapAnd 算子对应的代码源文件是“nodeBitmapAnd.cpp”,用于对多个属性约束都有索引,且属性约束是 And 运算,对结果做 And 位图运算。例如:(colA 约束条件)AND (colB 约束条件),且 colA,colB 建有索引,colA 对应的位图是 Bitmap A,colB 对应的位图是 Bitmap B。位图运算如图 8 所示。



图 8 Bitmap And 操作


BitmapAnd 算子提供的主要函数如表 3-5 所示。


表 3-5 BitmapAnd 算子主要函数



ExecInitBitmapAnd 函数主要执行流程:首先创建 Bitmapand 状态节点;然后再逐一初始化子计划状态节点。


MultiExecBitmapAnd 函数是 BitmaAnd 计划节点的主体函数,通过迭代方式做求交运算,结果集是一个新的节点。


ExecEndBitmapAnd 函数是计划节点退出函数,负责关闭 BitmapAnd 子计划节点。


4. BitmapOr 算子


BitmapOr 节点同 BitmapAnd 节点类似,主要差异是 BitmapAnd 对子计划结果做求交计算(tbm_intersect),而 BitmapOr 是对子计划结果做并集计算(tbm_union)。BitmapOr 算子提供的主要函数如表 3-6 所示。


表 3-6 BitmapOr 算子主要函数



5. RecursiveUnion 算子


RecursiveUnion 算子对应的代码源文件是“nodeRecursiveUnion.cpp”,用于递归处理 UNION 语句。下面给出一个例子,用 SQL 实现 1 到 10 递归求和,语句如下:


/* 递归求和 */WITH RECURSIVE t_recursive_union(i)AS(VALUES (0)UNION ALLSELECT i + 1 FROM t_recursive_union WHERE i < 10)SELECT sum(i) FROM t_recursive_union;/* 查询计划 */Aggregate   CTE t_recursive_union     ->  Recursive Union           ->  Values Scan on "*VALUES*"           ->  WorkTable Scan on t_recursive_union                 Filter: (i < 10)   ->  CTE Scan on t_recursive_union
复制代码


上述例子由 RecursiveUnion 算子处理,初始数据是 VALUSE (0),然后再递归部分处理输出,即“SELECT i + 1 FROM t_recursive_union WHERE i < 10”。


RecursiveUnion 使用左子树获取初始元组(初始迭代种子),使用右子树递归输出其余元组。RecursiveUnion 算子提供的主要函数如表 3-7 所示。


表 3-7 RecursiveUnion 算子主要函数



RecursiveUnion 算子对应的关键结构体代码如下:


typedef struct RecursiveUnion{Planplan;intwtParam;/* 对应的工作表ID */intnumCols;/* 去重属性个数 */AttrNumber *dupColIdx;/* 去重判断属性编号 */Oid   *dupOperators;/* 去重判断函数 */Oid   *dupCollations;longnumGroups;/* 元组树估算 */} RecursiveUnion;
复制代码


ExecInitRecursiveUnion 函数的主要执行流程如下。


(1) 构造递归合并状态节点,并初始化工作表(working_table)和缓存表(intermediate_table),如果需要去除重复则需要构造哈希表上下文。


(2) 初始化左子节点(用于输出初始元组作为迭代种子)和右子节点(用于迭代输出其他满足迭代条件的元组)。


(3) 创建用于去重的哈希表。


ExecRecursiveUnion 函数是 RecursiveUnion 节点的主体函数,它的主要执行流程是:


(1) 执行左子节点,将获取元组直接返回(左子节点用于输出初始迭代种子);如需要去重则把元组加入哈希表中。


(2) 当处理完左节点(所有的初始种子已经输出),则执行右子节点获取其余元组,在执行右子节点时会逐一从工作表(working_table)获取迭代输入,并把非空的元组放入缓存表(intermediate_table)。


(3) 当工作表为空时,则把缓存表作为新的工作表,直至所有的元组都输出(缓存表和工作表都为空),如需要去重则把元组加入哈希表中。


ExecEndRecursiveUnion 是清理函数,负责释放执行过程申请的存储资源(用于去重的哈希表),并关闭左子节点和右子节点。


3.2 扫描算子


扫描算子用于表、结果集、链表子查询等结果遍历,每次获取一条元组作为上层节点的输入。控制算子中的 BitmapAnd/BitmapOr 函数所需的位图与扫描算子(索引扫描算子)密切相关。主要包括顺序扫描(SeqScan)、索引扫描(IndexScan)、位图扫描(BitmapHeapScan)、位图索引扫描(BitmapIndexScan)、元组 TID 扫描(TIDScan)、子查询扫描(SubqueryScan)、函数扫描(FunctionScan)等。扫描算子如表 3-8 所示。


表 3-8 扫描算子



1. SeqScan 算子


SeqScan 算子是最基本的扫描算子,对应 SeqScan 执行节点,对应的代码源文件是“nodeSeqScan.cpp”,用于对基础表做顺序扫描。算子对应的主要函数如表 3-9 所示。


表 3-9 SeqScan 算子主要函数



ExecInitSeqScan 函数初始化 SeqScan 状态节点,负责节点状态结构构造,并初始化用于存储结果的元组表。


ExecSeqScan 函数是 SeqScan 算子执行的主体函数,用于迭代获取每一个元组。ExecSeqScan 函数通过回调函数调用 SeqNext 函数、HbktSeqSampleNext 函数、SeqSampleNext 函数实现获取元组。非采样获取元组时调用 SeqNext 函数;如果需要采样且对应的表采用哈希桶方式存储则调用 HbktSeqSampleNext 函数,否则调用 SeqSampleNext 函数。


2. IndexScan 算子


IndexScan 算子是索引索引扫描算子,对应 IndexScan 计划节点,相关的代码源文件是“nodeIndexScan.cpp”文件。如果过滤条件涉及索引,查询计划对表的扫描使用 IndexScan 算子,利用索引加速元组获取。算子对应的主要函数如表 3-10 所示。


表 3-10 IndexScan 算子主要函数



ExecInitIndexScan 函数负责初始化 IndexScan 状态节点。主要执行流程如下。


(1) 创建 IndexScanState 节点。


(2) 初始化子节点,初始化目标列表、索引过滤条件、原始过滤条件。


(3) 打开对应表。


(4) 打开索引。


(5) 构建索引扫描 Key。


(6) 处理 ORDER BY 对应的 Key。


(7) 启动索引扫描(返回索引扫描描述符 IndexScanDesc)。


(8) 把过滤 Key 传递给索引器。


ExecIndexScan 函数负责迭代获取元组,通过回调函数的形式调用 IndexNext 函数获取元组。IndexNext 函数首先按照扫描 Key 获取元组,然后再执行表达式 indexqualorig 判断元组是否满足过滤条件,如果不满足则需要继续获取。


ExecEndIndexScan 函数负责清理 IndexScanState 节点。主要执行流程如下。


(1) 清理元组占用的槽位。


(2) 关闭索引扫描描述子。


(3) 关闭索引(如果是分区表则需要关闭分区索引及分区映射)。


(4) 关闭表。


3. BitmapIndexScan 算子


BitmapIndexScan 算子通过位图索引做扫描操作,利用位图记录元组在存储页面的偏移位置,对应 BitmapIndexScan 计划节点。BitmapIndexScan 算子相关的代码源文件是“nodeBitmapIndexScan.cpp”。BitmapIndexScan 执行的结果是位图,该算子配合 BitmapHeapScan 算子获取位图对应的元组。算子对应的主要函数如表 3-11 所示。


表 3-11 BitmapIndexScan 算子主要函数



BitmapIndexScan 算子对应的状态节点代码如下:


typedef struct BitmapIndexScanState {    ScanState ss;  /* 节点状态标识 */    TIDBitmap* biss_result;  /* 位图:扫描结果集 */    ScanKey biss_ScanKeys; /* 索引扫描过滤表达式 */    int biss_NumScanKeys; /* 索引扫描键数量 */    IndexRuntimeKeyInfo* biss_RuntimeKeys; /* 索引扫描运行时求值表达式 */    int biss_NumRuntimeKeys; /* 运行时索引扫描键数量 */    IndexArrayKeyInfo* biss_ArrayKeys;/* 扫描键数组 */    int biss_NumArrayKeys; /* 数组长度 */    bool biss_RuntimeKeysReady; /* 运行时扫描键已经计算标识 */    ExprContext* biss_RuntimeContext; /* 求值表达式上下文 */    Relation biss_RelationDesc; /* 索引描述 */    List* biss_IndexPartitionList; /* 分区表对应索引 */    LOCKMODE lockMode; /* 锁模式 */    Relation biss_CurrentIndexPartition; /* 当前对应分区索引 */} BitmapIndexScanState;
复制代码


ExecInitBitmapIndexScan 函数初始化 BitmapIndexScan 状态节点(BitmapIndexScanState)。主要执行流程如下。


(1) 创建 BitmapIndexScanState 节点用于存储状态信息。


(2) 打开索引。


(3) 构建索引扫描 Key。


(4) 启动索引扫描(返回索引扫描描述符 IndexScanDesc)。


(5) 把过滤 Key 传递给索引器。


MultiExecBitmapIndexScan 函数返回所有元组位图。主要执行流程如下。


(1) 准备 Bitmap 结果集,用于存储元组 ID。


(2) 步循环批量获取元组并存储于 Bitmap 结果集。如果有多组过滤 Key(使用函数 ExecIndexAdvanceArrayKeys 判断)则继续循环批量获取元组。


4. BitmapHeapScan 算子


BitmapHeapSan 算子通过位图(BitmapIndexScan 的输出)获取实际的元组,对应的代码源文件是“BitmapHeap.cpp”。算子对应的主要函数如表 3-12 所示。


表 3-12 BitmapHeapScan 算子主要函数



BitmapHeapScan 算子对应的状态节点代码如下:


typedef struct BitmapHeapScanState {    ScanState ss; /* 节点标识 */    List* bitmapqualorig; /* 元组过滤条件 */    TIDBitmap* tbm; /* 位图:来自BitmapIndexScan节点输出 */    TBMIterator* tbmiterator; /* 位图迭代器 */    TBMIterateResult* tbmres; /* 迭代结果 */    TBMIterator* prefetch_iterator; /* 预抓取迭代器 */    int prefetch_pages; /* 预获取页面数量 */    int prefetch_target; /* 当前获取页面 */} BitmapHeapScanState;
复制代码


ExecInitBitmapHeapScan 函数负责初始化 BitmapHeapScan 状态节点(BitmapHeapScanState)。主要执行流程如下。


(1) 创建 BitmapHeapScanState 状态节点。


(2) 初始化子节点,初始化目标列表、索引过滤条件、原始过滤条件。


(3) 打开对应表。


(4) 初始化元组槽位并设置元组迭代获取函数。


(5) 启动表扫描(返回表扫描描述符 TableScanDesc)。


(6) 初始化左子节点(左子节点负责执行位图索引扫描,并返回位图)。


ExecBitmapHeapScan 函数负责迭代输出元组。使用回调函数获取元组,依照表的类型调用 BitmapHeapTblNext 函数或 BitmapHbucketTblNext(哈希桶类型)函数。BitmapHeapTblNext 函数的主要执行流程是:首先初始化位图,然后使用位图迭代器 tbmres 获取元组偏移位置,最后从缓冲区获取元组 slot。


ExecEndBitmapHeapScan 函数负责清理 BitmapHeapScan 状态节点,清理流程类似于 ExecEndIndexScan 函数


5. TIDScan 算子


TIDScan 算子用于通过遍历元组的物理存储位置获取每一个元组(TID 由块编号和偏移位置组成),对应 TIDScanState 计划节点,相应的代码源文件是“nodeTIDScan.cpp”。算子对应的主要函数如表 3-13 所示。


表 3-13 TIDScan 算子主要函数



TID 扫描算子对应的状态节点代码如下:


typedef struct TidScanState {    ScanState ss;       /* 节点标识 */    List* tss_tidquals; /* tid过滤表达式 */    bool tss_isCurrentOf; /* 游标同当前扫描表是否匹配 */    Relation tss_CurrentOf_CurrentPartition; /* 当前扫描分区 */    int tss_NumTids; /* tid数量 */    int tss_TidPtr; /* 当前扫描位置 */    int tss_MarkTidPtr; /* 标记扫描位置 */    ItemPointerData* tss_TidList; /* tid列表 */    HeapTupleData tss_htup; /* 堆元组 */    HeapTupleHeaderData tss_ctbuf_hdr; /* 堆元组头信息 */} TidScanState;
复制代码


ExecInitTidScan 是 TIDScan 节点状态初始化函数。主要执行流程如下。


(1) 创建 TidScanState 节点。


(2) 初始化子节点,初始化目标列表、索引过滤条件、原始过滤条件。


(3) 打开对应表。


(4) 初始化结果元组;


(5) 启动表扫描(返回表扫描描述符 TableScanDesc)。


ExecTidScan 是元组迭代获取函数,通过调用 TidNext 函数实现功能。TidNext 函数首先获取 Tid 列表,并存放到 tss_TidList 数组中,根据 heap_relation 调用 TidFetchTuple 函数或 HbtTidFetchTuble 函数(哈希桶类型)中逐一获取元组(tss_TidPtr 是 tid 在数组中的相对偏移位置,使用函数 InitTidPtr 移动偏移位置)。


6. SubqueryScan 算子


SubqueryScan 算子以子计划为扫描对象,实际执行会转换成调用子节点计划,对应的代码源文件是“nodeSubqueryScan.cpp”。SubqueryScan 状态节点初始由 ExecInitSubqueryScan 函数完成。ExecInitSubqueryScan 函数首先创建 SubqueryScan 状态节点,然后初始化子计划(调用 ExecInitNode 函数实现)。ExecSubqueryScan 函数负责迭代输出元组,通过调用函数 SubqueryNext 实现,在 SubqueryNext 函数中使用 ExecProcNode 函数执行子节点计划。算子对应的主要函数如表 3-14 所示。


表 3-14 SubqueryScan 算子主要函数



7. FunctionScan 算子


FunctionScan 算子用于从函数返回的数据集中获取元组,对应的代码源文件是“nodeFunctionScan.cpp”。算子对应的主要函数如表 3-15 所示。


表 3-15 FunctionScan 算子主要函数



ExecInitFunctionScan 函数负责初始化 FunctionScan 状态节点。主要执行流程如下。


(1) 构造 FuctionScan 状态节点。


(2) 初始化目标表达式和过滤条件表达式。


(3) 根据 functypclass 的类型构造元组表述符(函数返回元组)。


ExecFunctionScan 函数负责迭代输出函数返回元组。主要执行流程如下。


(1) 初始化 tuplestorestate(首次执行存储函数执行的全量结果)。


(2) 从 tuplestorestate 逐一取出元组。


8. ValuesScan 算子


ValuesScan 算子用于处理“Values (…),(…), …”类型语句,从值列表中输出元组,对应与 ValuesScan 计划节点,相关的代码源文件是“nodeValuesScan.cpp”。values_lists 数组存储值表达式列表。算子对应的主要函数如表 3-16 所示。


表 3-16 ValuesScan 主要函数



ExecInitValuesScan 函数初始化 ValuesScan 状态节点,该函数把值表达式链表转换成表达式数组,该表达式数组即为元组集合。


ExecValuesScan 函数迭代输出元组,通过回调函数调用 ValuesNext 函数实现,curr_idx 字段是偏移位置,从 exprlists 数组中逐一取出数值构造元组。


9. CteScan 算子


CteScan 算子用于处理 With 表达式对应的子查询,对应于 CteScan 计划节点,相应的代码源文件是“nodeCteScan.cpp”。算子对应的主要函数如表 3-17 所示。


表 3-17 CteScan 算子主要函数



ExecInitCteScan 函数初始化 CteScan 状态节点。主要执行流程如下。


(1) 获得 Cte 计划节点。


(2) 根据全局参数 prmdata(所有 CteScan 子计划共享)判断当前 CteScan 计划是否为起始 Cte,如果是则构造 cte_table 用于缓存。


(3) 初始化目标表达式和条件过滤表达式。


(4) 初始化元组用于缓存。


ExecCteScan 函数用于迭代获取元组,通过回调函数调用 CteScanNext 实现。主要执行流程是:首先判断缓存数组中是否有未取元组,如果有则取出返回(使用 tuplestore_gettupleslot 函数),否则执行子计划获取元组。


10. WorkTableScan 算子


WorkTableScan 算子用于处理递归项,同 RecursiveUnion 算子紧密关联,对应的代码源文件是“nodeWorkTableScan.cpp”。WorkTableScan 算子处理 RecursiveUnion 子节点中的工作表,提供了对缓存扫描的支持。算子对应的主要函数如表 3-18 所示。


表 3-18 WorkTableScan 算子主要函数



11. PartIterator 算子


PartIterator 算子用于支持分区 wise join,从分区中迭代获取元组,对应的代码源文件是“nodePartIterator.cpp”。PartIterator 算子通过执行子节点计划获取分区遍历获取元组。算子对应的主要函数如表 3-19 所示。


表 3-19 ParIterator 算子主要函数



分区遍历关键数据结构代码如下:


typedef struct PartIterator {    Plan plan;    PartitionType partType;  /* 分区类型 */    int itrs;               /* 分区数量 */    ScanDirection direction;    PartIteratorParam* param;    int startPartitionId;   /* 并行执行起始分区ID */    int endPartitionId;   /* 并行执行分区结束ID */} PartIterator;
typedef struct PartIteratorState { PlanState ps; /* 状态节点类型 */ int currentItr; /* 当前迭代分区索引编号 */} PartIteratorState;
复制代码


ExecInitPartIteratorScan 函数用于初始化 PartIteratorScan 状态节点(PartIteratorState),功能是初始化左子节点并设置初始迭代分区索引号。


ExecPartIteratorScan 函数迭代输出元组。主要执行流程是:首先初始化分区索引号,执行左子节点获取元组,如果元组为空则获取下一个分区的元组。


3.3 物化算子


物化算子用于把元组缓存起来供后续使用。物化算子有多种类型,将介绍部分物化算子。如表 3-20 所示。


表 3-20 物化算子列表



1. Material 算子


Material 算子用于缓存子节点执行结果,对应的代码源文件是“nodeMaterial.cpp”。Material 算子使用函数 tuplestorestate 缓存迭代输出的元组。算子对应的主要函数如表 3-21 所示。


表 3-21 Material 算子主要函数



ExecInitMaterial 函数用于初始化 Material 状态节点,并初始化左子节点。


ExecMaterial 函数用于迭代获取元组。根据计划选择 ExecMaterialOne 函数和 ExecMaterialAll 函数输出元组:ExecMaterialOne 函数从子计划中迭代获取一个元组并放入 tuplestorestate 对象中;ExecMaterialAll 函数从子计划中迭代获取所有的元组并存储在 tuplestorestate 对象中。


ExecEndMaterial 函数是清理函数,主要清理元组缓存。


2. Sort 算子


Sort 算子用于执行排序计划节点(即 SQL 语句中的 ORDER BY 命令),对应的代码源文件是“nodeSort.cpp”。算子对应的主要函数如表 3-22 所示。


表 3-22 Sort 算子主要函数



排序算子对应的结构体是 SortState,该结构体代码如下:


typedef struct SortState {    ScanState ss;          /* 扫描节点 */    bool randomAccess;    /* 随机访问标识*/    bool bounded;         /* 结果集边界标识 */    int64 bound;          /* 结果集中总数 */    bool sort_Done;       /* 排序完成标识 */    bool bounded_Done;   /* 结果集边界设置标识 */    int64 bound_Done;     /* 参与排序的数据集 */    void* tuplesortstate;    /* 排序表 */    int32 local_work_mem;  /* 内存使用 */    int sortMethodId;       /* 所用排序方法(explain) */    int spaceTypeId;        /* 空间类型(explain) */    long spaceUsed;        /* 所用空间大小(explain) */    int64* space_size;      /* 临时表外溢大小 */} SortState;
复制代码


ExecInitSort 函数用于初始化排序节点,创建排序时的状态信息。主要执行流程如下。


(1) 创建 Sort 状态结构体,生成排序状态节点(SortState)。


(2) 对结果元组表初始化(分别调用“ExecInitResultTupleSlot(estate,&sortstate->ss.ps)”函数和“ExecInitScanTupleSlot(estate,&sortstate->ss)”函数)。


(3) 初始化子节点。


(4) 初始化元组类型。


ExecSort 函数是执行排序的主函数。主要执行流程是:


(1) 判断排序状态节点是否已经做过排序,如果没有做过排序,需要调用 tuplesort 函数做一次全部排序。


(2) 使用 tuplesort 函数做排序的流程是先初始化堆排序,然后调用 tuplesort_performsort 函数执行排序。


(3) 根据排序执行节点的逐一读取元组。


ExecSort 函数的执行流程如图 9 所示。



图 9 ExecSort 函数操作流程


ExecEndSort 函数用于释放排序过程使用的资源。主要执行流程是:首先释放用于存放中间元组的排序表,然后清理结果表,最后关闭排序执行计划。


ExecSortMarkPos 函数用于标记排序位置。ExecSortRestrPos 函数用于恢复保存的排序文件。ExecReScanSort 函数用于重置排序结果。


3. Limit 算子


Limit 算子节点主要用来处理 LIMIT/OFFSET 子句,用于限制子查询语句处理元组的数量,对应的代码源文件是“nodeLimit.cpp”。算子对应的主要函数如表 3-23 所示。


表 3-23 Limit 算子主要函数



Limit 算子对应的关键结构体是 LimitState,相关代码如下:


typedef struct LimitState {    PlanState ps;            /* 计划状态节点*/    ExprState* limitOffset;  /* 偏移位置*/    ExprState* limitCount;   /* 总数 */    int64 offset;            /* 当前偏移位置 */    int64 count;             /* 当前总数 */    bool noCount;            /* 忽略总数标识 */    LimitStateCond lstate;   /* 状态机当前状态 */    int64 position;          /* 上一个元组的位置 */    TupleTableSlot* subSlot; /* 上一个元组 */} LimitState;
复制代码


ExecInitLimit 函数用于把 Limit 计划节点转成 Limit 执行节点。主要执行流程如下。


(1) 初始化 Limit 状态节点(LimitState)并做子表达式处理,分别初始化 limitOffset(调用“ExecInitExpr((Expr*)node->limitOffset, (PlanState*)limit_state)”函数)和 limitCount(调用“ExecInitExpr((Expr*)node->limitCount, (PlanState*)limit_state);”函数)表达式。


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


(3) 外部计划初始化(调用“outer_plan = outerPlan(node); outerPlanState(limit_state) = ExecInitNode(outer_plan, estate, eflags);”函数)。


(4) 对投影信息置空(由于 Limit 无投影)。


ExecLimit 是执行 Limit 算子的入口,每次返回一个元组。在函数体内部通过“switch (node->lstate) ”函数来处理 Limit 算子的各种 Limit 状态,如果 Limit 对应的状态不是叶子节点则调用 ExecProcNode 做递归处理。“node->lstate”对应的状态有 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 用于在窗口开始处结束。


recompute_limits 函数用于在初始化时处理 limit/offset 表达式。主要执行流程如下。


(1) 处理计划节点中的 limitOffset,如果非空则对 limitOffset 对应的表达式做求值处理,判断 limitOffset 是否满足约束条件,如果不满足则做报错处理。


(2) 处理计划节点中的 limitCount,如果非空则对 limitCount 对应的表达式做求值处理,判断 limitCount 是否满足约束条件,如果不满足则做报错处理。


(3) 调用 pass_down_bound 递归处理子节点。


4. Group 算子


Group 算子用于处理 GROUP BY 子句(节点),对满足条件的元组做分组处理,对应的代码源文件是“nodeGroup.cpp”。Group 算子对应的子节点返回的元组是按照分组属性排列的结果。算子对应的主要函数如表 3-24 所示。


表 3-24 Group 算子主要函数



ExecInitGroup 函数初始 Group 状态节点。主要执行流程如下。


(1) 构造 Group 状态节点。


(2) 初始化目标表达式和过滤表达式。


(3) 初始化唯一子节点(用于输出元组)。


(4) 获取唯一值过滤函数。


ExecGroup 函数输出分组后的元组。Group 子节点输出的元组已按照分组属性排序,在迭代输出时只要发现同上一个元组属性不匹配,则生成新的元组(新分组)输出。


5. Agg 算子


Agg 算子用于执行含有聚集函数的操作,对应的代码源文件是“nodeAgg.cpp”。Agg 算子支持 3 种策略处理:普通聚集(不分组聚集计算)、排序聚集、哈希聚集。排序聚集和哈希聚集计算都包含 GROUP BY 子句而不分组聚集计算则不包含。普通聚集实际可以看作分组聚集的一种特例(每个元组对应一个分组)。普通聚集与排序聚集使用 agg_retrieve_direct 函数获取元组,哈希聚集使用 agg_retrieve 函数获取元组。算子对应的主要函数如表 3-25 所示。


表 3-25 Agg 算子主要函数



ExecInitAgg 函数用于初始化 Agg 状态节点。主要执行流程如下。


(1) 构建 AggState 状态节点。


(2) 计算最大分组数(迭代阶段)。


(3) 初始化子计划节点(左子节点)。


(4) 初始化聚合函数。


(5) 初始化罗盘文件。


ExecAgg 函数输出聚合元组。从子节点(子计划执行)获取元组,按照指定的属性列聚合,根据不同的聚合调用 agg_retrieve 或 agg_retrieve_direct 函数。agg_retrieve 函数的执行逻辑是:首先准备数据(从子


6. Unique 算子


Unique 算子用于对子计划返回的元组去重,对应的代码源文件是“nodeUnique.cpp”。Unique 算子的去重逻辑建立在子计划返回的元组已经按照属性排序之上,如果不重复则输出,并放入缓存元组中(用作下一次迭代去重判断),否则继续从子计划中获取元组。


7. hash 算子


hash 算子用于辅助 hash 连接算子,对应的代码源文件是“nodeHash.cpp”。hash 算子作为辅助算子,仅用来初始化 hash 状态节点,并提供哈希表创建接口(供 hash join 算子调用),不迭代输出元组(hash join 算子负责输出)。


8. SetOp 算子


SetOp 算子用于处理 Execept 与 Intersect 两种集合操作(INTERSECT、INTERSECT ALL、EXCEPT、EXCECPT ALL),对应的代码源文件是“nodeSetOp.cpp”。Setop 算子只有一个左子节点作为输入。SetOp 算子在处理集合操作时有 2 种策略:排序和哈希。哈希模式(SETOP_HASHED)下处理非有序元组集合,而排序模式(SETOP_SORTED)下处理有序元组集合。


9. WindowAgg 算子


WindowAgg 算子用于处理元组窗口聚合,对应的代码源文件是“nodeWindAgg.cpp”。WindowAgg 算子同 Agg 算子实现的功能类似,实现的模式也类似,主要的差异是窗口聚合处理的元组限定于同一个划分内(窗口),而 Agg 算子处理的元组是“整个表”(GROUP BY 划分)。


10. LockRows 算子


LockRows 算子提供行级锁,用于 SQL 语句包含“FOR UPDATE”(排他锁)或“FOR SHARE”(共享锁)时,对元组加锁。对应的源文件是 nodeLockRows.cpp。LockRows 算子的执行逻辑是从子节点获取元组,然后尝试对元组加锁;如果针对 UPDATE 操作,需要重新检查子查询(执行 EvalPlanQualBegin),并对子查询获得的元组过滤检查,把满足过滤条件的元组返回。


3.4 连接算子


连接算子用于处理表关联,openGauss 支持 12 种连接类型(inner join、left join、right join、full join、semi join、anti join 等),提供了 3 种连接算子:hash join、merge join、nested loop join 算子;下面分别介绍这 3 种算子。


1. hash join 算子


hash join 算子用于 hash 连接处理,对应的代码源文件是“nodeHashJoin.cpp”。hash 连接是做大数据集连接时的常用方式,优化器使用两个表中较小的表(或数据源)利用连接键在内存中建立哈希表,然后扫描较大的表并探测哈希表,找出与哈希表匹配的行。这种方式适用于较小的表完全可以放于内存中的情况,这样总成本就是访问两个表的成本之和。但是在表很大的情况下并不能完全放入内存,执行器会将它分割成若干不同的分区,不能放入内存的部分就把该分区写入磁盘的临时段,此时要有较大的临时段从而尽量提高 I/O 的性能。算子对应的主要函数如表 3-26 所示。


表 3-26 hash join 算子主要函数



hash join 算子对应的状态节点代码如下:


typedef struct HashJoinState {    JoinState js;                          /* Join节点 */    List* hashclauses;                     /* hash计算表达式 */    List* hj_OuterHashKeys;                /* hash外表键表达式 */    List* hj_InnerHashKeys;                /* hash内表键表达式 */    List* hj_HashOperators;                /* hash计算运算符 */    HashJoinTable hj_HashTable;            /* 哈希表 */    uint32 hj_CurHashValue;               /* 当前哈希值 */    int hj_CurBucketNo;                   /* 当前桶编号 */    int hj_CurSkewBucketNo;               /* 倾斜桶编号 */    HashJoinTuple hj_CurTuple;             /* 当前处理元组 */    HashJoinTuple hj_PreTuple;             /* 前一个处理元组 */    TupleTableSlot* hj_OuterTupleSlot;       /* 外元组槽 */    TupleTableSlot* hj_HashTupleSlot;        /* 内元组槽 */    TupleTableSlot* hj_NullOuterTupleSlot;    /* NULL值外元组槽 */    TupleTableSlot* hj_NullInnerTupleSlot;    /* NULL值内元组槽 */    TupleTableSlot* hj_FirstOuterTupleSlot;   /* 第一个外元组槽 */    int hj_JoinState;                      /* 连接状态 */    bool hj_MatchedOuter;                /* 匹配外元组 */    bool hj_OuterNotEmpty;               /* 外表非空 */    bool hj_streamBothSides;              /* 内外表是否都包含stream */    bool hj_rebuildHashtable;              /* 重建哈希表标识 */} HashJoinState;
复制代码


ExecInitHashJoin 函数用于初始化 hash join 执行节点,把 hash join 计划节点转换成计划执行节点,整个转换过程采用递归处理的方式。主要执行流程如下。


(1) 构建 hash join 状态节点(HashJoinState)。


(2) 初始化表达式(目标表达式、join 连接表达式、条件过滤表达式等)。


(3) 初始化左右子节点(初始化外表和内表)。


(4) 初始化 HashKey 及 hash 函数链表。


ExecHashJoin 函数实现元组迭代输出。主要执行流程是:


(1) 获取节点信息,然后做投影处理,接着重置上下文,最后执行 hash join 连接状态机(首先对内表做扫描,根据连接键计算哈希值,放入哈希表。


(2) 扫描外表元组,根据连接键计算哈希值,直接查找哈希表进行连接操作,如果匹配成功将结果输出(在内存中匹配),否则做落盘处理。


(3) 对外表和内表落盘的元组做连接。


ExecHashJoin 函数操作流程图如图 10 所示。



图 10 ExecHashJoin 函数操作流程


ExecEndHashJoin 函数用于资源清理。主要执行流程是:首先释放哈希表,然后清空表达资源,接着释放三个元组表,最后再释放子节点。操作流程如图 11 所示。



图 11 ExecEndHashJoin 函数操作流程


ExecReSetHashJoin 函数用于重置 hash join 状态节点。主要执行流程是:首先调用 ExecHashTableDestroy 释放哈希表,然后调用 ExecReSetRecursivePlanTree 递归重置左右子树。


ExecReScanHashJoin 函数用于重新执行扫描计划。主要执行流程是:首先判断重置状态信息,如果已经递归重置,只需执行重新扫描左右子树计划即可,否则则需要重建哈希表。


2. merge join 算子


merge join 算子用于支持排序结果集连接,对应的代码源文件是“nodeMergeJoin.cpp”。通常情况下 hash 连接的效果都比排序合并连接要好,但如果元组已经被排序,在执行排序合并连接时不需要再排序,这时排序合并连接的性能会优于 hash 连接。merge join 算子连接处理的逻辑同经典的归并排序算法相似,需要首先找到匹配位置,然后迭代获取外表与内表匹配位置。


merge join 算子相关的核心函数包括:ExecInitMergeJoin、ExecMergeJoin。下面分别介绍这两个主要函数。


ExecInitMergeJoin 函数用于初始化 merge join 状态节点。主要执行流程如下。


(1) 创建 merge join 状态节点。


(2) 初始化表达式(目标表达式、join 连接表达式、条件过滤表达式等)。


(3) 初始化内外节点。


(4) 根据 join 类型初始化状态节点信息。


ExecMergeJoin 函数用于处理归并连接。主要执行流程是:通过 2 层 switch 判断当前归并连接的状态(类似与归并排序),计算连接值如发现匹配元组则直接返回,否则继续从外表或内表中获取有序元组,按照连接状态做匹配判断。


3. nested loop join 算子


nested loop join 算子一般用在连接的表中有索引,并且索引选择性较好的时候,对应的代码源文件是“nodeNestedloop.cpp”。对于被连接的数据子集较小的情况,嵌套循环连接是个较好的选择。Nestedloop 算子执行的主要过程是:通过外表(左子节点)驱动内表(内子节点),外表处于外循环,外表返回的每一行都要在内表中检索找到与它匹配的行。因此整个查询返回的结果集不能太大,要把返回子集较小表的作为外表,而且在内表的连接字段上一定要有索引。

用户头像

daydayup

关注

还未添加个人签名 2023-07-18 加入

还未添加个人简介

评论

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