【PostgreSQL内核学习(十五)—— (ExecutorRun)】

ExecutorRun

  • 概述
  • ExecutorRun 函数
    • standard_ExecutorRun 函数
    • exec_explain_plan 函数
    • PlanAnalyzerOperator 函数
    • RecordQueryPlanIssues 函数
    • instr_stmt_report_query_plan 函数
    • ExplainNodeFinish 函数

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

概述

  在【PostgreSQL内核学习(八)—— 查询执行(查询执行策略)】中我们了解到,Portal 的执行过程为:CreatePortal —> PortalDefineQuery —> PortalStart —> PortalRun —> PortalDrop 。随后,我们在在【PostgreSQL内核学习(十一)—— (CreatePortal)】、【PostgreSQL内核学习(十二)—— (PortalStart)】 和 【PostgreSQL内核学习(十三)—— (PortalRun)】中学习了 Portal 的执行过程。本文则紧接着来学习 ExecutorRun 函数。
  ExecutorRun 函数和 Portal 之间的关系是在执行 SQL 查询时的协作关系PortalPostgreSQL 中用于处理查询的抽象层,它可以看作是一个查询计划的容器,包含了查询计划和相关的执行状态信息。ExecutorRun 函数的主要作用是执行一个给定的 Portal,它接受 Portal 作为参数,并负责执行其中包含的查询计划
  具体来说,当执行一个 SQL 查询时,首先通过解析分析优化阶段生成查询计划。然后,将该查询计划存储在一个 Portal 中,同时设置执行的参数等。接下来,通过调用 ExecutorRun 函数来执行这个 Portal,这个函数会根据查询计划的指令逐步执行查询,将查询结果发送到指定的结果接收器中。
  总结一下,ExecutorRun 函数是用于执行查询计划的核心函数,而 Portal 则是用于组织和管理查询计划执行的数据结构。在查询执行过程中,ExecutorRun 函数会根据 Portal 中的查询计划逐步执行查询,这两者密切协作以实现 SQL 查询的执行。

ExecutorRun 函数

  ExecutorRun 函数是 PostgreSQL 执行器模块中关键函数,它接受查询描述符并负责执行查询计划。其作用在于协调执行查询操作,包括解释计划执行查询记录统计信息执行钩子函数,从而实现了数据库查询的核心功能,保证了查询的正确性和性能。这一函数的意义在于确保数据库能够高效、准确地处理各种查询请求,是数据库引擎的核心组成部分之一

