【OpenGauss源码学习 —— 执行算子(Result 算子)】

执行算子(Result 算子)

  • 控制算子
  • Result 算子
    • ExecInitResult 函数
      • ResultState 结构体
      • ExecInitResultTupleSlot 函数
      • ExecAllocTableSlot函数
    • ExecResult 函数
      • TupleTableSlot 结构体
      • ExecProcNode 函数
      • ExecProcNodeByType 函数
      • ExecProject 函数
    • ExecEndResult 函数
      • ExecFreeExprContext 函数
      • ExecClearTuple 函数
      • ExecEndNode函数
    • ExecResultMarkPos 函数
      • ExecMarkpos 函数
    • ExecResultRestrPos 函数
    • ExecReScanResult 函数
      • ExecReScan 函数

声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 OpenGauss1.1.0 的开源代码和《OpenGauss数据库源码解析》和《PostgresSQL数据库内核分析》一书

控制算子

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

算子名称 说 明
Result算子 处理只有一个结果或过滤条件是常量的流程
Append算子 处理包含一个或多个子计划的链表
BitmapAnd算子 对结果做And位图运算
BitmapOr算子 对结果做Or位图运算
RecursionUnion算子 递归处理UNION语句

Result 算子

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

主要函数 说 明
ExecInitResult 初始化状态机
ExecResult 迭代执行算子
ExecEndResult 结束清理
ExecResultMarkPos 标记扫描位置
ExecResultRestrPos 重置扫描位置
ExecReScanResult 重置执行计划

  为了更好地理解和学习 Result 算子的相关操作,我们还是从一个实际案例来入手吧。首先执行以下 sql 语句:

-- 创建 employees 表
CREATE TABLE employees (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50),
    age INT,
    salary DECIMAL(10, 2)
);

-- 插入一些数据
INSERT INTO employees (name, age, salary) VALUES
    ('Alice', 28, 60000.00),
    ('Bob', 35, 75000.00),
    ('Charlie', 22, 45000.00);

-- 执行查询
SELECT * FROM employees;

-- 查询结果
 id |  name   | age |  salary
----+---------+-----+----------
  1 | Alice   |  28 | 60000.00
  2 | Bob     |  35 | 75000.00
  3 | Charlie |  22 | 45000.00
(3 rows)

ExecInitResult 函数

  ExecInitResult 函数是用于初始化 Result 算子节点(结果节点)的运行时状态信息的。在 PostgreSQL 查询计划中,Result 节点用于产生查询的最终结果,并且通常是查询计划树的最后一个节点。该函数负责为 Result 节点创建一个运行时状态结构体(ResultState),初始化相关属性,并初始化其子节点的运行时状态。
  首先,执行以下SQL语句,并在 ExecInitResult 函数上打上断点:

INSERT INTO employees (name, age, salary) VALUES ('Tom', 23, 60000.00);

  函数的调用关系如下:

