• 【OpenGauss源码学习 —— 执行算子(Nest Loop 算子)】


    声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
    本文主要参考了 OpenGauss5.1.0 的开源代码和《OpenGauss数据库源码解析》一书以及一些参考资料多表连接的三种方式详解 hash join、merge join、 nested loop

    概述

      连接算子用于处理表关联,openGauss支持 12 种连接类型(inner join、left join、right join、full join、semi join、anti join等),提供了 3 种连接算子:hash join、Merge join、nested loop join 算子;其中,在先前的学习中,【OpenGauss源码学习 —— 执行算子(hash join 算子)】一文中详细介绍了 hash join 算子的执行过程。【OpenGauss源码学习 —— 执行算子(Merge Join 算子)】一文中学习了 Merge Join 算子的执行过程。本文则来继续学习另一个扫描算子Nest Loop 算子。

    Nest Loop 算子

      Nest Loop嵌套循环)算子是数据库执行计划中的一种基本算法,用于实现两个数据集的连接操作它的工作原理是,对于参与连接的两个数据集,算子先遍历第一个数据集中的每一条记录,然后对于每一条记录,再遍历第二个数据集,寻找满足连接条件的匹配记录。这种算法的特点是简单直观,但在处理大数据量时可能效率较低,因为它需要对第二个数据集进行多次完整的遍历。因此,Nest Loop 算子更适合于至少有一个参与连接的数据集较小的情况。尽管它的基本原理简单,现代数据库系统会通过各种优化,比如使用索引来减少对第二个数据集的遍历次数,来提高 Nest Loop 连接的效率。
      NestLoop 节点实现了嵌套循环连接方法,能够进行 Inner JoinLeft Outer JoinSemi JoinAnti Join 四种连接方式。图 6-53 展示了 NestLoop 节点及对应的状态节点 NestLoopState 的定义。NestLoop 节点并未对 Join 节点进行扩展,NestLoop 节点的初始化过程会将节点中连接条件(joinqual 字段)进行处理,转化为对应的状态节点 JoinState 中的 joinqual 链表。

    在这里插入图片描述
      在嵌套循环连接(Nest Loop Join)的过程中,算子遍历外部数据集 S 中的每一个元组 s,并对每一个内部数据集 R 中的元组 r 进行遍历。如果 rs 满足连接条件,即可以连接成一个新的元组 t,那么输出 t。如下图所示:(内容来自《PostgresSQL数据库内核分析》329 页)
    在这里插入图片描述

      当存在需要检查是否需要从外部数据集中获取新的元组时,NestLoopState 中有两个关键的字段:nl_NeedNewOuternl_MatchedOuter

    • nl_NeedNewOuter:当这个字段为 true 时,意味着需要从外部数据集中获取新的元组
    • nl_MatchedOuter:当这个字段为 false 时,意味着当前的外部元组没有找到匹配的内部元组,这在执行 Left Outer JoinAnti Join 时特别重要。

      如果在执行 Left Outer JoinAnti Join 时,没有找到匹配的内部元组(即 nl_MatchedOuterfalse),那么需要生成一个包含外部元组和空的内部元组的新元组
      NestLoop 算子的执行逻辑在处理 Left Outer JoinAnti Join 时有特殊的处理逻辑,主要通过更新 NestLoopStatenl_NullInnerTupleSlot 字段来实现,这个字段用于存储一个空的内部元组槽。执行逻辑如下:

    • 如果 nl_NeedNewOutertrue,算子将获取外部数据集的新元组。
    • 如果 nl_MatchedOuterfalse,算子将生成一个包含外部元组和空内部元组的新元组。

      以下代码是定义了一个用于实现嵌套循环连接(Nest Loop Join)的结构体和相关组件,在数据库查询执行计划的实现中使用。(路径:src\include\nodes\plannodes.h

    // 嵌套循环连接节点结构定义
    typedef struct NestLoop {
        Join join;            // 继承自Join结构,包含了连接操作的基本信息,如连接类型等
        List* nestParams;     // 一个包含NestLoopParam节点的列表,用于指定在执行内部子计划时必须传递的执行器参数,这些参数携带来自外部子计划当前行的值
        bool materialAll;     // 一个标志位,指示是否需要物化(即存储到内存中)所有的结果
    } NestLoop;
    
    // VecNestLoop是NestLoop的向量化版本,继承自NestLoop,用于处理向量化的数据处理,以提高性能
    typedef struct VecNestLoop : public NestLoop {
    } VecNestLoop;
    
    // 定义NestLoopParam结构,用于描述如何将外部关系的变量值传递给内部执行计划
    typedef struct NestLoopParam {
        NodeTag type;    // 节点类型标记,用于辨识结构体的类型
        int paramno;     // PARAM_EXEC类型的参数编号,指定要设置的参数
        Var* paramval;   // 指向外部关系变量的指针,该变量的值将被赋给参数
    } NestLoopParam;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

      以上码段定义了实现嵌套循环连接的数据结构,它是数据库执行计划中的一部分,用于执行两个数据集的连接操作。NestLoop 结构体包含了执行连接所需的基本信息,包括连接的类型参数以及是否需要物化所有结果的标志NestLoopParam 结构体用于详细说明如何将外部数据集的当前行值传递到内部查询计划中,以便执行连接条件的匹配。这样的设计允许数据库执行引擎在执行嵌套循环连接时,能够有效地传递和使用来自外部数据集的数据,为查询优化提供了灵活性和高效性。

      算子对应的主要函数如下表所示。

    主要函数说 明
    ExecInitNestLoop初始化 Nest Loop 状态节点
    ExecNestLoop执行 Nest Loop 的主要函数
    ExecEndNestLoop清理 Nest Loop 状态节点
    ExecReScanNestLoop重置 Nest Loop 状态节点

    ExecInitNestLoop 函数

      ExecInitNestLoop 函数是用来初始化一个嵌套循环连接(Nest Loop Join)的执行状态。这个函数创建并设置一个 NestLoopState 结构体,这个结构体将用于在查询执行期间跟踪嵌套循环连接的状态。函数源码如下所示:(路径:src\gausskernel\runtime\executor\nodeNestloop.cpp

    /*
     * ExecInitNestLoop函数用于初始化NestLoop节点的执行状态结构NestLoopState。
     * 该函数对NestLoop节点进行初始化,包括创建状态结构、初始化表达式上下文、
     * 初始化子节点等操作,以及对连接类型的处理和结果元组的初始化。
     * 参数:
     *   - node: NestLoop节点
     *   - estate: 执行状态信息
     *   - eflags: 执行标志
     * 返回值:
     *   - 初始化后的NestLoopState结构
     */
    NestLoopState* ExecInitNestLoop(NestLoop* node, EState* estate, int eflags)
    {
        /* 检查不支持的标志 */
        Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
    
        NL1_printf("ExecInitNestLoop: %s\n", "初始化节点");
    
        /*
         * 创建状态结构
         */
        NestLoopState* nlstate = makeNode(NestLoopState);
        nlstate->js.ps.plan = (Plan*)node;
        nlstate->js.ps.state = estate;
        nlstate->nl_MaterialAll = node->materialAll;
        nlstate->js.ps.ExecProcNode = ExecNestLoop;
    
        /*
         * 杂项初始化
         *
         * 创建节点的表达式上下文
         */
        ExecAssignExprContext(estate, &nlstate->js.ps);
    
        /*
         * 初始化子表达式
         */
        if (estate->es_is_flt_frame) {
            nlstate->js.ps.qual = (List*)ExecInitQualByFlatten(node->join.plan.qual, (PlanState*)nlstate);
            nlstate->js.jointype = node->join.jointype;
            nlstate->js.joinqual = (List*)ExecInitQualByFlatten(node->join.joinqual, (PlanState*)nlstate);
            Assert(node->join.nulleqqual == NIL);
        } else {
            nlstate->js.ps.targetlist = (List*)ExecInitExprByRecursion((Expr*)node->join.plan.targetlist, (PlanState*)nlstate);
            nlstate->js.ps.qual = (List*)ExecInitExprByRecursion((Expr*)node->join.plan.qual, (PlanState*)nlstate);
            nlstate->js.jointype = node->join.jointype;
            nlstate->js.joinqual = (List*)ExecInitExprByRecursion((Expr*)node->join.joinqual, (PlanState*)nlstate);
        }
        Assert(node->join.nulleqqual == NIL);
    
        /*
         * 初始化子节点
         *
         * 如果从外部传递给内部关系的参数为空,则告知内部子节点可以进行廉价的重新扫描。
         * 如果有这样的参数,则内部子节点根本不需要REWIND支持,因为它将始终使用新的参数值重新扫描。
         */
        outerPlanState(nlstate) = ExecInitNode(outerPlan(node), estate, eflags);
        if (node->nestParams == NIL)
            eflags |= EXEC_FLAG_REWIND;
        else
            eflags &= ~EXEC_FLAG_REWIND;
        innerPlanState(nlstate) = ExecInitNode(innerPlan(node), estate, eflags);
    
        /*
         * 元组表初始化
         */
        ExecInitResultTupleSlot(estate, &nlstate->js.ps);
    
        nlstate->js.single_match = (node->join.inner_unique || node->join.jointype == JOIN_SEMI);
    
        switch (node->join.jointype) {
            case JOIN_INNER:
            case JOIN_SEMI:
                break;
            case JOIN_LEFT:
            case JOIN_ANTI:
            case JOIN_LEFT_ANTI_FULL:
                nlstate->nl_NullInnerTupleSlot = ExecInitNullTupleSlot(estate, ExecGetResultType(innerPlanState(nlstate)));
                break;
            default:
                ereport(ERROR,
                    (errcode(ERRCODE_UNRECOGNIZED_NODE_TYPE),
                        errmodule(MOD_EXECUTOR),
                        errmsg("在初始化nestLoop时出现无法识别的连接类型: %d", (int)node->join.jointype)));
        }
    
        /*
         * 初始化元组类型和投影信息
         * 在这种情况下,结果将仅包含虚拟数据。
         */
        ExecAssignResultTypeFromTL(&nlstate->js.ps, TableAmHeap);
        ExecAssignProjectionInfo(&nlstate->js.ps, NULL);
    
        /*
         * 最后,清除当前外部元组的数据。
         */
        nlstate->js.ps.ps_vec_TupFromTlist = false;
        nlstate->nl_NeedNewOuter = true;
        nlstate->nl_MatchedOuter = false;
    
        NL1_printf("ExecInitNestLoop: %s\n", "节点已初始化");
    
        return nlstate;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104

      此外,ExecInitNestLoop 函数中所涉及到的几种 JOIN 类型的含义如下:

    JOIN 类型含 义
    JOIN_INNER: 内连接返回两个表中匹配的行,不包括不匹配的行。
    JOIN_SEMI: 半连接返回左表中有匹配行的行,不包括右表的列。
    JOIN_LEFT: 左外连接返回左表中的所有行和右表中匹配的行。如果右表中没有匹配的行,返回 NULL 值。
    JOIN_ANTI: 反向半连接返回左表中没有匹配行的行
    JOIN_LEFT_ANTI_FULL: 左反向外连接返回左表中没有匹配行的行,同时包括右表中没有匹配的行

    ExecNestLoop 函数

      ExecNestLoop 函数的作用是执行嵌套循环连接操作,它通过遍历外部关系表和内部关系表的所有可能组合,根据连接条件将满足条件的元组组合成连接元组,并输出结果。在执行过程中,它会检查是否需要重新获取外部关系表的新元组,并对满足特定条件的连接进行优化。ExecNestLoop 函数如下所示:(路径:src\gausskernel\runtime\executor\nodeNestloop.cpp

    /*
     * ExecNestLoop函数用于执行NestLoop节点的主循环,从外部和内部关系中获取元组,并进行连接操作。
     * 该函数根据连接条件将外部和内部元组进行连接,并对连接后的元组进行限定条件的判断,最终返回符合条件的元组。
     * 参数:
     *   - state: NestLoop节点的执行状态
     * 返回值:
     *   - 符合条件的元组槽
     */
    static TupleTableSlot* ExecNestLoop(PlanState* state)
    {
        NestLoopState* node = castNode(NestLoopState, state); //将给定的PlanState类型的指针转换为NestLoopState类型的指针
        TupleTableSlot* outer_tuple_slot = NULL;  // 用于存储外部关系中的元组
        TupleTableSlot* inner_tuple_slot = NULL;  // 用于存储内部关系中的元组
        ListCell* lc = NULL;  // 用于遍历列表的指针
    
        /*
         * 从节点中获取信息
         */
        ENL1_printf("从节点中获取信息");
    
        /* 从NestLoopState中获取相关信息 */
        NestLoop* nl = (NestLoop*)node->js.ps.plan;  // 获取NestLoop节点的计划信息
        List* joinqual = node->js.joinqual;          // 获取连接限定条件列表
        List* otherqual = node->js.ps.qual;          // 获取其他限定条件列表
        PlanState* outer_plan = outerPlanState(node);  // 获取外部计划节点状态
        PlanState* inner_plan = innerPlanState(node);  // 获取内部计划节点状态
        ExprContext* econtext = node->js.ps.ps_ExprContext;  // 获取表达式上下文
    
        CHECK_FOR_INTERRUPTS();
        
        /*
         * 检查是否还在从先前连接的元组中投影出元组(因为投影表达式中存在返回集的函数)。
         * 如果是这样,则尝试投影另一个元组。
         */
        if (node->js.ps.ps_vec_TupFromTlist) {
            ExprDoneCond is_done;
    
            TupleTableSlot* result = ExecProject(node->js.ps.ps_ProjInfo, &is_done);
            if (is_done == ExprMultipleResult)
                return result;
            /* 完成这个源元组... */
            node->js.ps.ps_vec_TupFromTlist = false;
        }
    
        /*
         * 重置每个元组的内存上下文,以释放上一个元组周期中分配的任何表达式评估存储空间。
         * 注意,这不能发生,直到我们完成从连接元组中投影出元组的操作。
         */
        ResetExprContext(econtext);
    
        /*
         * 现在,一切都准备就绪,可以循环,直到返回符合条件的连接元组。
         */
        ENL1_printf("进入主循环");
    
        /* 如果需要在内部关系上执行物化操作,则执行 */
        if (node->nl_MaterialAll) {
            MaterialAll(inner_plan);          // 在内部计划上执行物化操作
            node->nl_MaterialAll = false;     // 将标志位设置为false,表示物化操作已完成
        }
    
        for (;;) {
            /*
             * 如果没有外部元组,获取下一个并重置内部扫描。
             */
            if (node->nl_NeedNewOuter) {
                ENL1_printf("获取新的外部元组");
                outer_tuple_slot = ExecProcNode(outer_plan);
                /*
                 * 如果没有更多的外部元组,则连接完成。
                 */
                if (TupIsNull(outer_tuple_slot)) {
                    ExecEarlyFree(inner_plan);
                    ExecEarlyFree(outer_plan);
    
                    EARLY_FREE_LOG(elog(LOG,
                        "Early Free: NestLoop is done "
                        "at node %d, memory used %d MB.",
                        (node->js.ps.plan)->plan_node_id,
                        getSessionMemoryUsageMB()));
    
                    ENL1_printf("没有外部元组,结束连接");
    
                    return NULL;
                }
    
                ENL1_printf("保存新的外部元组信息");
                econtext->ecxt_outertuple = outer_tuple_slot;
                node->nl_NeedNewOuter = false;
                node->nl_MatchedOuter = false;
    
                /*
                 * 获取必须传递给内部扫描的任何外部Var的值,并将其存储在相应的PARAM_EXEC插槽中。
                 */
                /* 遍历内部关系参数列表,设置参数执行数据 */
                foreach (lc, nl->nestParams) {
                    NestLoopParam* nlp = (NestLoopParam*)lfirst(lc);  // 获取参数信息
                    int paramno = nlp->paramno;  // 获取参数编号
                    ParamExecData* prm = NULL;   // 参数执行数据指针
    
                    prm = &(econtext->ecxt_param_exec_vals[paramno]);  // 获取参数执行数据的指针
                    /* 确保参数是OUTER_VAR类型 */
                    Assert(IsA(nlp->paramval, Var));
                    Assert(nlp->paramval->varno == OUTER_VAR);
                    Assert(nlp->paramval->varattno > 0);
                    Assert(outer_tuple_slot != NULL && outer_tuple_slot->tts_tupleDescriptor != NULL);
                    /* 获取外部元组中的属性值 */
                    prm->value = tableam_tslot_getattr(outer_tuple_slot, nlp->paramval->varattno, &(prm->isnull));
                    prm->valueType = outer_tuple_slot->tts_tupleDescriptor->tdtypeid;  // 设置参数值的类型
                    prm->isChanged = true;  // 将参数值标记为已更改
                    /* 将参数编号添加到内部计划的参数更改集合中 */
                    inner_plan->chgParam = bms_add_member(inner_plan->chgParam, paramno);
                }
    
                /*
                 * 现在重新扫描内部计划
                 */
                ENL1_printf("重新扫描内部计划");
                ExecReScan(inner_plan);
            }
    
            /*
             * 我们有一个外部元组,尝试获取下一个内部元组。
             */
            ENL1_printf("获取新的内部元组");
    
            /*
             * 如果内部计划是mergejoin,它不缓存数据,
             * 但会提前释放左右树的缓存内存。
             * 当重新扫描左树时,可能失败。
             */
            bool orig_value = inner_plan->state->es_skip_early_free;
            if (!IsA(inner_plan, MaterialState))
                inner_plan->state->es_skip_early_free = true;
    
            inner_tuple_slot = ExecProcNode(inner_plan);
    
            inner_plan->state->es_skip_early_free = orig_value;
            econtext->ecxt_innertuple = inner_tuple_slot;
    
            if (TupIsNull(inner_tuple_slot)) {
                ENL1_printf("没有内部元组,需要新的外部元组");
    
                node->nl_NeedNewOuter = true;
    
                if (!node->nl_MatchedOuter && (node->js.jointype == JOIN_LEFT || node->js.jointype == JOIN_ANTI ||
                                                  node->js.jointype == JOIN_LEFT_ANTI_FULL)) {
                    /*
                     * 我们正在执行外连接,并且此外部元组没有连接匹配项。
                     * 生成一个带有内部元组的虚假连接元组,并且如果通过非连接限定符,则返回它。
                     */
                    econtext->ecxt_innertuple = node->nl_NullInnerTupleSlot;
    
                    ENL1_printf("测试外连接元组的限定条件");
    
                    if (otherqual == NIL || ExecQual(otherqual, econtext, false)) {
                        /*
                         * 限定条件满足,因此我们投影并返回包含结果元组的槽,使用ExecProject函数。
                         */
                        ExprDoneCond is_done;
    
                        ENL1_printf("限定条件满足,投影元组");
    
                        TupleTableSlot* result = ExecProject(node->js.ps.ps_ProjInfo, &is_done);
    
                        if (is_done != ExprEndResult) {
                            node->js.ps.ps_vec_TupFromTlist = (is_done == ExprMultipleResult);
                            return result;
                        }
                    } else
                        InstrCountFiltered2(node, 1);
                }
    
                /*
                 * 否则,只需返回循环顶部获取新的外部元组。
                 */
                continue;
            }
    
            /*
             * 在这一点上,我们有一对新的内部和外部元组,因此我们测试内部和外部元组是否满足节点的限定条件。
             *
             * 只有连接条件决定了MatchedOuter的状态,但所有限定条件必须通过才能实际返回元组。
             */
            ENL1_printf("测试限定条件");
    
            if (ExecQual(joinqual, econtext, false)) {
                node->nl_MatchedOuter = true;
    
                /* 在反连接中,我们永远不会返回匹配的元组 */
                if (node->js.jointype == JOIN_ANTI || node->js.jointype == JOIN_LEFT_ANTI_FULL) {
                    node->nl_NeedNewOuter = true;
                    continue; /* 返回到循环顶部 */
                }
    
                if (node->js.single_match) {
                    node->nl_NeedNewOuter = true;
                }
    
                if (otherqual == NIL || ExecQual(otherqual, econtext, false)) {
                    /*
                     * 限定条件满足,因此我们投影并返回包含结果元组的槽,使用ExecProject函数。
                     */
                    ExprDoneCond is_done;
    
                    ENL1_printf("限定条件满足,投影元组");
    
                    TupleTableSlot* result = ExecProject(node->js.ps.ps_ProjInfo, &is_done);
    
                    if (is_done != ExprEndResult) {
                        node->js.ps.ps_vec_TupFromTlist = (is_done == ExprMultipleResult);
                        /*
                         * 通过信息约束优化计划。
                         */
                        if (((NestLoop*)(node->js.ps.plan))->join.optimizable) {
                            node->nl_NeedNewOuter = true;
                        }
    
                        return result;
                    }
                } else
                    InstrCountFiltered2(node, 1);
            } else
                InstrCountFiltered1(node, 1);
    
            /*
             * 元组不符合限定条件,因此释放每个元组的内存并重试。
             */
            ResetExprContext(econtext);
    
            ENL1_printf("限定条件不满足,继续循环");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233

      ExecNestLoop 函数的执行流程总结如下:

    1. 从输入参数 state 中获取 NestLoopState 结构体指针 node,以及与连接相关的一些信息,如连接条件外部计划内部计划等。
    2. 检查是否需要从前一个连接元组中投影出新的元组,如果需要,则执行投影操作。
    3. 重置每个元组内存上下文,以释放之前元组周期中分配的表达式评估存储空间。
    4. 如果需要在内部关系上执行物化操作,则执行物化操作。
    5. 进入主循环直到返回一个符合连接条件的连接元组或者结束连接操作
    6. 在主循环中,首先检查是否需要获取新的外部元组,如果需要,则获取新的外部元组,并在必要时重置内部扫描。如果没有更多的外部元组可用,则结束连接操作。
    7. 在获取新的外部元组后,获取任何必须传递给内部扫描的外部变量的值,并将其存储在适当的 PARAM_EXEC 槽中。然后,重新扫描内部计划
    8. 获取外部元组和内部元组后,测试它们是否满足连接条件和其他限定条件
    9. 如果连接元组满足条件,则进行投影操作,并返回结果元组;否则,继续主循环获取下一个内部元组
    10. 如果内部元组耗尽,则需要获取新的外部元组,然后继续主循环。

      以下是 Nest Loop 算子的伪代码:

    for each tuple outer_tuple in outer_relation:
        for each tuple inner_tuple in inner_relation:
            if join_condition(outer_tuple, inner_tuple) is true:
                combine_tuples(outer_tuple, inner_tuple) as result_tuple
                output result_tuple
    
    -- outer_relation 是外部关系表。
    -- inner_relation 是内部关系表。
    -- join_condition 是连接条件的函数,用于判断外部关系和内部关系的元组是否满足连接条件。
    -- combine_tuples 是将外部关系和内部关系的元组组合成一个连接元组的函数。
    -- output 是输出连接结果的操作。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    ExecEndNestLoop 函数

      ExecEndNestLoop 函数的作用是关闭嵌套循环连接节点的扫描并释放分配的存储空间。具体功能包括释放表达式上下文清空元组表以及关闭子计划节点ExecEndNestLoop 函数源码如下所示:(路径:src\gausskernel\runtime\executor\nodeNestloop.cpp

    /* ----------------------------------------------------------------
     *		ExecEndNestLoop
     *
     *		关闭扫描并释放分配的存储空间
     * ----------------------------------------------------------------
     */
    void ExecEndNestLoop(NestLoopState* node)
    {
        NL1_printf("ExecEndNestLoop: %s\n", "ending node processing"); // 输出节点处理结束信息
    
        /*
         * 释放表达式上下文
         */
        ExecFreeExprContext(&node->js.ps);
    
        /*
         * 清空元组表
         */
        (void)ExecClearTuple(node->js.ps.ps_ResultTupleSlot);
    
        /*
         * 关闭子计划
         */
        ExecEndNode(outerPlanState(node));
        ExecEndNode(innerPlanState(node));
    
        NL1_printf("ExecEndNestLoop: %s\n", "node processing ended"); // 输出节点处理结束信息
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    ExecReScanNestLoop 函数

      ExecReScanNestLoop 函数用于重新扫描 Nest Loop 运算符的状态,根据需要重新扫描外部和内部子计划节点,并设置相应的状态标志以准备下一次迭代的处理。如果外部子计划节点的参数未更改,则在第一个执行步骤时会自动重新扫描。在递归流模式下,需要重新扫描外部和内部计划节点。最后,函数还会设置状态标志,以指示下一个迭代周期需要获取新的外部元组,并确保内部计划节点不会在此处重新扫描。ExecReScanNestLoop 函数源码如下所示:(路径:src\gausskernel\runtime\executor\nodeNestloop.cpp

    /* ----------------------------------------------------------------
     *		ExecReScanNestLoop
     * ----------------------------------------------------------------
     */
    void ExecReScanNestLoop(NestLoopState* node)
    {
        // 获取外部计划节点和内部计划节点
        PlanState* outer_plan = outerPlanState(node);
        PlanState* inner_plan = innerPlanState(node);
        PlanState ps = node->js.ps;
    
        /*
         * 如果外部计划节点的参数已经改变,则外部计划节点会在第一个 ExecProcNode 函数调用时自动重新扫描
         */
        if (outer_plan->chgParam == NULL)
            ExecReScan(outer_plan);
    
        /*
         * 在递归流条件下,需要重新扫描外部计划节点和内部计划节点
         */
        if (IS_PGXC_DATANODE && EXEC_IN_RECURSIVE_MODE(ps.plan) && ((ps.state)->es_recursive_next_iteration)) {
            ExecReScan(inner_plan);
            node->nl_MaterialAll = ((NestLoop*)ps.plan)->materialAll;
        }
    
        /*
         * 内部计划节点在每个新的外部元组中都会重新扫描,不应该从此处重新扫描,
         * 否则在使用外部变量作为运行时键时,会出现内部索引扫描的问题
         */
        node->js.ps.ps_vec_TupFromTlist = false;
        node->nl_NeedNewOuter = true;
        node->nl_MatchedOuter = false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    总结

      Nest Loop 算子的优势在于:

    1. 灵活性: 可以处理各种连接类型,包括内连接左连接右连接全连接
    2. 适用性: 对于小型数据集或者没有合适索引的情况下Nest Loop 可能是最有效的连接策略。
    3. 适用于复杂条件: 可以处理连接条件复杂无法使用索引的情况,因为它可以逐一检查每一对可能的连接。

      然而,Nest Loop 算子也有一些限制:

    1. 性能受限:大型数据集连接条件较复杂时Nest Loop 的性能可能会受到影响,因为它需要执行多次内部循环扫描
    2. 内存占用较高: 需要维护内部和外部循环的状态,可能导致内存占用较高
    3. 不适用于大型数据集:数据集较大时Nest Loop 的性能通常不佳,因为它需要在每次外部循环迭代中扫描整个内部数据集。
  • 相关阅读:
    2023 的一些技术总结
    安装系统出现dracut-initqueue状态
    数据统计和分析怎么做?spss如何做好数据分析?
    Pig的搭建和配置
    遗失的访谈 —— 关于软件与硬件的人生
    基于Ubuntu20.04运行OP-TEE_3.17.0_QEMU_V8的环境搭建
    C语言 操作符
    CentOS7.9安装
    从零开始 Spring Cloud 15:多级缓存
    分布式概念与协议
  • 原文地址:https://blog.csdn.net/qq_43899283/article/details/136345137