以下是 ExecutorRun 函数的执行过程的详细解释:

  1. 准备变量和上下文
  • 首先,函数声明了一些变量,包括 instrument_option查询执行的统计选项)、has_track_operator(是否需要跟踪运算符历史)和 old_stmt_name(用于保存旧的语句名称)等。
  • 检查当前会话是否与 SPIServer Programming Interface)连接,如果是,则将当前语句名称清空。
  1. 执行计划解释
  • 调用 exec_explain_plan 函数,用于执行查询计划的解释
  1. 处理运算符历史统计
  • 检查是否启用了工作负载管理,并且需要跟踪运算符历史。如果是流式计划并且需要跟踪资源,则进入下一步。
  1. 初始化全局运算符统计信息
  • 如果是协调节点Coordinator)并且设置了运算符历史跟踪选项,且全局运算符统计信息尚未初始化,以及计划语句涉及的节点数量不为 0,则初始化全局运算符统计信息
  • 这包括设置运算符历史跟踪选项为查询的并行度分配资源,并初始化相关上下文
  1. 检查是否可以记录运算符历史统计信息
  • 检查是否需要跟踪资源并且存在查询描述,并且已经设置了需要跟踪运算符历史。如果满足条件,则设置 can_operator_history_statisticstrue
  1. 记录节点执行完成的统计信息
  • 如果可以记录运算符历史统计信息,调用 ExplainNodeFinish 函数,记录节点执行完成的统计信息。
  1. 执行函数钩子
  • 如果存在 ExecutorRun_hook 钩子函数,则调用该函数,以便插件或扩展可以在这一点上获取控制权。
  1. 执行查询计划
  • 如果没有钩子函数,或者钩子函数执行完成,调用 standard_ExecutorRun 函数执行查询计划
  • 执行查询计划意味着按照指定的方向(例如向前或向后)检索查询的结果集
  1. 报告插入/删除/更新/合并操作的时间
  • 如果当前会话是协调节点单节点,并且执行的操作是插入删除更新合并,则报告操作的执行时间。
  1. SQL 自适应
  • 根据运行时信息分析查询计划问题。如果需要跟踪资源、存在运算符历史跟踪,且是协调节点单节点,则执行以下步骤:
    • 调用 PlanAnalyzerOperator 函数分析查询计划问题
    • 如果发现查询计划问题,则将问题记录到系统视图 gs_wlm_session_history 中。
  1. 打印查询执行时间
  • 调用 print_duration 函数,用于记录打印查询的执行时间
  1. 报告查询计划
  • 调用 instr_stmt_report_query_plan 函数,报告查询的执行计划
  1. 记录运算符历史统计信息
    如果可以记录运算符历史统计信息,将 u_sess->instr_cxt.can_record_to_table 设置为 true,并再次调用 ExplainNodeFinish 函数,记录节点执行完成的统计信息。
  2. 清理工作
    最后,恢复旧的语句名称,以确保不影响后续查询的执行。

  ExecutorRun 函数代码如下所示:(路径:src/gausskernel/runtime/executor/execMain.cpp

void ExecutorRun(QueryDesc *queryDesc, ScanDirection direction, long count)
{
    /* 
     * 这是执行器模块的主要函数,负责接受来自Traffic Cop的查询描述符并执行查询计划。
     * 要求ExecutorStart必须已经被调用。
     * 如果direction为NoMovementScanDirection,那么除了启动/关闭目标外,不会执行任何操作。
     * 否则,我们按照指定的方向检索最多'count'个元组。
     * 注意:count = 0被解释为没有门户限制,即一直执行直到完成。
     * 注意:count限制仅适用于检索的元组,不适用于由ModifyTable计划节点插入/更新/删除的元组。
     * 没有返回值,但输出元组(如果有)将发送到QueryDesc中指定的目标接收器;
     * 并且可以在estate->es_processed中找到顶层处理的元组数。
     * 我们提供一个函数钩子变量,允许可加载插件在调用ExecutorRun时获得控制。
     * 这样的插件通常会调用standard_ExecutorRun()。
     */

    /* sql活动特性,运算符历史统计信息 */
    int instrument_option = 0; // 查询执行的统计选项
    bool has_track_operator = false; // 是否需要跟踪运算符历史
    char* old_stmt_name = u_sess->pcache_cxt.cur_stmt_name; // 保存旧的语句名称

    // 如果当前会话与SPI连接,则将当前语句名称清空
    if (u_sess->SPI_cxt._connected >= 0) {
        u_sess->pcache_cxt.cur_stmt_name = NULL;
    }

    exec_explain_plan(queryDesc); // 执行查询计划的解释

    /* 
     * 如果启用了工作负载管理且需要跟踪运算符历史,
     * 并且查询描述符和计划语句描述符存在,
     * 并且计划语句为流式计划(Stream Plan),
     * 并且需要跟踪资源,那么进入下面的条件。
     */
    if (u_sess->attr.attr_resource.use_workload_manager &&
        u_sess->attr.attr_resource.resource_track_level == RESOURCE_TRACK_OPERATOR && 
        queryDesc != NULL && queryDesc->plannedstmt != NULL &&
        queryDesc->plannedstmt->is_stream_plan && u_sess->exec_cxt.need_track_resource) {
#ifdef STREAMPLAN
        if (queryDesc->instrument_options) {
            instrument_option = queryDesc->instrument_options;
        }

        /* 
         * 如果是协调节点(Coordinator)且设置了运算符历史跟踪选项,
         * 且全局运算符统计信息尚未初始化,
         * 且计划语句涉及的节点数量不为0,
         * 那么进入下面的条件。
         */
        if (IS_PGXC_COORDINATOR && instrument_option != 0 && u_sess->instr_cxt.global_instr == NULL &&
            queryDesc->plannedstmt->num_nodes != 0) {
            has_track_operator = true;
            queryDesc->plannedstmt->instrument_option = instrument_option; // 设置运算符历史跟踪选项
            AutoContextSwitch streamCxtGuard(t_thrd.mem_cxt.msg_mem_cxt); // 自动切换内存上下文
            int dop = queryDesc->plannedstmt->query_dop; // 查询的并行度

            /* 初始化全局运算符统计信息 */
            u_sess->instr_cxt.global_instr = StreamInstrumentation::InitOnCn(queryDesc, dop);

            MemoryContext old_context = u_sess->instr_cxt.global_instr->getInstrDataContext(); // 获取旧的内存上下文
            /* 为线程分配运算符统计信息 */
            u_sess->instr_cxt.thread_instr = u_sess->instr_cxt.global_instr->allocThreadInstrumentation(
                queryDesc->plannedstmt->planTree->plan_node_id);
            (void)MemoryContextSwitchTo(old_context); // 切换回旧的内存上下文
        }
#endif
    }

    bool can_operator_history_statistics = false; // 是否可以记录运算符历史统计信息

    /* 
     * 如果需要跟踪资源并且存在查询描述,
     * 且已经设置了需要跟踪运算符历史,
     * 那么设置为可以记录运算符历史统计信息。
     */
    if (u_sess->exec_cxt.need_track_resource && queryDesc &&
        (has_track_operator || (IS_PGXC_DATANODE && queryDesc->instrument_options))) {
        can_operator_history_statistics = true;
    }

    /* 如果可以记录运算符历史统计信息 */
    if (can_operator_history_statistics) {
        ExplainNodeFinish(queryDesc->planstate, NULL, (TimestampTz)0.0, true); // 记录节点执行完成的统计信息
    }

    /* 调用ExecutorRun_hook钩子函数,如果存在的话 */
    if (ExecutorRun_hook) {
        (*ExecutorRun_hook)(queryDesc, direction, count); // 执行钩子函数
    } else {
        standard_ExecutorRun(queryDesc, direction, count); // 执行标准的ExecutorRun函数
    }

    /* 如果是协调节点或单节点 */
    if (IS_PGXC_COORDINATOR || IS_SINGLE_NODE) {
        /* 如果是插入、删除、更新或合并操作 */
        if (queryDesc->operation == CMD_INSERT || queryDesc->operation == CMD_DELETE ||
            queryDesc->operation == CMD_UPDATE || queryDesc->operation == CMD_MERGE) {
            report_iud_time(queryDesc); // 上报插入/删除/更新/合并操作的时间
        }
    }

    /* 
     * SQL自适应:根据运行时信息分析查询计划问题,
     * 当查询执行完成时,如果需要跟踪资源且存在运算符历史跟踪,
     * 且是协调节点或单节点,那么进入下面的条件。
     */
    if (u_sess->exec_cxt.need_track_resource && queryDesc != NULL && has_track_operator &&
        (IS_PGXC_COORDINATOR || IS_SINGLE_NODE)) {
        List *issue_results = PlanAnalyzerOperator(queryDesc, queryDesc->planstate); // 分析查询计划问题

        /* 如果发现查询计划问题,则将其记录到系统视图gs_wlm_session_history中 */
        if (issue_results != NIL) {
            RecordQueryPlanIssues(issue_results); // 记录查询计划问题
        }
    }
    
    print_duration(queryDesc); // 打印查询执行时间
    instr_stmt_report_query_plan(queryDesc); // 报告查询计划

    /* 如果可以记录运算符历史统计信息 */
    if (can_operator_history_statistics) {
        u_sess->instr_cxt.can_record_to_table = true;
        ExplainNodeFinish(queryDesc->planstate, queryDesc->plannedstmt, GetCurrentTimestamp(), false); // 记录节点执行完成的统计信息

#ifdef ENABLE_MULTIPLE_NODES
        /* 如果是协调节点,且全局运算符统计信息存在,则删除它 */
        if ((IS_PGXC_COORDINATOR) && u_sess->instr_cxt.global_instr != NULL) {
#else
        /* 如果是流式顶层消费者节点,且全局运算符统计信息存在,则删除它 */
        if (StreamTopConsumerAmI() && u_sess->instr_cxt.global_instr != NULL) {
#endif
            delete u_sess->instr_cxt.global_instr; // 删除全局运算符统计信息
            u_sess->instr_cxt.thread_instr = NULL;
            u_sess->instr_cxt.global_instr = NULL;
        }
    }

    u_sess->pcache_cxt.cur_stmt_name = old_stmt_name; // 恢复旧的语句名称
}

standard_ExecutorRun 函数

  ExecutorRun 函数是 PostgreSQL 查询执行的入口点,它通过调用 standard_ExecutorRun 函数来执行查询计划,处理结果并监控性能,以确保查询在数据库系统中正确且高效地执行。这是 PostgreSQL 引擎核心的一部分,用于实际处理用户提交的 SQL 查询。
  standard_ExecutorRun 函数是执行查询计划的核心函数。在函数内部,它会进行一系列操作,包括生成机器代码启动元组接收器运行查询计划关闭元组接收器等,最终完成了查询的执行。该函数是 PostgreSQL 查询执行的关键组成部分

下是对 standard_ExecutorRun 函数入参的解释

  1. QueryDesc * queryDesc这是一个指向查询描述符的指针,包含了执行查询所需的所有信息。查询描述符中包括了查询计划执行状态EState)、查询操作类型(例如 SELECT、INSERT、UPDATE、DELETE 等)、结果目标DestReceiver)以及其他查询执行所需的上下文信息
  2. ScanDirection direction这是一个指示查询扫描方向的参数。它可以有以下几个可能的取值:
  • NoMovementScanDirection表示不执行查询扫描,只是启动或关闭结果目标DestReceiver)。通常在一些特殊情况下使用,不用于数据扫描。
  • ForwardScanDirection表示向前扫描数据,通常用于从头到尾读取数据。
  • BackwardScanDirection表示向后扫描数据,通常用于从尾到头读取数据。
  • 其他可能的扫描方向,根据具体需求而定。
  1. long count这是一个用于限制查询结果数量的参数。它指示查询最多应该返回多少行结果。如果设置为 0,表示没有结果数量限制,查询会一直运行直到完成。这通常用于控制查询的行数,以避免返回太多数据。

  standard_ExecutorRun 函数代码如下所示:(路径:src/gausskernel/runtime/executor/execMain.cpp

void standard_ExecutorRun(QueryDesc *queryDesc, ScanDirection direction, long count)
{
    EState *estate = NULL;                       // 执行状态
    CmdType operation;                           // 命令类型(SELECT、INSERT、UPDATE、DELETE 等)
    DestReceiver *dest = NULL;                   // 目标接收器,用于接收查询结果
    bool send_tuples = false;                    // 是否发送查询结果元组的标志
    MemoryContext old_context;                   // 旧的内存上下文
    instr_time starttime;                        // 记录执行开始时间
    double totaltime = 0;                        // 总执行时间

    /* 做一些基本的健全性检查 */
    Assert(queryDesc != NULL);
    estate = queryDesc->estate;
    Assert(estate != NULL);
    Assert(!(estate->es_top_eflags & EXEC_FLAG_EXPLAIN_ONLY));

    /*
     * 切换到查询内存上下文,以便在执行过程中分配内存
     */
    old_context = MemoryContextSwitchTo(estate->es_query_cxt);

    /*
     * 为此查询生成机器代码,如果可用
     */
    if (CodeGenThreadObjectReady()) {
        if (anls_opt_is_on(ANLS_LLVM_COMPILE) && estate->es_instrument > 0) {
            TRACK_START(queryDesc->planstate->plan->plan_node_id, LLVM_COMPILE_TIME);
            CodeGenThreadRuntimeCodeGenerate();
            TRACK_END(queryDesc->planstate->plan->plan_node_id, LLVM_COMPILE_TIME);
        } else {
            CodeGenThreadRuntimeCodeGenerate();
        }
    }

    /* 允许记录执行器的整体运行时间 */
    if (queryDesc->totaltime) {
        queryDesc->totaltime->memoryinfo.nodeContext = estate->es_query_cxt;
        InstrStartNode(queryDesc->totaltime);
    }

    /*
     * 从查询描述符和查询特性中提取必要的信息
     */
    operation = queryDesc->operation;
    dest = queryDesc->dest;

    /*
     * 启动元组接收器,如果查询需要返回结果元组
     */
    estate->es_processed = 0;
    estate->es_last_processed = 0;
    estate->es_lastoid = InvalidOid;

    send_tuples = (operation == CMD_SELECT || queryDesc->plannedstmt->hasReturning);

    /*
     * 为了确保消息的完整性(T-C-Z),无论 u_sess->exec_cxt.executor_stop_flag 的值如何,
     * 都应该发送 'T' 消息。
     */
    if (send_tuples)
        (*dest->rStartup)(dest, operation, queryDesc->tupDesc);

    // 设置全局桶映射信息,如果可用
    if (queryDesc->plannedstmt->bucketMap != NULL) {
        u_sess->exec_cxt.global_bucket_map = queryDesc->plannedstmt->bucketMap[0];
    } else {
        u_sess->exec_cxt.global_bucket_map = NULL;
    }

    (void)INSTR_TIME_SET_CURRENT(starttime);
    /*
     * 运行查询计划
     */
    if (!ScanDirectionIsNoMovement(direction)) {
        if (queryDesc->planstate->vectorized) {
            ExecuteVectorizedPlan(estate, queryDesc->planstate, operation, send_tuples, count, direction, dest);
        } else {
#ifdef ENABLE_MOT
            ExecutePlan(estate, queryDesc->planstate, operation, send_tuples,
                count, direction, dest, queryDesc->mot_jit_context);
#else
            ExecutePlan(estate, queryDesc->planstate, operation, send_tuples, count, direction, dest);
#endif
        }
    }
    totaltime += elapsed_time(&starttime);

    queryDesc->executed = true;

    /*
     * 如果当前计划用于表达式处理,不需要收集仪器数据
     */
    if (estate->es_instrument != INSTRUMENT_NONE && StreamTopConsumerAmI() && u_sess->instr_cxt.global_instr &&
        u_sess->instr_cxt.thread_instr) {
        int node_id = queryDesc->plannedstmt->planTree->plan_node_id - 1;
        int* m_instrArrayMap = u_sess->instr_cxt.thread_instr->m_instrArrayMap;

        u_sess->instr_cxt.thread_instr->m_instrArray[m_instrArrayMap[node_id]].instr.instruPlanData.run_time =
            totaltime;
    }

    /*
     * 关闭元组接收器,如果我们启动了它
     */
    if (send_tuples) {
        (*dest->rShutdown)(dest);
    }
    if (queryDesc->totaltime) {
        InstrStopNode(queryDesc->totaltime, estate->es_processed);
    }

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

函数的执行过程的解释

  1. 初始化变量和内存上下文切换:
  • 函数开始时,它会初始化一些变量,包括查询状态estate)、操作类型operation)、结果目标dest)等。
  • 接着,它会切换到查询的内存上下文es_query_cxt),以便在该上下文中执行查询
  1. 生成机器代码
  • 如果启用了代码生成器Code Generation),函数会生成查询的机器代码。这通常在并行查询等情况下使用,以提高执行速度
  1. 允许总体运行时间的仪表化
  • 如果查询描述符中包含了总体运行时间的仪表化信息queryDesc->totaltime),则允许仪表化开始。
  1. 处理结果目标和元组发送
  • 函数会根据查询的操作类型和计划中是否包含返回结果的节点,决定是否需要发送元组(行数据)给结果目标
  • 如果需要发送元组,会调用结果目标的启动方法rStartup)来准备接收元组
  1. 设置全局分桶映射
  • 如果查询计划中包含了分布式表的分桶映射信息,函数会设置全局的分桶映射,以便查询在分布式环境中正确执行。
  1. 执行查询计划
  • 函数调用 ExecutePlanExecuteVectorizedPlan执行查询计划。这是查询的实际执行阶段,它会遍历执行计划树的节点,从数据源中检索数据,并应用相应的操作(例如过滤、聚合、连接等)。
  • 如果查询是矢量化的,会调用 ExecuteVectorizedPlan;否则,会调用 ExecutePlan。在某些情况下,还会涉及到 JIT 编译(Just-In-Time Compilation)。
  1. 停止总体运行时间的仪表化
  • 如果启用了总体运行时间的仪表化,函数会停止计时并记录总体运行时间。
  1. 记录查询已执行
  • 函数将标记查询已经执行完毕,以避免多次执行同一个查询
  1. 收尾工作
  • 如果查询需要发送元组结果,函数会调用结果目标的关闭方法rShutdown)来完成收尾工作。
  • 如果总体运行时间被仪表化,函数会记录运行时间的统计信息
  1. 内存上下文切换
  • 最后,函数会切换回先前的内存上下文,以释放查询期间分配的内存