【OpenGauss源码学习 —— 执行算子(Result 算子)】_第1张图片
  ExecInitResult 函数源码如下:(路径:src/gausskernel/runtime/executor/nodeResult.cpp

/* ----------------------------------------------------------------
 *		ExecInitResult
 *
 *		为由规划器生成的结果节点创建运行时状态信息,并初始化外部关系
 *		(子节点)。
 * ----------------------------------------------------------------
 */
ResultState* ExecInitResult(BaseResult* node, EState* estate, int eflags)
{
    /* 检查是否有不支持的标志 */
    Assert(!(eflags & (EXEC_FLAG_MARK | EXEC_FLAG_BACKWARD)) || outerPlan(node) != NULL);

    /*
     * 创建状态结构体
     */
    ResultState* resstate = makeNode(ResultState);
    resstate->ps.plan = (Plan*)node;
    resstate->ps.state = estate;

    resstate->rs_done = false;
    resstate->rs_checkqual = (node->resconstantqual == NULL) ? false : true;

    /*
     * 杂项初始化
     *
     * 为节点创建表达式上下文
     */
    ExecAssignExprContext(estate, &resstate->ps);

    resstate->ps.ps_TupFromTlist = false;

    /*
     * 元组表初始化
     */
    ExecInitResultTupleSlot(estate, &resstate->ps);

    /*
     * 初始化子表达式
     */
    resstate->ps.targetlist = (List*)ExecInitExpr((Expr*)node->plan.targetlist, (PlanState*)resstate);
    resstate->ps.qual = (List*)ExecInitExpr((Expr*)node->plan.qual, (PlanState*)resstate);
    resstate->resconstantqual = ExecInitExpr((Expr*)node->resconstantqual, (PlanState*)resstate);

    /*
     * 初始化子节点
     */
    outerPlanState(resstate) = ExecInitNode(outerPlan(node), estate, eflags);

    /*
     * 我们不使用内部计划
     */
    Assert(innerPlan(node) == NULL);

    /*
     * 初始化元组类型和投影信息
     * 节点Result中没有涉及关系,将默认的表访问方法类型设置为HEAP
     */
    ExecAssignResultTypeFromTL(&resstate->ps, TAM_HEAP);

    ExecAssignProjectionInfo(&resstate->ps, NULL);

    return resstate;
}

  ExecInitResult 函数的三个入参分别是:
  BaseResult* node: 这是指向查询计划树中的 Result 节点的指针BaseResultResult 节点的基本结构体,包含了 Plan 结构体的成员,以及与结果节点相关的属性。
【OpenGauss源码学习 —— 执行算子(Result 算子)】_第2张图片
  EState* estate: 这是查询的执行状态,包含了在查询执行过程中需要的上下文信息,例如内存管理、表达式计算上下文等。
【OpenGauss源码学习 —— 执行算子(Result 算子)】_第3张图片
  int eflags: 这是一个标志位,用于指示执行的参数。通常包含执行标志的组合,例如 EXEC_FLAG_MARKEXEC_FLAG_BACKWARD

ResultState 结构体

  ResultState 结构体是用于执行查询计划中的 Result 节点的状态信息。在查询执行引擎中,每个查询计划节点都有相应的状态结构体来跟踪执行过程中的状态和信息。Result 节点表示一个结果集,它可能包含了一些常量表达式过滤条件,需要根据这些信息生成最终的查询结果

/* ----------------
 *	 ResultState 信息
 * ----------------
 */
typedef struct ResultState {
    PlanState ps;               /* 这个结构体的第一个字段是 NodeTag */
    ExprState* resconstantqual; /* 用于评估常量条件表达式的状态 */
    bool rs_done;               /* 是否完成了所有的输出 */
    bool rs_checkqual;          /* 是否需要检查过滤条件 */
} ResultState;

  Result 节点被定义成如图 6-22 所示的样子,除了继承Plan 节点的基本属性外,还扩展定义了 resconstantqual 字段。顾名思义,该字段保存只需计算一次的常量表达式。
【OpenGauss源码学习 —— 执行算子(Result 算子)】_第4张图片

ExecInitResultTupleSlot 函数

  ExecInitResultTupleSlot 函数的作用是为执行 Result 节点时准备一个用于存储结果元组的槽(slot)。在查询执行过程中,每个查询计划节点都会有一个或多个槽,用于存储不同阶段产生的元组数据。函数源码如下:(路径:src/gausskernel/runtime/executor/execTuples.cpp

/* ----------------
 *		ExecInitResultTupleSlot
 * ----------------
 */
void ExecInitResultTupleSlot(EState* estate, PlanState* plan_state, TableAmType tam)
{
    plan_state->ps_ResultTupleSlot = ExecAllocTableSlot(&estate->es_tupleTable, tam);
}

  调试信息如下:
在这里插入图片描述

ExecAllocTableSlot函数

  ExecAllocTableSlot 函数由 ExecInitResultTupleSlot 函数调用。函数源码如下:(路径:src/gausskernel/runtime/executor/execTuples.cpp

/* --------------------------------
 *		ExecAllocTableSlot
 *
 *		Create a tuple table slot within a tuple table (which is just a List).
 * --------------------------------
 */
TupleTableSlot* ExecAllocTableSlot(List** tuple_table, TableAmType tupslotTableAm)
{
    TupleTableSlot* slot = MakeTupleTableSlot();

    *tuple_table = lappend(*tuple_table, slot);

    slot->tts_tupslotTableAm = tupslotTableAm;

    return slot;
}

  调式信息如下:
在这里插入图片描述

ExecResult 函数

   ExecResult 函数这实现了执行 Result 节点的逻辑。它首先检查常量条件,如果不满足常量条件,就会返回 NULL,表示没有满足条件的结果。然后它会处理从外部计划获取的元组,通过投影表达式进行计算,然后返回投影的结果。如果投影产生了多个结果,会逐个返回,直到没有更多结果。最后,如果没有外部计划,它会生成常量目标列表中的结果。这个函数在执行查询计划时负责处理 Result 节点的数据生成和处理。函数源码如下所示:(路径:src/gausskernel/runtime/executor/nodeResult.cpp

/* ----------------------------------------------------------------
 *		ExecResult(node)
 *
 *		返回满足限制条件的外部计划产生的元组。由于带有右子树的结果节点
 *		不会被计划,我们完全忽略了右子树(暂时忽略)。-cim 10/7/89
 *
 *		在进行任何处理之前,首先检查只包含常量子句的限制条件。
 *		如果常量限制条件不满足,则总是返回'nil'。
 * ----------------------------------------------------------------
 */
TupleTableSlot* ExecResult(ResultState* node)
{
    TupleTableSlot* outer_tuple_slot = NULL;
    TupleTableSlot* result_slot = NULL;
    PlanState* outer_plan = NULL;
    ExprDoneCond is_done;
    ExprContext* econtext = node->ps.ps_ExprContext;

    /*
     * 检查常量条件,如 (2 > 1),如果尚未进行过检查
     */
    if (node->rs_checkqual) {
        bool qualResult = ExecQual((List*)node->resconstantqual, econtext, false);

        node->rs_checkqual = false;
        if (!qualResult) {
            node->rs_done = true;
            /*
             * 标记这次常量限制条件检查失败,以备将来的特殊情况。当前仅在 GDS 外部扫描中使用。
             */
            u_sess->exec_cxt.exec_result_checkqual_fail = true;
            return NULL;
        }
    }

    /*
     * 检查是否仍在从前一个扫描元组中投影出元组(因为投影表达式中存在返回集合的函数)。
     * 如果是这样,尝试投影另一个元组。
     */
    if (node->ps.ps_TupFromTlist) {
        result_slot = ExecProject(node->ps.ps_ProjInfo, &is_done);
        if (is_done == ExprMultipleResult) {
            return result_slot;
        }
        /* 完成这个源元组... */
        node->ps.ps_TupFromTlist = false;
    }

    /*
     * 重置每个元组的内存上下文,以释放上一个元组循环中分配的表达式计算存储空间。
     * 注意,这只能在我们完成从扫描元组投影出元组后才能进行。
     */
    ResetExprContext(econtext);

    /*
     * 如果 rs_done 为 true,那么意味着我们被要求返回一个常量元组,
     * 并且我们在上次调用 ExecResult() 时已经执行了此操作,
     * 或者我们未通过常量限制条件检查。无论哪种方式,现在我们都完成了。
     */
    while (!node->rs_done) {
        outer_plan = outerPlanState(node);
        if (outer_plan != NULL) {
            /*
             * 检索来自外部计划的元组,直到没有更多元组为止。
             */
            outer_tuple_slot = ExecProcNode(outer_plan);
            if (TupIsNull(outer_tuple_slot))
                return NULL;

            /*
             * 准备计算投影表达式,这些表达式将期望以 OUTER 作为 varno 访问输入元组。
             */
            econtext->ecxt_outertuple = outer_tuple_slot;

            if (node->ps.qual && !ExecQual(node->ps.qual, econtext, false))
                continue;
        } else {
            /*
             * 如果我们没有外部计划,那么我们只是从一个常量目标列表中生成结果。只执行一次。
             */
            node->rs_done = true;
        }

        /*
         * 使用 ExecProject() 形成结果元组,并返回它 --- 除非投影产生了一个空集,
         * 在这种情况下,我们必须循环回来看是否还有更多的 outer_plan 元组。
         */
        result_slot = ExecProject(node->ps.ps_ProjInfo, &is_done);

        if (is_done != ExprEndResult) {
            node->ps.ps_TupFromTlist = (is_done == ExprMultipleResult);
            return result_slot;
        }
    }

    return NULL;
}

  ExecResult 函数调用关系如下:
【OpenGauss源码学习 —— 执行算子(Result 算子)】_第5张图片

  ExecResult 函数迭代输出元组流程如下图所示:
【OpenGauss源码学习 —— 执行算子(Result 算子)】_第6张图片
在这里插入图片描述

TupleTableSlot 结构体

  TupleTableSlot 结构体表示一个元组槽,用于在数据库查询执行过程中存储元组数据。定义如下:(路径:src/include/executor/tuptable.h

typedef struct TupleTableSlot {
    NodeTag type;               /* 节点类型标记 */

    bool tts_isempty;           /* 槽是否为空 */
    bool tts_shouldFree;        /* 是否应该释放 tts_tuple 内存 */
    bool tts_shouldFreeMin;     /* 是否应该释放 tts_mintuple 内存 */
    bool tts_slow;              /* 用于 slot_deform_tuple 的保存状态 */

    Tuple tts_tuple;            /* 物理元组数据,若为虚拟元组则为 NULL */

#ifdef PGXC
    /*
     * PGXC 扩展,支持从远程 Datanode 发送的元组。
     */
    char* tts_dataRow;          /* DataRow 格式的元组数据 */
    int tts_dataLen;            /* 数据行的实际长度 */
    bool tts_shouldFreeRow;     /* 是否应该释放 tts_dataRow 内存 */
    struct AttInMetadata* tts_attinmeta; /* 存储从 DataRow 提取值所需的信息 */
    Oid tts_xcnodeoid;          /* 获取数据行的节点的 Oid */
    MemoryContext tts_per_tuple_mcxt; /* 元组特定的内存上下文 */
#endif

    TupleDesc tts_tupleDescriptor;   /* 槽的元组描述符 */
    MemoryContext tts_mcxt;          /* 槽本身所在的内存上下文 */
    Buffer tts_buffer;               /* 元组的缓冲区,或者是 InvalidBuffer */
    int tts_nvalid;                  /* tts_values 中有效值的数量 */
    Datum* tts_values;               /* 当前每个属性的值 */
    bool* tts_isnull;                /* 当前每个属性的是否为 NULL 标志 */
    MinimalTuple tts_mintuple;       /* 最小元组数据,如果没有则为 NULL */
    HeapTupleData tts_minhdr;        /* 仅用于最小元组情况的工作空间 */
    long tts_off;                    /* 用于 slot_deform_tuple 的保存状态 */
    long tts_meta_off;               /* 用于 slot_deform_cmpr_tuple 的保存状态 */
    TableAmType tts_tupslotTableAm;  /* 槽的元组表类型 */
} TupleTableSlot;

ExecProcNode 函数

  ExecProcNode 函数作用是执行给定的计划节点,以返回一个(另一个)元组。源码如下:(路径:src/gausskernel/runtime/executor/execProcnode.cpp

/* ----------------------------------------------------------------
 *		ExecProcNode
 *
 *		执行给定的节点以返回一个(另一个)元组。
 * ----------------------------------------------------------------
 */
TupleTableSlot* ExecProcNode(PlanState* node)
{
    TupleTableSlot* result = NULL;

    // 检查是否发生中断
    CHECK_FOR_INTERRUPTS();
    MemoryContext old_context;

    /* 响应停止或取消信号。 */
    if (unlikely(executorEarlyStop())) {
        return NULL;
    }

    /* 切换到节点级内存上下文 */
    old_context = MemoryContextSwitchTo(node->nodeContext);

    // 如果有参数发生变化,执行重新扫描操作
    if (node->chgParam != NULL) { 
        ExecReScan(node);       /* 交给 ReScan 处理 */
    }
    
    // 如果有仪器信息,开始节点执行计时
    if (node->instrument != NULL) {
        InstrStartNode(node->instrument);
    }

    // 如果需要存根执行,调用存根执行函数
    if (unlikely(planstate_need_stub(node))) {
        result = ExecProcNodeStub(node);
    } else {
        result = ExecProcNodeByType(node); // 否则,按节点类型执行
    }

    // 如果有仪器信息,记录节点执行信息
    if (node->instrument != NULL) {
        ExecProcNodeInstr(node, result);
    }

    // 切换回旧的内存上下文
    MemoryContextSwitchTo(old_context);

    // 增加行号计数
    node->ps_rownum++;

    return result;
}

ExecProcNodeByType 函数

  ExecProcNodeByType 函数,其作用是根据计划节点的类型执行相应类型的操作,以返回一个元组槽。源码如下:(路径:src/gausskernel/runtime/executor/execProcnode.cpp

TupleTableSlot* ExecProcNodeByType(PlanState* node)
{
    TupleTableSlot* result = NULL;
    switch (nodeTag(node)) {
        case T_ResultState:
            return ExecResult((ResultState*)node);
        case T_ModifyTableState:
        case T_DistInsertSelectState:
            return ExecModifyTable((ModifyTableState*)node);
        case T_AppendState:
            return ExecAppend((AppendState*)node);
        case T_MergeAppendState:
            return ExecMergeAppend((MergeAppendState*)node);
        case T_RecursiveUnionState:
            return ExecRecursiveUnion((RecursiveUnionState*)node);
        case T_SeqScanState:
            return ExecSeqScan((SeqScanState*)node);
        case T_IndexScanState:
            return ExecIndexScan((IndexScanState*)node);
        case T_IndexOnlyScanState:
            return ExecIndexOnlyScan((IndexOnlyScanState*)node);
        case T_BitmapHeapScanState:
            return ExecBitmapHeapScan((BitmapHeapScanState*)node);
        case T_TidScanState:
            return ExecTidScan((TidScanState*)node);
        case T_SubqueryScanState:
            return ExecSubqueryScan((SubqueryScanState*)node);
        case T_FunctionScanState:
            return ExecFunctionScan((FunctionScanState*)node);
        case T_ValuesScanState:
            return ExecValuesScan((ValuesScanState*)node);
        case T_CteScanState:
            return ExecCteScan((CteScanState*)node);
        case T_WorkTableScanState:
            return ExecWorkTableScan((WorkTableScanState*)node);
        case T_ForeignScanState:
            return ExecForeignScan((ForeignScanState*)node);
        case T_ExtensiblePlanState:
            return ExecExtensiblePlan((ExtensiblePlanState*)node);
            // ... 其他类型的执行操作

        default:
            ereport(ERROR,
                (errmodule(MOD_EXECUTOR),
                    errcode(ERRCODE_UNRECOGNIZED_NODE_TYPE),
                    errmsg("unrecognized node type: %d when executing executor node.", (int)nodeTag(node))));
            return NULL;
    }
}

ExecProject 函数

  ExecProject 函数的作用是执行一个投影操作,即根据表达式计算生成一个或多个结果。在查询执行计划中,投影操作用于生成新的元组,这些元组可能来自于底层的计划节点,并且可能涉及到表达式的计算。以下是 ExecProject 函数的基本作用:

  1. 执行投影操作:根据给定的表达式列表,在给定的上下文中计算这些表达式,并生成一个结果元组。
  2. 处理多个结果:表达式列表中的某些表达式可能返回集合,而不是单个值。在这种情况下,ExecProject 可能会返回多个结果。
  3. 返回结果槽ExecProject 函数返回一个 TupleTableSlot,其中包含生成的结果。如果投影操作产生多个结果,可以多次调用 ExecProject 来获取每个结果。
  4. 处理投影信息:函数需要一个 ProjectionInfo 结构,其中包含了投影表达式、目标列表等信息。这些信息用于进行表达式计算和结果生成。
  5. 表达式计算ExecProject 根据给定的表达式列表,在给定的上下文中计算每个表达式的值。这可能涉及到对关联的表进行访问、函数计算等操作。

  ExecProject 函数源码如下:(路径:src/gausskernel/runtime/executor/execQual.cpp

/*
 * ExecProject
 *
 *		projects a tuple based on projection info and stores
 *		it in the previously specified tuple table slot.
 *
 *		Note: the result is always a virtual tuple; therefore it
 *		may reference the contents of the exprContext's scan tuples
 *		and/or temporary results constructed in the exprContext.
 *		If the caller wishes the result to be valid longer than that
 *		data will be valid, he must call ExecMaterializeSlot on the
 *		result slot.
 */
TupleTableSlot* ExecProject(ProjectionInfo* projInfo, ExprDoneCond* isDone)
{
    /*
     * sanity checks
     */
    Assert(projInfo != NULL);

    /*
     * get the projection info we want
     */
    TupleTableSlot *slot = projInfo->pi_slot;
    ExprContext *econtext = projInfo->pi_exprContext;

    /* Assume single result row until proven otherwise */
    if (isDone != NULL)
        *isDone = ExprSingleResult;

    /*
     * Clear any former contents of the result slot.  This makes it safe for
     * us to use the slot's Datum/isnull arrays as workspace. (Also, we can
     * return the slot as-is if we decide no rows can be projected.)
     */
    (void)ExecClearTuple(slot);

    /*
     * Force extraction of all input values that we'll need.  The
     * Var-extraction loops below depend on this, and we are also prefetching
     * all attributes that will be referenced in the generic expressions.
     */
    if (projInfo->pi_lastInnerVar > 0) {
        tableam_tslot_getsomeattrs(econtext->ecxt_innertuple, projInfo->pi_lastInnerVar);
    }

    if (projInfo->pi_lastOuterVar > 0) {
        tableam_tslot_getsomeattrs(econtext->ecxt_outertuple, projInfo->pi_lastOuterVar);
    }

    if (projInfo->pi_lastScanVar > 0) {
    	tableam_tslot_getsomeattrs(econtext->ecxt_scantuple, projInfo->pi_lastScanVar);
    }

    /*
     * Assign simple Vars to result by direct extraction of fields from source
     * slots ... a mite ugly, but fast ...
     */
	int numSimpleVars = projInfo->pi_numSimpleVars; // 获取投影信息中简单变量的数量
	if (numSimpleVars > 0) {
	    Datum* values = slot->tts_values; // 获取当前槽中的数据值数组
	    bool* isnull = slot->tts_isnull; // 获取当前槽中的空值标志数组
	    int* varSlotOffsets = projInfo->pi_varSlotOffsets; // 获取投影信息中变量槽的偏移数组
	    int* varNumbers = projInfo->pi_varNumbers; // 获取投影信息中变量编号数组
	    int i;
	
	    if (projInfo->pi_directMap) {
	        // 特殊情况:直接映射,变量按顺序输出
	        for (i = 0; i < numSimpleVars; i++) {
	            char* slotptr = ((char*)econtext) + varSlotOffsets[i]; // 计算变量槽的指针位置
	            TupleTableSlot* varSlot = *((TupleTableSlot**)slotptr); // 获取变量槽指针
	            int varNumber = varNumbers[i] - 1; // 获取变量编号(从1开始)
	            
	            Assert (varNumber < varSlot->tts_tupleDescriptor->natts); // 断言:变量编号合法
	            Assert (i < slot->tts_tupleDescriptor->natts); // 断言:投影元组的索引合法
	            values[i] = varSlot->tts_values[varNumber]; // 将变量槽中的数据值赋给结果槽
	            isnull[i] = varSlot->tts_isnull[varNumber]; // 将变量槽中的空值标志赋给结果槽
	        }
	    } else {
	        // 需要考虑 varOutputCols[]
	        int* varOutputCols = projInfo->pi_varOutputCols; // 获取投影信息中变量输出列的数组
	
	        for (i = 0; i < numSimpleVars; i++) {
	            char* slotptr = ((char*)econtext) + varSlotOffsets[i]; // 计算变量槽的指针位置
	            TupleTableSlot* varSlot = *((TupleTableSlot**)slotptr); // 获取变量槽指针
	            int varNumber = varNumbers[i] - 1; // 获取变量编号(从1开始)
	            int varOutputCol = varOutputCols[i] - 1; // 获取变量输出列编号(从1开始)
	
	            Assert (varNumber < varSlot->tts_tupleDescriptor->natts); // 断言:变量编号合法
	            Assert (varOutputCol < slot->tts_tupleDescriptor->natts); // 断言:输出列编号合法
	            values[varOutputCol] = varSlot->tts_values[varNumber]; // 将变量槽中的数据值赋给结果槽
	            isnull[varOutputCol] = varSlot->tts_isnull[varNumber]; // 将变量槽中的空值标志赋给结果槽
	        }
	    }
	}


    /*
     * If there are any generic expressions, evaluate them.  It's possible
     * that there are set-returning functions in such expressions; if so and
     * we have reached the end of the set, we return the result slot, which we
     * already marked empty.
     */
    if (projInfo->pi_targetlist) {
        if (!ExecTargetList(
                projInfo->pi_targetlist, econtext, slot->tts_values, slot->tts_isnull, projInfo->pi_itemIsDone, isDone))
            return slot; /* no more result rows, return empty slot */
    }

    /*
     * Successfully formed a result row.  Mark the result slot as containing a
     * valid virtual tuple.
     */
    return ExecStoreVirtualTuple(slot);
}

ExecEndResult 函数

   ExecEndResult 函数的作用是在执行完一个操作节点(可能是查询的一部分)后,释放与该节点相关的资源,包括释放表达式计算上下文、清空结果元组槽,并完成当前节点的结束操作,包括关闭子计划。这有助于确保数据库系统能够高效地管理内存和资源,并保持正确的状态转换。函数源码如下:(路径:src/gausskernel/runtime/executor/nodeResult.cpp

/* ----------------------------------------------------------------
 *		ExecEndResult
 *
 *		通过C例程分配的存储空间
 * ----------------------------------------------------------------
 */
void ExecEndResult(ResultState* node)
{
    /*
     * 释放表达式计算上下文
     */
    ExecFreeExprContext(&node->ps);

    /*
     * 清空元组表
     */
    (void)ExecClearTuple(node->ps.ps_ResultTupleSlot);

    /*
     * 关闭子计划
     */
    ExecEndNode(outerPlanState(node));
}

ExecEndResult 函数包含三部分:

  1. ExecFreeExprContext(&node->ps);:在执行查询期间,数据库引擎可能会使用表达式上下文(exprcontext)来计算各种表达式,例如查询条件计算列等。这一行代码释放了与当前节点相关的表达式上下文,以释放与表达式计算相关的内存。
  2. (void)ExecClearTuple(node->ps.ps_ResultTupleSlot);:在查询中,结果元组槽用于存储当前节点计算出的结果元组。这一行代码清空了结果元组槽,以释放其中存储的元组数据。
  3. ExecEndNode(outerPlanState(node));OpenGauss 使用计划树plan tree)来表示查询计划,其中包含了一系列操作节点,包括顺序扫描聚合连接等。每个操作节点都可能有一个或多个子计划。这一行代码调用了 ExecEndNode 函数,来处理当前节点的结束操作,包括关闭相关的子计划。

ExecFreeExprContext 函数

  ExecFreeExprContext 函数并没有真正释放 ExprContext 所占用的内存,但它会断开计划节点和表达式上下文之间的关联。这意味着 ExprContext 对象仍然存在,但不再与特定的计划节点相关联,从而允许其他操作继续进行,或者在适当的时候由其他地方进行内存释放操作。这可能是为了确保在某些情况下不会意外释放正在使用的内存,而是将释放操作推迟到更合适的时机。ExecFreeExprContext 函数源码如下:(路径:src/gausskernel/runtime/executor/execUtils.cpp

/* ----------------
 *		ExecFreeExprContext
 *
 * A plan node's ExprContext should be freed explicitly during executor
 * shutdown because there may be shutdown callbacks to call.  (Other resources
 * made by the above routines, such as projection info, don't need to be freed
 * explicitly because they're just memory in the per-query memory context.)
 *
 * However ... there is no particular need to do it during ExecEndNode,
 * because FreeExecutorState will free any remaining ExprContexts within
 * the EState.	Letting FreeExecutorState do it allows the ExprContexts to
 * be freed in reverse order of creation, rather than order of creation as
 * will happen if we delete them here, which saves O(N^2) work in the list
 * cleanup inside FreeExprContext.
 * ----------------
 */
void ExecFreeExprContext(PlanState* planstate)
{
    /*
     * Per above discussion, don't actually delete the ExprContext. We do
     * unlink it from the plan node, though.
     */
    planstate->ps_ExprContext = NULL;
}

  调试信息如下:
在这里插入图片描述

ExecClearTuple 函数

  函数 ExecClearTuple 清除槽位中存储的元组数据,并在需要的情况下释放相关资源,例如物理元组、最小元组等。函数还负责将槽位标记为空,并进行一些与内存管理相关的操作。这有助于确保查询执行期间的资源管理和正确性。函数源码如下所示:(路径:src/gausskernel/runtime/executor/execTuples.cpp

/* --------------------------------
 *		ExecClearTuple
 *
 *		该函数用于清除元组表中的一个槽位。
 *
 *		注意:仅清除元组,不清除元组描述符(如果有的话)。
 * --------------------------------
 */
TupleTableSlot* ExecClearTuple(TupleTableSlot* slot) /* 返回:传入的槽位,用于存储元组 */
{
    /*
     * 检查合法性
     */
    Assert(slot != NULL);

    /*
     * 通过 TableAm 清除物理元组或最小元组,如果存在的话。
     */
    if (slot->tts_shouldFree || slot->tts_shouldFreeMin)
    {
        Assert(slot->tts_tupleDescriptor != NULL);
        tableam_tslot_clear(slot);
    }

    /* 
     * 如果 tts_shouldFree 为 false,tts_tuple 可能仍然有效,原始调用者不希望该槽位释放元组。
     */
    slot->tts_tuple = NULL;
    slot->tts_mintuple = NULL;
    slot->tts_shouldFree = false;
    slot->tts_shouldFreeMin = false;

#ifdef PGXC
    if (slot->tts_shouldFreeRow) {
        pfree_ext(slot->tts_dataRow);
    }
    slot->tts_shouldFreeRow = false;
    slot->tts_dataRow = NULL;
    slot->tts_dataLen = -1;
    slot->tts_xcnodeoid = 0;
#endif

    /*
     * 如果存在引用的缓冲区,释放引用的缓冲区的引用。
     */
    if (BufferIsValid(slot->tts_buffer)) {
        ReleaseBuffer(slot->tts_buffer);
    }
    slot->tts_buffer = InvalidBuffer;

    /*
     * 标记为空。
     */
    slot->tts_isempty = true;
    slot->tts_nvalid = 0;

    // 在某些情况下,行解压会使用 slot->tts_per_tuple_mcxt,因此我们需要重置内存上下文。
    // 该内存上下文是由 PGXC 引入的,仅在函数 'slot_deform_datarow' 中使用。
    // PGXC 还在函数 'FetchTuple' 中进行了重置。因此是安全的。
    ResetSlotPerTupleContext(slot);
    return slot;
}

ExecEndNode函数

   ExecEndNode 函数的作用是在查询执行的过程中,对一个执行计划节点(PlanState)进行结束操作。这个函数会执行一系列的清理工作释放资源,以及结束与该节点相关的一些操作。函数源码如下:(路径:src/gausskernel/runtime/executor/execProcnode.cpp

void ExecEndNode(PlanState* node)
{
    if (node == NULL) {
        return;
    }
    cleanup_sensitive_information(); // 清理敏感信息(函数的作用不在代码中详细说明)

    // 如果节点的改变参数集不为空,则释放相关资源
    if (node->chgParam != NULL) {
        bms_free_ext(node->chgParam);
        node->chgParam = NULL;
    }

    // 如果节点有执行计划信息,根据条件终止执行计划
    if (node->instrument != NULL) {
        // 如果是数据节点,结束仪器信息的循环
        if (IS_PGXC_DATANODE) {
            InstrEndLoop(node->instrument);
        }
        // 如果需要执行活动 SQL,移除与计划节点相关的解释信息
        if (NeedExecuteActiveSql(node->plan)) {
            removeExplainInfo(node->plan->plan_node_id);
        }
    }

    // 如果是协调节点(IS_PGXC_COORDINATOR)且处于流顶消费者(StreamTopConsumerAmI()),结束仪器信息的循环
    if (node->instrument != NULL && IS_PGXC_COORDINATOR && StreamTopConsumerAmI()) {
        InstrEndLoop(node->instrument);
    }

    // 如果需要在计划节点上应用存根操作,执行存根操作并返回
    if (planstate_need_stub(node)) {
        ExecEndNodeStub(node);
        return;
    }

    // 根据节点类型终止计划节点的执行
    ExecEndNodeByType(node);
}

ExecResultMarkPos 函数

  ExecResultMarkPos 函数是在查询执行过程中对 “Result” 节点(结果节点)进行标记位置(mark position)操作,以支持后续的恢复restore)操作。函数源码如下:(路径:src/gausskernel/runtime/executor/nodeResult.cpp

/* ----------------------------------------------------------------
 *		ExecResultMarkPos
 * ----------------------------------------------------------------
 */
void ExecResultMarkPos(ResultState* node)
{
    // 获取外部计划节点状态
    PlanState* outer_plan = outerPlanState(node);

    // 如果存在外部计划节点
    if (outer_plan != NULL) {
        // 在外部计划节点上标记位置
        ExecMarkPos(outer_plan);
    } else {
        // 否则,输出调试信息,指示 "Result" 节点不支持标记/恢复操作
        elog(DEBUG2, "Result 节点不支持标记/恢复操作");
    }
}

ExecMarkpos 函数

  ExecMarkpos 函数主要目的是根据传入的节点类型,在执行计划中选择特定类型的操作进行标记扫描位置ExecMarkPos 函数标记扫描位置的作用在于支持在查询执行过程中执行位置的恢复操作。标记扫描位置实际上是记录当前扫描的状态,以便后续可以重新定位到这个状态,从而实现位置的恢复

/*
 * ExecMarkPos
 *
 * 标记当前扫描位置。
 */
void ExecMarkPos(PlanState* node)
{
    // 根据节点类型选择不同的操作
    switch (nodeTag(node)) {
        case T_SeqScanState:
            ExecSeqMarkPos((SeqScanState*)node);
            break;

        case T_IndexScanState:
            ExecIndexMarkPos((IndexScanState*)node);
            break;

        case T_IndexOnlyScanState:
            ExecIndexOnlyMarkPos((IndexOnlyScanState*)node);
            break;

        case T_TidScanState:
            ExecTidMarkPos((TidScanState*)node);
            break;

        case T_ValuesScanState:
            ExecValuesMarkPos((ValuesScanState*)node);
            break;

        case T_MaterialState:
            ExecMaterialMarkPos((MaterialState*)node);
            break;

        case T_SortState:
            ExecSortMarkPos((SortState*)node);
            break;

        case T_ResultState:
            ExecResultMarkPos((ResultState*)node);
            break;

        default:
            /* 除非调用者要求还原,否则不会产生硬错误... */
            elog(DEBUG2, "无法识别的节点类型:%d", (int)nodeTag(node));
            break;
    }
}

ExecResultRestrPos 函数

  ExecResultRestrPos 函数作用是在查询执行中对 “Result” 节点(结果节点)进行位置恢复操作的函数。源码如下所示:(路径:src/gausskernel/runtime/executor/nodeResult.cpp

/* ----------------------------------------------------------------
 *		ExecResultRestrPos
 * ----------------------------------------------------------------
 */
void ExecResultRestrPos(ResultState* node)
{
    // 获取外部计划节点状态
    PlanState* outer_plan = outerPlanState(node);

    // 如果存在外部计划节点
    if (outer_plan != NULL) {
        // 在外部计划节点上执行位置恢复操作
        ExecRestrPos(outer_plan);
    } else {
        // 否则,输出错误信息,指示 "Result" 节点不支持标记/恢复操作
        ereport(ERROR,
            (errmodule(MOD_EXECUTOR),
                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("Result nodes do not support mark/restore"))));
    }
}

  这个函数的主要目的是根据传入的 “Result” 节点状态,在执行计划中进行位置恢复操作。具体步骤如下:

  • 获取外部计划节点状态,这是连接到 “Result” 节点的外部计划节点的状态。
  • 如果存在外部计划节点,就在该外部计划节点上执行位置恢复操作,这是通过调用 ExecRestrPos 函数来实现的。
  • 如果不存在外部计划节点,就会产生一个错误报告,表明 “Result” 节点不支持标记/恢复操作。这是通过 ereport 函数生成一个错误消息,包括错误代码和错误信息来实现的。

ExecReScanResult 函数

  ExecReScanResult 函数作用是在查询执行中对 “Result” 节点(结果节点)进行重新扫描操作。函数源码如下:(路径:src/gausskernel/runtime/executor/nodeResult.cpp

void ExecReScanResult(ResultState* node)
{
    // 标记为未完成
    node->rs_done = false;
    // 将从目标列表获得的元组标记为未完成
    node->ps.ps_TupFromTlist = false;
    // 检查是否需要检查限定条件
    node->rs_checkqual = (node->resconstantqual == NULL) ? false : true;

    /*
     * 如果子节点的 chgParam 不为 null,则计划将由第一个 ExecProcNode 进行重新扫描。
     */
    if (node->ps.lefttree && node->ps.lefttree->chgParam == NULL) {
        // 对子节点进行重新扫描
        ExecReScan(node->ps.lefttree);
    }
}

  这个函数的主要目的是在查询重新扫描时,对 “Result” 节点进行重新扫描操作。具体步骤如下:

  • rs_done 标记设置为未完成,表示还未完成扫描。
  • ps_TupFromTlist 标记设置为未完成,表示还未从目标列表获得元组。
  • 根据是否存在 resconstantqual,确定是否需要检查限定条件,将 rs_checkqual 标记设置为对应的值。
  • 如果 “Result” 节点有左子节点(lefttree)且左子节点的 chgParam 为 NULL,则执行子节点的重新扫描,这是通过调用 ExecReScan 函数来实现的。

ExecReScan 函数

  ExecReScan 函数作用是对执行计划节点进行重新扫描,以便其输出可以重新被扫描。它执行了一系列的操作,包括更新参数重新计算表达式关闭函数等,以准备执行计划节点的重新扫描。其中一些操作可能会涉及到在重新扫描前重置状态和资源。

/*
 * ExecReScan
 *		重置计划节点,使其输出可以重新扫描。
 *
 * 注意,如果计划节点的参数值发生了变化,输出可能与上次不同。
 */
void ExecReScan(PlanState* node)
{
    /* 如果收集时间统计信息,更新它们 */
    if (node->instrument) {
        InstrEndLoop(node->instrument);
    }

    /* 重置行号计数 */
    node->ps_rownum = 0;

    /*
     * 如果参数值发生了变化,传播该信息。
     *
     * 注意:ExecReScanSetParamPlan() 可以向 node->chgParam 添加位,
     * 对应于 InitPlan 将要更新的输出参数。由于我们只在列表上进行一次遍历,
     * 这意味着 InitPlan 只能依赖于列表中出现在其前面的 InitPlan 的输出参数。
     * 目前的限制足够应对一个 InitPlan 可能依赖于另一个 InitPlan 的有限方式,
     * 但最终我们可能需要更多的工作(或者使规划器扩大 extParam/allParam 集合,以包含所依赖的 InitPlan 参数)。
     */
    if (node->chgParam != NULL) {
        ListCell* l = NULL;

        // 遍历 InitPlan 和子计划
        foreach (l, node->initPlan) {
            SubPlanState* sstate = (SubPlanState*)lfirst(l);
            PlanState* splan = sstate->planstate;

            if (splan->plan->extParam != NULL) {
                /* 忽略子计划的本地参数 */
                UpdateChangedParamSet(splan, node->chgParam);
            }
            if (splan->chgParam != NULL) {
                ExecReScanSetParamPlan(sstate, node);
            }
        }
        foreach (l, node->subPlan) {
            SubPlanState* sstate = (SubPlanState*)lfirst(l);
            PlanState* splan = sstate->planstate;

            if (splan->plan->extParam != NULL) {
                UpdateChangedParamSet(splan, node->chgParam);
            }
        }
        /* 接着,为左子树和右子树设置 chgParam。 */
        if (node->lefttree != NULL) {
            UpdateChangedParamSet(node->lefttree, node->chgParam);
        }
        if (node->righttree != NULL) {
            UpdateChangedParamSet(node->righttree, node->chgParam);
        }
    }

    /* 关闭计划节点目标列表中的任何 SRF 函数 */
    if (node->ps_ExprContext) {
        ReScanExprContext(node->ps_ExprContext);
    }

    /* 如果需要存根执行,在这里停止重新扫描 */
    if (!planstate_need_stub(node)) {
        if (IS_PGXC_DATANODE && EXEC_IN_RECURSIVE_MODE(node->plan) && IsA(node, StreamState)) {
            return;
        }

        ExecReScanByType(node);
    }

    /* 如果参数值发生了变化,释放相应资源 */
    if (node->chgParam != NULL) {
        bms_free_ext(node->chgParam);
        node->chgParam = NULL;
    }
}

你可能感兴趣的:(OpenGauss,gaussdb,postgresql,数据库)