openGauss学习——优化 UNION ALL 语句

引言

本篇博客中介绍SQL查询规划的预处理过程中,对 UNION ALL 语句的优化过程。

文件路径

/src/gausskernel/optimizer/prep/ prepnonjointree.cpp

优化 UNION ALL

在查询规划的预处理过程中,subquery_planner() 会对顶层的 UNION ALL 语句进行处理,目的是将 UNION ALL 这种集合操作的形式转换为AppendRelInfo的形式,实现函数为flatten_simple_union_all() 。

说地具体些,如果一个查询包含的 setop(setOperations) tree 完全由简单的 UNION ALL操作组成,那么函数会将集合树 setop tree 压平,变成一个扁平的追加关系,这样就可以把集合结构简化,不需要在树形结构中处理集合了。

例如查询语句:

SELECT * from ( SELECT * from A UNION ALL ( SELECT * from B UNION ALL SELECT * from ( SELECT * from C UNION ALL SELECT * from D )) ) ;

将其 jointree(setop tree)压平如下图所示:

另外需要注意的是,函数 flatten_simple_union_all() 只针对查询顶层的 setop tree 结点进行压平操作,因为对于FROM语句中的子查询,subquery_planner() 在调用此函数之前已经调用函数 pull_up_subquery() ,并通过 pull_up_simple_UNION_all() 函数完成了UNION语句的拉平。因此函数 flatten_simple_union_all() 不针对子查询中的 UNION 结构。

UNION 操作符

在这里简单介绍下SQL查询语句中的 UNION 操作符。UNION 用于合并两个或多个 SELECT 语句的结果集,需要注意的是,UNION 内部的 SELECT 语句必须拥有相同数量的列,列也必须拥有相似的数据类型。同时,每条 SELECT 语句中的列的顺序必须相同。

SQL UNION 语法

SELECT column_name(s) FROM table_name1
UNION
SELECT column_name(s) FROM table_name2

SQL UNION ALL 语法

SELECT column_name(s) FROM table_name1
UNION ALL
SELECT column_name(s) FROM table_name2

需要注意的是,默认情况下,UNION操作符会选取两个子集中具有不相同字段的表项,相当于简单的并集操作;因此如果允许结果中出现完全重复的值,应使用 UNION ALL语句。

另外,UNION 结果集中的列名总是等于 UNION 中第一个 SELECT 语句中的列名。下面举例说明 UNION 和 UNION ALL 之间的差别。例如如下两个表A,B,将他们做 UNION 和 UNION ALL 操作;

得到的结果为:

函数 flatten_simple_union_all()

void flatten_simple_union_all(PlannerInfo* root)

接下来看函数实现。

首先,如果 setop tree 的顶层结点的结构类型不为SetOperationStmt或 setop tree 包含递归的UNION结构,不对其做优化。关于结构体 SetOperationStmt 我们在 SQL optimizer解读(4)—— 替换空FROM语句 已经介绍过,它是集合树的顶层结点,它包含了查询中使用的集合操作类型以及对应的子查询。

    /* Shouldn't be called unless query has setops */
    topop = (SetOperationStmt*)parse->setOperations;
    AssertEreport(topop != NULL && IsA(topop, SetOperationStmt),
        MOD_OPT_REWRITE,
        "SetOperationStmt shouldn't be NULL in flatten_simple_union_all");
    /* Can't optimize away a recursive UNION */
    if (root->hasRecursion)
        return;

接下来遍历集合树,确定其是否只包含简单的UNION ALL语句,如果不是,不对其做压平操作。

    if (!is_simple_union_all_recurse((Node*)topop, parse, topop->colTypes) || ng_is_multiple_nodegroup_scenario())
        return;