exec_explain_plan 函数

  exec_explain_plan 函数用于生成查询计划的解释信息,包括查询计划的节点、成本、参数等详细信息。如果生成解释计划失败,它会报告错误并不会影响查询的执行。其函数源码如下:(路径:src/gausskernel/optimizer/commands/auto_explain.cpp

函数的主要执行过程

  1. 初始化 ExplainState 结构:
  • 函数首先创建一个 ExplainState 结构变量 es,该结构用于存储查询计划的解释信息
  1. 检查查询是否有效
  • 函数调用 is_valid_query(queryDesc) 来检查查询是否有效,以及是否应该生成解释计划
  • 同时,函数检查是否启用了自动查询计划auto_explain_plan() )。
  1. 初始化 ExplainState 结构的属性
  • 设置 es 结构的各种属性,包括:
    • costs指示是否包括查询计划的成本信息
    • nodes指示是否包括查询计划的节点信息
    • format设置输出的解释计划格式为文本EXPLAIN_FORMAT_TEXT)。
    • verbose指示是否生成详细的解释计划
    • analyzetiming设置为 false,表示不生成执行分析信息计时信息
  1. 记录嵌套层级
  • 函数记录查询的嵌套层级,将其附加到输出的解释计划中。
  1. 生成查询文本和输出
  • 调用 ExplainQueryText(&es, queryDesc) 生成查询的文本表示,并将其附加到 es.str 中。
  • 输出数据库节点的名称(g_instance.attr.attr_common.PGXCNodeName)。
  1. 开始解释输出
  • 调用 ExplainBeginOutput(&es) 开始输出解释计划
  1. 打印查询计划
  • 使用 ExplainPrintPlan(&es, queryDesc) 打印查询计划的详细信息。
  1. 结束解释输出
  • 调用 ExplainEndOutput(&es) 结束输出解释计划
  1. 处理异常
  • 使用 PostgreSQL异常处理机制进行错误处理。如果在生成解释计划的过程中发生异常,将执行以下操作:
    • 切换到先前的内存上下文
    • 复制错误信息并清除错误状态
    • 报告解释失败的错误消息
    • 释放相关资源,包括 es.str->data
    • 返回,不生成解释计划
  1. 打印查询参数
  • 调用 print_parameters(queryDesc, es) 打印查询中的参数信息
  1. 输出解释计划
  • 使用 ereport 函数将生成的解释计划输出,包括头信息和解释计划文本
  1. 释放资源
  • 最后,函数释放生成的解释计划文本的内存。
void exec_explain_plan(QueryDesc *queryDesc)
{
    ExplainState es;

    // 检查查询是否有效且是否应生成解释计划
    if (is_valid_query(queryDesc) && auto_explain_plan()) {
        INSTR_TIME_SET_CURRENT(plan_time);
        ExplainInitState(&es);

        // 设置解释计划的属性
        es.costs = true;
        es.nodes = true;
        es.format = EXPLAIN_FORMAT_TEXT;
        es.verbose = true;
        es.analyze = false;
        es.timing = false;

        // 保存旧的 explain_perf_mode 属性,并设置为 EXPLAIN_NORMAL
        int old_explain_perf_mode = t_thrd.explain_cxt.explain_perf_mode;
        t_thrd.explain_cxt.explain_perf_mode = (int)EXPLAIN_NORMAL;

        // 添加解释计划的嵌套层级信息到输出
        appendStringInfo(es.str, "\n---------------------------"
            "-NestLevel:%d----------------------------\n", u_sess->exec_cxt.nesting_level);

        // 生成查询文本
        ExplainQueryText(&es, queryDesc);

        // 输出数据库节点的名称
        appendStringInfo(es.str, "Name: %s\n", g_instance.attr.attr_common.PGXCNodeName);

        // 开始输出解释计划
        ExplainBeginOutput(&es);

        // 记录当前内存上下文
        MemoryContext current_ctx = CurrentMemoryContext;

        PG_TRY();
        {
            // 打印查询计划的详细信息
            ExplainPrintPlan(&es, queryDesc);
            // 结束输出解释计划
            ExplainEndOutput(&es);
        }
        PG_CATCH();
        {
            // 处理异常情况
            MemoryContextSwitchTo(current_ctx);
            ErrorData* edata = CopyErrorData();
            FlushErrorState();
            // 报告解释失败的错误消息
            ereport(u_sess->attr.attr_resource.auto_explain_level,
                (errmsg("\nQueryPlan\nexplain failed\nerror:%s", edata->message), errhidestmt(true)));
            FreeErrorData(edata);
            // 释放资源,包括解释计划文本
            pfree(es.str->data);
            t_thrd.explain_cxt.explain_perf_mode = old_explain_perf_mode;
            return;
        }
        PG_END_TRY();
        t_thrd.explain_cxt.explain_perf_mode = old_explain_perf_mode;

        // 打印查询参数
        print_parameters(queryDesc, es);

        // 输出解释计划,包括头信息和解释计划文本
        ereport(u_sess->attr.attr_resource.auto_explain_level,
            (errmsg("\nQueryPlan\n%s\n", es.str->data), errhidestmt(true)));

        // 释放解释计划文本的内存
        pfree(es.str->data);
    }
}

PlanAnalyzerOperator 函数

  PlanAnalyzerOperator 函数的主要功能是对查询计划的执行进行分析,以识别和报告与操作符级别的性能问题相关的问题。它通过递归方式遍历查询计划树中的每个节点,并在每个节点上执行一系列检查,以查找潜在的问题。如果发现问题,它会将问题描述添加到一个列表中,并在分析完成后返回该列表。这些问题包括数据倾斜估算行数不准确大表作为内部表等。
  这段代码的核心部分是在不同类型的计划节点上执行不同的检查,并根据检查结果将问题描述添加到问题列表中。检查的种类包括大表作为内部表数据倾斜不适合的扫描方法等。该代码还处理了子计划并行执行等复杂情况。

  PlanAnalyzerOperator 函数是用于分析执行计划的函数,其目的是检测执行计划中的问题和性能瓶颈下面是该函数的执行过程解释:

  1. 参数接收 函数接受两个参数,querydescplanstatequerydesc查询描述对象,其中包含了执行计划的信息planstate执行计划的状态信息,表示当前要分析的计划节点
  2. 初始化函数开始时,会初始化一些变量,如 ps当前要分析的计划节点)、plan当前计划节点的计划对象)、issueResults用于存储分析结果的列表)等。
  3. 条件检查 首先会检查一些条件,例如是否到达了计划树的末端全局指令是否已设置等。如果不满足这些条件,函数可能会提前返回。
  4. 递归分析子计划 函数会递归分析当前计划节点的子计划,包括初始化计划节点、左子树、右子树等。这是一个深度优先遍历执行计划树的过程。
  5. 分析当前计划节点 在递归过程中,函数会检查当前计划节点的类型(如 Hash Join、Nest Loop、Seq Scan 等),然后根据不同类型的计划节点执行不同的分析逻辑
  6. 收集分析结果 对于每个计划节点,函数会根据分析逻辑收集问题和性能瓶颈信息,并将其添加到 issueResults 列表中。
  7. 返回结果最终,函数会返回 issueResults 列表,其中包含了分析结果,这些结果描述了执行计划中的问题和建议