接下来找到 setop tree 最左侧的叶子结点(leftmostjtnode的类型声明为Node *,但其实际上指向一个RangeTblRef类型的RTE结点),这个结点对应的RTE将在压平之后充当“根结点”的作用,其他叶子结点引用的RTE信息都会附在最左侧叶子结点之后。

    /*
     * Locate the leftmost leaf query in the setops tree.  The upper query's
     * Vars all refer to this RTE (see transformSetOperationStmt).
     */
    leftmostjtnode = topop->larg;
    while (leftmostjtnode && IsA(leftmostjtnode, SetOperationStmt))
        leftmostjtnode = ((SetOperationStmt*)leftmostjtnode)->larg;

找到最左侧的叶子结点之后,要进行两层的错误判断,首先leftmostjtnode应为RangeTblRef类型,其次其引用的RTE的类型应为RTE_SUBQUERY,否则函数中止,抛出err信息。

    if (leftmostjtnode && IsA(leftmostjtnode, RangeTblRef)) {
        leftmostRTI = ((RangeTblRef*)leftmostjtnode)->rtindex;
        leftmostRTE = rt_fetch(leftmostRTI, parse->rtable);
        AssertEreport(leftmostRTE->rtekind == RTE_SUBQUERY,
            MOD_OPT_REWRITE,
            "leftmostRTE should be SUBQUERY in flatten_simple_union_all");
    } else {
        ereport(ERROR,
            (errmodule(MOD_OPT), errcode(ERRCODE_OPTIMIZER_INCONSISTENT_STATE), (errmsg("RangeTblRef not found."))));
    }

把最左侧叶子结点引用的RTE加入range table,并让集合树setop tree引用这个新加入的RTE结点。

    childRTE = (RangeTblEntry*)copyObject(leftmostRTE);
    parse->rtable = lappend(parse->rtable, childRTE);
    childRTI = list_length(parse->rtable);
    /* Modify the setops tree to reference the child copy */
    ((RangeTblRef*)leftmostjtnode)->rtindex = childRTI;

接下来把这个RTE结点的inh字段置为true,表示其为一个追加关系中的起始结点。

    /* Modify the formerly-leftmost RTE to mark it as an appendrel parent */
    leftmostRTE->inh = true;

为当前层的查询结点的jointree字段构造fromlist结构。实际上就是让当前层查询结点的fromlist信息指向这个拉平后的追加关系表集。

    /*
     * Form a RangeTblRef for the appendrel, and insert it into FROM.  The top
     * Query of a setops tree should have had an empty FromClause initially.
     */
    rtr = makeNode(RangeTblRef);
    rtr->rtindex = leftmostRTI;
    AssertEreport(parse->jointree->fromlist == NIL,
        MOD_OPT_REWRITE,
        "The top Query of a setops tree should have had an empty FromClause in flatten_simple_union_all");
parse->jointree->fromlist = list_make1(rtr);

接下来将setOperations字段置空,表示集合树已经被拉平,查询里已经不包括原本的树形集合结构了,但此时实际的拉平工作还未完成。

    /*
     * Now pretend the query has no setops.  We must do this before trying to
     * do subquery pullup, because of Assert in pull_up_simple_subquery.
     */
    parse->setOperations = NULL;

最后调用函数 pull_up_union_leaf_queries() ,这个函数完成两个工作,一个是将各个叶子节点的RTE信息附加到fromlist中,构造附加关系;由于集合树的叶子结点引用的RTE类型应为RTE_SUBQUERY,因此另一个工作是调用pull_up_subqueries() 函数对各个叶子结点的子查询进行优化。

    /*
     * Build AppendRelInfo information, and apply pull_up_subqueries to the
     * leaf queries of the UNION ALL.  (We must do that now because they
     * weren't previously referenced by the jointree, and so were missed by
     * the main invocation of pull_up_subqueries.)
     */
    pull_up_union_leaf_queries((Node*)topop, root, leftmostRTI, parse, 0);

函数 pull_up_union_leaf_queries()

接下来看函数 pull_up_union_leaf_queries(),它会为集合树中的每个叶子结点构造一个AppendRelInfo 结构,并且对叶子结点上的子查询应用 pull_up_subqueries() 函数进行优化。