/*
 * - 简述:
 *     SQL自助调整操作级别问题分析的主要函数入口,当执行完成并捕获运行时信息时调用,
 *     这个过程以一种自底向上的递归方式工作,首先到达叶子节点,必须这样做,因为我们希望一次遍历整个计划树,
 *     而需要问题去重。对于问题去重,如果底下的节点已经是"数据倾斜"或"E-Rows不准确",那么只报告问题的根节点。
 *
 * - 参数:
 *   @querydesc:保存plannedstmt对象的查询描述
 *   @planstate:正在分析的当前(顶级)计划节点
 *
 * - 返回:
 *   找到的计划问题列表(操作级别)
 */
List* PlanAnalyzerOperator(QueryDesc* querydesc, PlanState* planstate)
{
    PlanState* ps = planstate;
    Plan* plan = NULL;
    List* issueResults = NIL;
    QueryPlanIssueDesc* issueResultsItem = NULL;
    bool skip_inaccurate_erows = false;

    /* 当我们到达计划树的末尾时返回NIL */
    if (ps == NULL) {
        return NIL;
    }

    /* 当仪器框架尚未设置时返回NIL */
    if (u_sess->instr_cxt.global_instr == NULL) {
        return NIL;
    }

    plan = ps->plan;

    /*
     * 跳过一些情况,其中A-Rows/E-Rows不反映实际返回的行数
     * - [1]. 子计划包含Limit/VecLimit操作符,我们停止分析当前操作符及其下级操作符
     * - [2]. Material/VecMaterial表示计划节点重新扫描,A-rows不反映实际返回行数。
     *   注意:MergeJoin/NestLoop的内部计划树也是重新扫描的情况,在当前版本中,我们仅报告覆盖大多数重新扫描情况的材料节点,
     *   我们将通过增强explain-perf框架来改进整体重新扫描情况,以告诉我们扫描行数和返回行数。
     */
    if (nodeTag(plan) == T_Limit || nodeTag(plan) == T_VecLimit) {
        elog(DEBUG1,
            "跳过分析数据倾斜、E-Rows不准确,因为在PlanNode[%d]:%s下的计划节点",
            plan->plan_node_id,
            OperatorName(plan));

        return NIL;
    } else if (nodeTag(plan) == T_Material || nodeTag(plan) == T_VecMaterial) {
        /*
         * 对于Material计划节点,我们将跳过分析E-rows不准确的问题,
         * 因为它可能被重新扫描,A-rows不反映查询计划期间的实际行数
         */
        elog(DEBUG1,
            "跳过由于重新扫描导致的不准确的E-Rows分析,PlanNode[%d]:%s下的计划节点",
            plan->plan_node_id,
            OperatorName(plan));
        skip_inaccurate_erows = true;
    }

    /* 输出调试信息 */
    elog(DEBUG1, "QueryAnalayzer 检查PlanNode[%d]:%s", plan->plan_node_id, OperatorName(plan));

    /* 展开子计划节点,初始化SubPlanState节点(非关联表达式的子查询) */
    if (ps->initPlan) {
        ListCell* lc = NULL;
        foreach (lc, ps->initPlan) {
            SubPlanState* sps = (SubPlanState*)lfirst(lc);
            issueResults = list_concat(issueResults, PlanAnalyzerOperator(querydesc, (PlanState*)sps->planstate));
        }
    }

    /* 递归分析左计划树 */
    if (ps->lefttree) {
        issueResults = list_concat(issueResults, PlanAnalyzerOperator(querydesc, ps->lefttree));
    }

    /* 递归分析右计划树 */
    if (ps->righttree) {
        issueResults = list_concat(issueResults, PlanAnalyzerOperator(querydesc, ps->righttree));
    }

    /* 查找特殊计划节点中的计划列表。 */
    List* ps_list = getPlanSubNodes(ps);

    /* 对于SubPlanState节点的表达式子查询中的子计划 */
    if (ps->subPlan) {
        ListCell* lc = NULL;
        foreach (lc, ps->subPlan) {
            SubPlanState* sps = (SubPlanState*)lfirst(lc);
            issueResults = list_concat(issueResults, PlanAnalyzerOperator(querydesc, (PlanState*)sps->planstate));
        }
    }

    if (ps_list != NIL) {
        ListCell* lc = NULL;

        /* 继续分析底层计划节点,因为还没有得到叶子计划节点 */
        foreach (lc, ps_list) {
            PlanState* plan_state = (PlanState*)lfirst(lc);
            issueResults = list_concat(issueResults, PlanAnalyzerOperator(querydesc, plan_state));
        }

        list_free(ps_list);
    }

    {
        /* 在计划上运行时,忽略问题 */
        if (plan->exec_type == EXEC_ON_COORDS || plan->exec_type == EXEC_ON_NONE) {
            return issueResults;
        }
        /* 如果plan->exec_type == EXEC_ON_ALL_NODES,并且m_planIdOffsetArray[plan->plan_node_id - 1] == 0
         * 表示计划在协调器上运行,应该忽略。
         */
        int* m_planIdOffsetArray = u_sess->instr_cxt.global_instr->get_planIdOffsetArray();
        if (plan->exec_type == EXEC_ON_ALL_NODES && m_planIdOffsetArray[plan->plan_node_id - 1] == 0) {
            return issueResults;
        }

        /* 开始分析当前计划节点 */
        int dn_num = 0;
        int dn_index = 0;
        double dn_tuples = 0.0;
        double dnFiltereds = 0.0;
        double min_dn_tuples = get_float8_infinity();
        double max_dn_tuples = 0.0;
        double total_tuples = 0.0;
        double totalFiltereds = 0.0;
        bool write_file = false;
        int dop = plan->dop;
        ListCell* nodeitem = NULL;
        List* exec_nodeList = NIL;
        ExecNodes* exec_nodes = NULL;

        /* 从u_sess->instr_cxt.global_instr和计划中提取信息用于SQL调优 */
        exec_nodes = ng_get_dest_execnodes(plan);
        exec_nodeList = exec_nodes->nodeList;

        dn_num = list_length(exec_nodeList);

        foreach (nodeitem, exec_nodeList) {
            dn_index = lfirst_int(nodeitem);
            dn_tuples = 0.0;
            dnFiltereds = 0.0;
            for (int j = 0; j < dop; j++) {
                Instrumentation* node_instr =
                    u_sess->instr_cxt.global_instr->getInstrSlot(dn_index, plan->plan_node_id, j);
                /* 特殊情况下,如果node_instr为NULL,表示计划未执行,应返回并退出 */
                if (node_instr == NULL)
                    return issueResults;
                dn_tuples += node_instr->ntuples;

                /* 索引扫描:过滤 + 重检删除元组 */
                dnFiltereds += node_instr->nfiltered1 + node_instr->nfiltered2;

                /* 计数优化器不计算的布隆过滤行数
                 * 用于检查数据倾斜和不准确的估计行数
                 */
                if (node_instr->bloomFilterRows > 0 && node_instr->bloomFilterBlocks == 0) {
                    dn_tuples += node_instr->bloomFilterRows;
                }

                /*
                 * 准备报告HashJoin中作为Inner表的大表的情况,
                 * 为了优雅地报告这种问题,我们需要首先确定是否在Inner表中遇到了溢出问题。
                 */
                if (IsA(plan, VecHashJoin) && !write_file) {
                    /*
                     * 对于VecHashJoin,临时文件溢出信息记录在当前T_VecHashJoin节点中
                     */
                    write_file = node_instr->sorthashinfo.hash_writefile;
                } else if (nodeTag(plan) == T_HashJoin && !write_file) {
                    Assert(plan->righttree->dop == dop);
                    /*
                     * 对于RowHashJoin,临时文件溢出信息记录在其内部(righttree)分支的Hash节点中,所以我们需要在那里获取instr对象
                     */
                    Instrumentation* instrument =
                        u_sess->instr_cxt.global_instr->getInstrSlot(dn_index, plan->righttree->plan_node_id, j);
                    if (instrument != NULL) {
                        /* 表示HashJoin的Inner在此处溢出 */
                        write_file = (instrument->sorthashinfo.nbatch > 1);
                    }
                }
            }

            /* 更新DN级别的最小/最大行数和总行数 */
            min_dn_tuples = Min(dn_tuples, min_dn_tuples);
            max_dn_tuples = Max(dn_tuples, max_dn_tuples);
            total_tuples += dn_tuples;
            totalFiltereds += dnFiltereds;
        }

        switch (nodeTag(plan)) {
            case T_VecHashJoin:
            case T_HashJoin: {
                /* 检查大表作为HashJoin的Inner表的情况 */
                if (write_file &&
                    (issueResultsItem = CheckLargeTableInHashJoinInner(
                         ps, dn_num, ComputeSumOfDNTuples(plan->righttree), ComputeSumOfDNTuples(plan->lefttree))) !=
                        NULL) {
                    issueResults = lappend(issueResults, issueResultsItem);
                }

                break;
            }
            case T_VecNestLoop:
            case T_NestLoop: {
                bool is_eq_nestloop_only = IsEqualConditionandNestLoopOnly(plan);
                /* 检查大表作为带有等式连接条件的Nestloop的情况 */
                if (is_eq_nestloop_only &&
                    (issueResultsItem = CheckLargeTableInNestloopWithEqualCondition(ps, dn_num,
                        Max(ComputeSumOfDNTuples(plan->righttree), ComputeSumOfDNTuples(plan->lefttree)))) != NULL) {
                    issueResults = lappend(issueResults, issueResultsItem);
                }

                break;
            }
            case T_VecStream:
            case T_Stream: {
                /* 检查大表作为广播的情况 */
                if ((issueResultsItem = CheckLargeTableInBroadcast(ps, dn_num, total_tuples)) != NULL) {
                    issueResults = lappend(issueResults, issueResultsItem);
                }

                break;
            }
            case T_SeqScan: {
                /* 检查不适合的顺序扫描方法 */
                issueResultsItem = CheckUnsuitableScanMethod(ps, dn_num, total_tuples, totalFiltereds, false, false);
                issueResults = issueResultsItem != NULL ? lappend(issueResults, issueResultsItem) : issueResults;
                break;
            }
            case T_IndexScan:
            case T_IndexOnlyScan:
            case T_BitmapIndexScan: {
                /* 检查不适合的索引扫描方法 */
                issueResultsItem = CheckUnsuitableScanMethod(ps, dn_num, total_tuples, totalFiltereds, true, false);
                issueResults = issueResultsItem != NULL ? lappend(issueResults, issueResultsItem) : issueResults;
                break;
            }
            case T_CStoreScan:
            case T_DfsScan: {
                /* 检查不适合的CStore顺序扫描方法 */
                issueResultsItem = CheckUnsuitableScanMethod(ps, dn_num, total_tuples, totalFiltereds, false, true);
                issueResults = issueResultsItem != NULL ? lappend(issueResults, issueResultsItem) : issueResults;
                break;
            }
            case T_DfsIndexScan:
            case T_CStoreIndexScan: {
                /* 检查不适合的CStore索引扫描方法 */
                issueResultsItem = CheckUnsuitableScanMethod(ps, dn_num, total_tuples, totalFiltereds, true, true);
                issueResults = issueResultsItem != NULL ? lappend(issueResults, issueResultsItem) : issueResults;
                break;
            }
            default: {
                /* 无需操作,只是为了让编译器静默 */
            }
        }

        /* 分析数据倾斜问题 */
        if ((issueResultsItem = CheckDataSkew(ps, min_dn_tuples, max_dn_tuples)) != NULL) {
            issueResults = lappend(issueResults, issueResultsItem);
        }

        /* 分析估计行数不准确的问题 */
        if (!skip_inaccurate_erows &&
            (issueResultsItem = CheckInaccurateEstimatedRows(ps, dn_num, total_tuples)) != NULL) {
            issueResults = lappend(issueResults, issueResultsItem);
        }
    }

    return issueResults;
}

RecordQueryPlanIssues 函数

  RecordQueryPlanIssues 函数的作用是==将执行计划中的问题记录下来,并存储到会话级内存上下文中,以便后续输出==。它的目的是为了在执行过程中捕获到的计划问题,例如优化器的警告信息或性能问题,能够被有效地记录和检查,以便进一步的分析和调优。

下面是该函数的主要执行过程解释

  1. 参数接收 函数接受一个名为 results 的列表参数,其中包含了查询计划中的问题信息
  2. 条件检查 首先,函数会检查当前数据库的资源跟踪级别是否足够详细,并且检查传递给函数的 results 列表是否为空。如果资源跟踪级别较低或者没有问题信息,函数将提前返回,不进行处理。
  3. 初始化 函数初始化了一些变量,包括一个用于存储问题描述的字符数组 max_issue_desc,以及一个整数 current,表示当前已经记录的字符数量
  4. 复制现有问题描述 如果在会话内存上下文中已经存在计划问题的描述,函数会将其复制到 max_issue_desc 中,并更新 current 变量以跟踪已经记录的字符数量
  5. 扫描问题列表 函数会遍历传递给它的 results 列表,这个列表包含了查询计划中的问题描述。对于每个问题描述,函数将问题的建议描述追加到 max_issue_desc 中,并更新 current 变量以跟踪已经记录的字符数量。在处理问题描述时,函数还会释放相关的内存
  6. 限制描述长度 函数会检查已经记录的字符数量是否超过了一个预定义的最大值 MAX_OPTIMIZER_WARNING_LEN。如果超过了这个长度,函数会截断剩余的问题描述,并记录一个警告
  7. 存储问题描述 最后,函数将记录的问题描述存储在会话内存上下文中,以便稍后查询和查看。这样,用户可以在会话内存中访问查询计划中的问题信息。

  函数源码如下:(路径:src/gausskernel/optimizer/util/plananalyzer.cpp

/*
 * 存储执行计划中的问题到会话级内存上下文中(用于输出)
 */
void RecordQueryPlanIssues(const List* results)
{
    ListCell* lc = NULL;
    errno_t rc;

    // 检查资源跟踪级别是否足够详细或结果列表是否为空
    if (u_sess->attr.attr_resource.resource_track_level < RESOURCE_TRACK_QUERY || results == NIL) {
        return;  // 如果资源跟踪级别较低或结果列表为空,则提前返回
    }

    char max_issue_desc[MAX_OPTIMIZER_WARNING_LEN];

    // 将max_issue_desc缓冲区初始化为全零
    rc = memset_s(max_issue_desc, MAX_OPTIMIZER_WARNING_LEN, '\0', MAX_OPTIMIZER_WARNING_LEN);
    securec_check(rc, "\0", "\0");

    int current = 0;

    // 保留在SQL_Planned阶段找到的现有警告信息
    if (t_thrd.shemem_ptr_cxt.mySessionMemoryEntry->query_plan_issue) {
        // 断言资源跟踪级别为RESOURCE_TRACK_OPERATOR
        Assert(u_sess->attr.attr_resource.resource_track_level == RESOURCE_TRACK_OPERATOR);

        rc = sprintf_s((char*)max_issue_desc, MAX_OPTIMIZER_WARNING_LEN, "%s\n", 
            t_thrd.shemem_ptr_cxt.mySessionMemoryEntry->query_plan_issue);
        securec_check_ss_c(rc, "\0", "\0");

        current += strlen(t_thrd.shemem_ptr_cxt.mySessionMemoryEntry->query_plan_issue) + 1;

        // 释放原先使用的内存空间
        pfree_ext(t_thrd.shemem_ptr_cxt.mySessionMemoryEntry->query_plan_issue);
    }

    // 扫描计划问题列表并将它们存储起来
    foreach (lc, results) {
        QueryPlanIssueDesc* issue = (QueryPlanIssueDesc*)lfirst(lc);
        int issue_str_len = strlen(issue->issue_suggestion->data);

        // 检查是否达到允许的最大计划问题缓冲区长度
        if (MAX_OPTIMIZER_WARNING_LEN - current <= issue_str_len + 1) {
            ereport(LOG,
                (errmodule(MOD_OPT),
                    (errmsg("计划问题报告被截断,其余的计划问题将被跳过"))));
            break;
        }

        rc = sprintf_s((char*)max_issue_desc + current, MAX_OPTIMIZER_WARNING_LEN - current, "%s\n",
            issue->issue_suggestion->data);
        securec_check_ss_c(rc, "\0", "\0");

        current += strlen(issue->issue_suggestion->data) + 1;

        // 删除计划问题
        DeleteQueryPlanIssue(issue);
    }

    // 在工作负载管理器的内存上下文中保存计划问题信息
    AutoContextSwitch memSwitch(g_instance.wlm_cxt->query_resource_track_mcxt);
    t_thrd.shemem_ptr_cxt.mySessionMemoryEntry->query_plan_issue = pstrdup(max_issue_desc);

    return;
}

instr_stmt_report_query_plan 函数

  instr_stmt_report_query_plan 函数用于==报告查询计划信息,并将其存储在执行计划的统计上下文中,以便后续的性能分析和统计==。它会检查传入的 queryDesc 是否为空,以及当前语句的统计上下文是否满足记录计划的条件。如果满足条件,它会通过explain_querydesc 函数生成查询计划的文本表示,然后将该文本存储在统计上下文中。最后,它会输出调试信息,包括查询计划查询文本唯一 SQL 标识。这个函数的目的是为了帮助进行性能分析识别性能瓶颈,特别是对于自动解释exec_auto_explain)功能的支持。