static void pull_up_union_leaf_queries(
Node* setOp, PlannerInfo* root, int parentRTindex, Query* setOpQuery, int childRToffset)

对于 RangeTblRef 的结点,这类结点一定是 setop tree 上的叶子结点,为其构造相应的AppendRelInfo 结构,并添加到集合树的引用列表中。如前文所说,在上述操作结束后,调用 pull_up_subqueries() 函数对叶子结点RTE上的子查询进行优化。

   if (IsA(setOp, RangeTblRef)) {
        RangeTblRef* rtr = (RangeTblRef*)setOp;
        int childRTindex;
        AppendRelInfo* appinfo = NULL;
        childRTindex = childRToffset + rtr->rtindex;
        /*
         * Build a suitable AppendRelInfo, and attach to parent's list.
         */
        appinfo = makeNode(AppendRelInfo);
        appinfo->parent_relid = parentRTindex;
        appinfo->child_relid = childRTindex;
        appinfo->parent_reltype = InvalidOid;
        appinfo->child_reltype = InvalidOid;
        make_setop_translation_list(setOpQuery, childRTindex, &appinfo->translated_vars);
        appinfo->parent_reloid = InvalidOid;
        root->append_rel_list = lappend(root->append_rel_list, appinfo);
        /*
         * Recursively apply pull_up_subqueries to the new child RTE.  (We
         * must build the AppendRelInfo first, because this will modify it.)
         * Note that we can pass NULL for containing-join info even if we're
         * actually under an outer join, because the child's expressions
         * aren't going to propagate up above the join.
         */
        rtr = makeNode(RangeTblRef);
        rtr->rtindex = childRTindex;
        (void)pull_up_subqueries_recurse(root, (Node*)rtr, NULL, NULL, appinfo);
}

对于 SetOperationStmt 类型的结点,其一定为 setop tree 中的内部结点,对其左右的子结点进行递归处理即可。在 setop tree 中,所有的结点或为SetOperationStmt类型(集合操作的顶层结点),或为RangeTblRef(RTE引用类型结点)。

    else if (IsA(setOp, SetOperationStmt)) {
        SetOperationStmt* op = (SetOperationStmt*)setOp;
        /* Recurse to reach leaf queries */
        pull_up_union_leaf_queries(op->larg, root, parentRTindex, setOpQuery, childRToffset);
        pull_up_union_leaf_queries(op->rarg, root, parentRTindex, setOpQuery, childRToffset);
    } else {
        ereport(ERROR,
            (errmodule(MOD_OPT),
                errcode(ERRCODE_UNRECOGNIZED_NODE_TYPE),
                errmsg("unrecognized node type: %d", (int)nodeTag(setOp))));
    }

总结

在本篇博客中,我对查询规划的预处理过程中对UNION ALL语句的优化过程进行了简单介绍。谢谢阅读。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
优化union all语句可以采取以下几种方法: 1. 使用UNION替代UNION ALL:如果不需要重复的行,可以使用UNION操作符代替UNION ALL。UNION操作符会自动去重,可以减少查询结果集的大小。 2. 使用索引:为查询涉及的列创建索引,可以加快查询速度。在上述示例中,可以为order1和order2表的state和create_time列创建索引。 3. 使用子查询:将UNION ALL操作拆分为多个子查询,每个子查询只涉及一个表。这样可以减少查询的数据量,提高查询效率。 4. 使用临时表:将UNION ALL操作的结果存储在临时表中,然后再进行筛选和排序操作。这样可以避免重复的查询和计算,提高查询效率。 5. 使用分区表:如果数据量很大,可以考虑将表进行分区,按照某个列的值进行分区。这样可以减少查询的数据量,提高查询效率。 综合以上几种方法,可以根据具体情况选择适合的优化方式来提高UNION ALL语句的性能。 #### 引用[.reference_title] - *1* *3* [记一次mysql的union all的简单优化](https://blog.csdn.net/weixin_34254823/article/details/92509096)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [SQL union ALL用法](https://blog.csdn.net/RAVEEE/article/details/126824653)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值