下面是该函数的主要执行过程解释

  1. 函数 instr_stmt_report_query_plan 接受一个名为 queryDesc 的指针作为参数,该指针指向查询描述的结构体。这个函数用于报告查询计划信息
  2. 首先,它通过检查条件来确保是否应该记录查询计划信息。它检查queryDesc 是否为 NULL,并检查统计上下文 ssctx 的级别是否在指定的范围内(STMT_TRACK_L0到STMT_TRACK_L2之间),以及计划大小是否为0。如果这些条件不满足,函数将提前返回,不执行后续操作。
  3. 接下来,它创建一个名为 esExplainState 结构体,用于生成查询计划的文本表示。
  4. 然后,它切换内存上下文到 u_sess->statement_cxt.stmt_stat_cxt,以便在这个上下文中分配内存。这是为了在统计上下文中存储查询计划信息
  5. 函数通过分配足够大小的内存来创建一个字符串来存储查询计划文本内存的大小是查询计划文本的长度加1,这是为了容纳字符串结尾的空字符
  6. 接下来,它使用 memcpy_s 函数将查询计划文本从 es.str->data 复制到先前分配的内存中。
  7. 然后,它在存储的查询计划文本的末尾添加一个空字符,以确保字符串正确终止
  8. 函数将查询计划文本的长度(包括结尾的空字符)存储在 ssctx->plan_size中。
  9. 最后,函数切换回原始的内存上下文oldcontext),以便不影响后续的操作。
  10. 函数使用 ereport 函数输出调试信息,包括查询计划查询文本唯一 SQL 标识。这有助于跟踪查询计划的执行和性能分析
  11. 最后,函数释放查询计划文本的内存,通过 pfree 函数释放 es.str->data

  instr_stmt_report_query_plan 函数源码如下:(路径:src/gausskernel/cbb/instruments/statement/instr_statement.cpp

# 定义函数instr_stmt_report_query_plan,接收一个queryDesc参数
void instr_stmt_report_query_plan(QueryDesc *queryDesc)
{
    # 从全局变量中获取当前语句的统计上下文ssctx
    StatementStatContext *ssctx = (StatementStatContext *)u_sess->statement_cxt.curStatementMetrics;
    
    # 检查是否应该记录查询计划信息的条件
    if (queryDesc == NULL || ssctx == NULL || ssctx->level <= STMT_TRACK_L0
        || ssctx->level > STMT_TRACK_L2 || ssctx->plan_size != 0) {
        return;  # 如果条件不满足,则提前返回
    }

    # 创建一个用于生成查询计划文本的ExplainState结构体
    ExplainState es;
    explain_querydesc(&es, queryDesc);

    # 切换内存上下文到stmt_stat_cxt,以便在统计上下文中分配内存
    MemoryContext oldcontext = MemoryContextSwitchTo(u_sess->statement_cxt.stmt_stat_cxt);

    # 创建一个字符串来存储查询计划文本,分配足够大小的内存
    ssctx->query_plan = (char*)palloc0((Size)es.str->len + 1);

    # 将查询计划文本从es.str->data复制到分配的内存中
    errno_t rc = memcpy_s(ssctx->query_plan, (size_t)es.str->len, es.str->data, (size_t)es.str->len);
    securec_check(rc, "\0", "\0");

    # 在存储的查询计划文本末尾添加一个空字符,以确保字符串正确终止
    ssctx->query_plan[es.str->len] = '\0';

    # 存储查询计划文本的长度(包括结尾的空字符)
    ssctx->plan_size = (uint64)es.str->len + 1;

    # 切换回原始的内存上下文
    (void)MemoryContextSwitchTo(oldcontext);

    # 输出调试信息,包括查询计划、查询文本和唯一SQL标识
    ereport(DEBUG1,
        (errmodule(MOD_INSTR), errmsg("exec_auto_explain %s %s to %lu",
            ssctx->query_plan, queryDesc->sourceText, u_sess->unique_sql_cxt.unique_sql_id)));

    # 释放查询计划文本的内存
    pfree(es.str->data);
}

ExplainNodeFinish 函数

  ExplainNodeFinish 函数的作用是执行计划节点完成时记录执行计划的性能信息,包括查询计划的名称并行度行数等关键信息,以便于性能分析和优化,特别是在资源追踪和性能调试方面的意义重大。

下面是该函数的主要执行过程解释

  1. 首先,它会进行一系列条件检查,确保以下情况才会执行后续操作:
  • 工作负载管理器被启用
  • 资源跟踪级别处于操作符级别
  • 计划节点存在且需要执行激活的SQL计划(需要跟踪)。
  1. 接着,它创建一个 ExplainState 结构体 es解释查询计划,并获取执行计划的关键性能信息
  2. 然后,它在会话内存上下文中分配内存以存储查询计划的信息,并将该信息从 es 中拷贝进来,同时记录查询计划的长度
  3. 接下来,它会生成一个调试日志,包括查询计划的关键信息查询文本来源唯一 SQL 标识
  4. 最后,它会释放 es 中分配的内存空间,并完成函数的执行

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

# 定义函数ExplainNodeFinish,接收多个参数,其中包括result_plan、pstmt、current_time和is_pending
void ExplainNodeFinish(PlanState* result_plan, PlannedStmt *pstmt, TimestampTz current_time, bool is_pending)
{
    # 检查是否启用了工作负载管理
    # 是否资源追踪级别为操作符级别
    # 是否传入的result_plan有效
    # 是否需要执行活动的SQL计划
    if (!u_sess->attr.attr_resource.use_workload_manager ||
        u_sess->attr.attr_resource.resource_track_level != RESOURCE_TRACK_OPERATOR || result_plan == NULL ||
        !NeedExecuteActiveSql(result_plan->plan)) {
            return;  # 如果条件不满足,则提前返回
    }

    # 检查result_plan的instrument是否为空且es_can_history_statistics标志已设置
    if (result_plan->instrument != NULL && result_plan->state->es_can_history_statistics) {
        # 获取result_plan的执行并行度
        int plan_dop = result_plan->instrument->dop;

        # 根据不同的条件,调用ExplainNodePending或ExplainSetSessionInfo函数
        if (is_pending) {
            ExplainNodePending(result_plan);  # 处理挂起的节点
        } else {
            int64 plan_rows = e_rows_convert_to_int64(result_plan->plan->plan_rows);
            Plan* node = result_plan->plan;
            char *plan_name = NULL;

            # 根据节点的类型设置计划名称
            if (nodeTag(node) == T_VecAgg && ((Agg*)node)->aggstrategy == AGG_HASHED && ((VecAgg*)node)->is_sonichash) {
                plan_name = "VectorSonicHashAgg";
            } else if (nodeTag(node) == T_VecHashJoin && ((HashJoin*)node)->isSonicHash) {
                plan_name = "VectorSonicHashJoin";
            } else {
                plan_name = nodeTagToString(nodeTag(node));
            }

            OperatorPlanInfo* opt_plan_info = NULL;

            # 在单节点模式下,从pstmt中提取运算符计划信息
#ifndef ENABLE_MULTIPLE_NODES
            if (pstmt != NULL)
                opt_plan_info = ExtractOperatorPlanInfo(result_plan, pstmt);
#endif /* ENABLE_MULTIPLE_NODES */

            # 调用ExplainSetSessionInfo函数来记录查询计划信息
            ExplainSetSessionInfo(result_plan->plan->plan_node_id,
                result_plan->instrument, 
                result_plan->plan->exec_type == EXEC_ON_DATANODES,
                plan_name,
                plan_dop,
                plan_rows,
                current_time,
                opt_plan_info);
        }
    }

    # 根据节点的类型分别处理不同的子计划
    switch (nodeTag(result_plan->plan)) {
        case T_MergeAppend:
        case T_VecMergeAppend: {
            MergeAppendState* ma = (MergeAppendState*)result_plan;
            for (int i = 0; i < ma->ms_nplans; i++) {
                PlanState* plan = ma->mergeplans[i];
                ExplainNodeFinish(plan, pstmt, current_time, is_pending);
            }
        } break;
        case T_Append:
        case T_VecAppend: {
            AppendState* append = (AppendState*)result_plan;
            for (int i = 0; i < append->as_nplans; i++) {
                PlanState* plan = append->appendplans[i];
                ExplainNodeFinish(plan, pstmt, current_time, is_pending);
            }
        } break;
        case T_ModifyTable:
        case T_VecModifyTable: {
            ModifyTableState* mt = (ModifyTableState*)result_plan;
            for (int i = 0; i < mt->mt_nplans; i++) {
                PlanState* plan = mt->mt_plans[i];
                ExplainNodeFinish(plan, pstmt, current_time, is_pending);
            }
        } break;
        case T_SubqueryScan:
        case T_VecSubqueryScan: {
            SubqueryScanState* ss = (SubqueryScanState*)result_plan;
            if (ss->subplan)
                ExplainNodeFinish(ss->subplan, pstmt, current_time, is_pending);
        } break;
        case T_BitmapAnd:
        case T_CStoreIndexAnd: {
            BitmapAndState* ba = (BitmapAndState*)result_plan;
            for (int i = 0; i < ba->nplans; i++) {
                PlanState* plan = ba->bitmapplans[i];
                ExplainNodeFinish(plan, pstmt, current_time, is_pending);
            }
        } break;
        case T_BitmapOr:
        case T_CStoreIndexOr: {
            BitmapOrState* bo = (BitmapOrState*)result_plan;
            for (int i = 0; i < bo->nplans; i++) {
                PlanState* plan = bo->bitmapplans[i];
                ExplainNodeFinish(plan, pstmt, current_time, is_pending);
            }
        } break;
        default:
            if (result_plan->lefttree)
                ExplainNodeFinish(result_plan->lefttree, pstmt, current_time, is_pending);
            if (result_plan->righttree)
                ExplainNodeFinish(result_plan->righttree, pstmt, current_time, is_pending);
            break;
    }

    # 处理initPlan中的子计划
    ListCell* lst = NULL;
    foreach (lst, result_plan->initPlan) {
        SubPlanState* sps = (SubPlanState*)lfirst(lst);

        if (sps->planstate == NULL) {
            continue;
        }
        ExplainNodeFinish(sps->planstate, pstmt, current_time, is_pending);
    }

    # 处理subPlan中的子计划
    foreach (lst, result_plan->subPlan) {
        SubPlanState* sps = (SubPlanState*)lfirst(lst);

        if (sps->planstate == NULL) {
            continue;
        }
        ExplainNodeFinish(sps->planstate, pstmt, current_time, is_pending);
    }
}

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