暂无图片
暂无图片
3
暂无图片
暂无图片
暂无图片

内核探究 | Apache Cloudberry:如何利用物化视图进行查询改写

HashData 2024-11-26
456

Apache Cloudberry™ (Incubating) 是 Apache 软件基金会孵化项目,由 Greenplum 和 PostgreSQL 衍生而来,作为领先的开源 MPP 数据库,可用于建设企业级数据仓库,并适用于大规模分析和 AI/ML 工作负载。

GitHub:  https://github.com/apache/cloudberry

文章作者:张明礼,Apache Cloudberry PPMC 成员;整理:酷克数据
物化视图(Materialized View)是一种基于特定 SQL 查询预先计算并存储的结果集,它能够显著提升查询操作的效率。在执行查询时,系统能够智能地识别并替换为相应的物化视图,直接返回预先计算好的结果,或者在这些结果的基础上进行进一步的数据处理。这一技术在在线分析处理(OLAP)场景中表现得尤为出色,特别是在处理大型数据表的聚合查询时,能够大幅度缩减查询所需时间,实现性能的显著提升。

本文将深入探讨如何在 Apache Cloudberry™ (Incubating)(以下简称“Cloudberry”)中利用物化视图来实现 SQL 查询的改写,同时解析其背后的技术原理与实施细节。

01 利用物化视图进行查询改写

Cloudberry 依托 PostgreSQL 优化器的逻辑,支持利用物化视图来优化查询改写过程。我们来看一个具体示例:

假设我们有一个名为 T1 的表,它包含两列数据:

    create table t1(c1 int, c2 int); 
    create materialized view mv_t1 as
    select count(c1) as m_c1 from t1 where c2 > 1;

    对于原始的查询语句:

      select count(c1) from t1 where c2 > 1;

      我们可以将其改写为直接从物化视图 mv_t1 中选择所需的列 m_c1:

        select m_c1 from mv_t1;

        在 T1 表包含数亿行数据的场景下,直接进行扫描和计算的成本非常高,即使采用并行化技术也难以显著降低开销。然而,通过利用物化视图,我们可以在短短数秒内获取到查询结果。这种改写逻辑建立在查询等价性的基础之上,确保物化视图能够完全涵盖查询所需的所有行和列,从而保障查询结果的一致性。

        在 Cloudberry 中,查询的改写过程是基于成本优化的。系统会自动对比不同的物化视图和改写方案,从中选择出最优的执行路径,以确保查询的高效执行。

        物化视图改写的首要任务是确定候选视图。Cloudberry 提供两种类型的物化视图,每种类型都有其独特的特点和适用场景:

        • 增量物化视图:实时更新,自动化维护,非常适合对数据一致性有高要求且查询相对简单的场景。但请注意,其功能具有一定的局限性。

        • 普通物化视图:具有高度的兼容性,适用于查询复杂度高、实时性要求稍低的场景。目前,其改写功能正在持续优化中。

        同时,Cloudberry 还具备完善的普通物化视图有效性检查机制:

        • 智能判断视图有效性:如果自最近一次刷新后,没有增删改操作,则视为有效,直接提供高效查询。

        • 即时标记失效:一旦数据发生更新,立即标记为失效,以确保数据的准确性。

        • Delta处理复用视图:灵活应对数据修改,未来计划将此功能开源,进一步提升系统的灵活性和效率。

        Cloudberry 当前的物化视图改写功能主要聚焦于单表场景,能够满足大部分的使用需求。在实现过程中,连接树的检查相对简单,因为在单表查询中,只需要检查关系表的 OID(对象标识符)。对于某些复杂场景,单表可能是普通表,也可能是分区表,而分区表内部可能还包含多个子分区。Cloudberry 在设计时已经充分考虑了这些复杂性,以确保系统的稳定性和高效性。
        02 物化视图与查询条件匹配的实现逻辑

        物化视图的主要作用是加速查询响应,它通过存储预先计算好的查询结果来减少重复计算。然而,物化视图并非总能直接用于所有查询,其能否被利用取决于查询条件与物化视图条件的匹配程度。

        View matching: construct rows

        我们根据以下几个示例,来看查询条件与物化视图条件匹配的实现逻辑:

        示例一:

        我们先来看一个简单的场景,假设有一个基础表 T1,并基于它创建了一个物化视图 mvt1:

        View

          create incremental materialized view mvt1 as
          select c3 from t1 where c1 = 1 and c2 = 2;

          Query

             select c3 from t1 where c1 = 1 and c2 = 3;
            在这个例子中,查询的限制条件是 query_quals = {c1=1, c2=3};,而物化视图的限制条件是 mv_quals = {c1=1, c2=2};,我们可以通过下图来展示二者之间的数学关系。

            此时 mv_quals - query_quals = {c2 = 2},查询条件和物化视图条件的补集不为空,意味着物化视图的数据比查询需要的少,说明物化视图中有一部分数据与查询条件不完全匹配,因此不能直接利用物化视图来满足查询。

            示例二:

            物化视图改写同样不适用于范围配额,例如:

            View

              create incremental materialized view mvt2 as
              select c3 from t1 where c1 = 1 and c2 between 1 and 10;

              Query

                  select c3 from t1 where c1 = 1 and c2 = 5;

                在这个例子中,物化视图条件中 c2 between 1 and 10,而查询条件 c2=5。查询条件实际上是物化视图条件的子集,但当前 Cloudberry 对范围条件的改写支持有限,也无法直接利用该物化视图。

                示例三:

                View

                  create incremental materialized view mvt1 as
                  select c3 from t1 where c1 = 1;

                  Query

                    select c3 from t1 where c1 = 1 and c2 = 3;

                    在此示例中,物化视图的条件为 c1=1,而查询条件为 c1=1 and c2=3,二者有共同的交集 {c1=1},且补集为空。

                    虽然限制条件匹配,但因为物化视图只包含 c3 列,重写时无法直接添加 c2 = 3 条件。若强行将  c2 = 3  条件加入物化视图查询语句,虽然逻辑上满足 c1 = 1,但会导致错误。如果我们将其改写下变成:

                    View

                      create incremental materialized view mvt1 as
                      select c3, c1 from t1 where c1 = 1;

                      Query

                        select c3 from t1 where c1 = 1 and c2 = 3;

                        这里物化视图包含所需的所有列并满足 c1 = 1 条件。然而,由于物化视图已限定 c1 = 1,在查询中再次加入 c1 = 1 并无实际意义。

                        使用以下集合关系来分析重写条件:

                        • 补集:mv_quals - query_quals = {},表明物化视图的条件均包含于查询条件中。

                        • 交集:mv_quals ∩ query_quals = {c1 = 1},由于交集条件在物化视图中已满足,这里我们一起探讨下是否要保留交集条件。

                          • 简化查询避免冗余计算。

                          • 然而去除该条件会影响 PostgreSQL 等价类中的常数匹配,可能丧失 c1 = 1 的等价类信息。

                          • 物化视图中所有行均满足 c1 = 1,即选择率为 100%。因 PostgreSQL 的查询代价估算会基于选择率,保留该条件对优化无帮助。

                          • 在扫描物化视图时再执行 c1 = 1 过滤无实际作用。

                          • 若保留交集 {c1 = 1}:

                          • 若去除交集 {c1 = 1}:

                        • 补集处理(post_quals):

                          • 定义 post_quals = query_quals - mv_quals = {c2 = 3},即查询条件中物化视图无法直接满足的部分。

                          • 此处可以重写为以下查询:SELECT c3 FROM mvt1 WHERE c2 = 3;

                        这样,通过去除冗余条件并保留必要的 c2 = 3,该重写是合法且优化后的查询。我们来总结下,对于单表中的两个限制条件的集合能否匹配有以下要求:

                        • mv_quals - query_quals 必须为空,即物化视图的条件集合应完全包含于查询条件集合中;

                        • mv_quals ∩ query_quals 中的公共条件可以忽略,不影响结果;

                        • post_quals(即 query_quals - mv_quals)中的所有表达式应可由物化视图查询的目标列计算得出。

                        View matching: construct columns

                        接下来讨论如何处理查询列的问题,我们先看一个简单例子,假设有一个表 T1,有三个列 C1、C2 和 C3:

                        示例一:

                        View

                          create incremental materialized view mv as
                          select c2 as mc1 from t1 where c1 = 2;

                          Query

                            select c2 from t1 where c1 = 2;

                            显然,这个查询可以被物化视图替代。那么,这个替代过程是如何完成的呢?

                            首先,查询 select c2 from t1,我们只看投影部分。因为 T1 表中有 C1、C2、C3 三个列,C2 对应的是表中的第二个属性。在 PostgreSQL 的查询树中,每个列都有一个属性号(attribute number)。在执行这个查询时,C2 对应的是属性号是 2。

                            也就是 Query's TargetList: {Var[attno=2, 'c2', Table: 't1']}。

                            在物化视图中选择了 C2 作为 MC1 列,因此它的属性号是 1,尽管它的名字是 MC1。

                            • view query's TargetList: {Var[attno=2, 'c2', Table: 't1']}

                            • mv table's column: {[Var[attno=1, 'mc1', Table: ‘mv']]}

                            在这种情况下,C2 在物化视图和 T1 表之间有等价关系:物化视图中的 MC1 对应于 T1 表中的 C2。通过这个等价关系,我们可以把查询改写成:

                            View

                              create incremental materialized view mv as
                              select c2 as mc1 from t1 where c1 = 2;

                              Query

                                select c2(attno:2) as res  from t1 where c1 = 2;

                                改写为:

                                  select mc1(attno:1) as res from mv;

                                  示例二:

                                  我们再来看一个稍微复杂的例子:

                                  View

                                    create incremental materialized view mv as
                                    select c1 as mc1, c2 as mc2 from mv where c1 = 3;

                                    Query

                                      select abs(c1) + 1 + c2 as res from t1 where c1 = 3;

                                      假设查询条件相同,仅供展示的物化视图选取了两列:mc1 和 mc2,同时执行一个包含多个列和运算的表达式 {ABS(c1) + 1 + c2}。尽管在查询中找不到完全匹配的列,但表达式可以从物化视图的目标列表派生出来。

                                      这一过程改写复杂,涉及到 PostgreSQL 中的查询树遍历。在 PostgreSQL 中,查询语句会生成一个查询树,表示表达式的结构。

                                      abs(C1) + 1 + C2 Query Tree

                                      首先,PostgreSQL 从表达式树左侧开始深度遍历,第一个节点是 abs 函数,其参数是 C1。在查询树中,C1 是 abs 函数的子节点,而 abs 的父节点是加法表达式。加号的左侧是 abs(C1),右侧是常量 1,并与 C2 组成更大的表达式 abs(C1) + 1 + C2。

                                      (C1) + 1 + C2 Traversal Process

                                      在改写时,我们同样采用深度优先遍历。遍历到 C1 时,发现它与物化视图中的 MC1 等价,便将 C1 改写为 MC1,使表达式从 abs(C1) + 1 变为 abs(MC1) + 1。

                                      接着,遍历到 abs 函数,因其为通用函数,无需改写,直接跳过。继续遍历右侧,遇到常量 1,保持不变。最后,遍历到 C2,发现它可与 MC2 等价,改写为 MC2。

                                      最终,表达式重写为 abs(MC1) + 1 + MC2。虽然这个表达式的改写相对简单,但它展示了查询树遍历和表达式替换的基本逻辑。

                                      示例三:

                                      接下来,我们讨论一个查询中涉及到多个列和复杂运算,例如:

                                      View

                                        create incremental materialized view mv as
                                        select c1 as mc1,
                                        c2 as mc2,
                                        abs(c2) as mc3,
                                        abs(abs(c2) - c1 - 1) as mc4
                                        from t1 where c1 > 10 and c1 < 15;

                                        Query

                                          select c1,
                                          sqrt(abs(abs(c2) - c1 - 1) + abs(c2)) + 1
                                          from t1 where c1 > 10 and c1 < 15;

                                          这里sqrt(abs(abs(c2) - c1 - 1) + abs(c2)) + 1表达式更为复杂,嵌套了 abs、sqrt 等多个函数调用,并且涉及多个列。

                                          sqrt(abs(abs(c2) - c1 - 1) + abs(c2)) + 1 Query Tree

                                          复杂表达式会生成更深的查询树,每个函数或运算符都是一个节点。我们依旧采用深度优先遍历,依靠列的等价性和函数的保留性,对每个节点进行替换或保留,直到将表达式逐步改写为物化视图中对应的列和运算。

                                          因为物化视图中已经包含了所有需要的列,我们只需等价替换即可,直接将 c1、c2 这两个列替换为物化视图中的 MC1 和 MC2,就能得到sqrt(abs(abs(MC2) - MC1 - 1) + abs(MC2)) + 1。这种方法很直观,但是否存在更优的方式呢?

                                          sqrt(abs(abs(c2) - c1 - 1) + abs(c2)) + 1  Traversal Process

                                          在这个示例中,第三列定义了 abs(c2),并命名为 MC3。查询表达式树中包含两个 abs(c2),因此我们可以尝试使用 MC3 来代替它们,从而简化表达式。

                                          接下来,我们可以进一步优化。例如,第四列 MC4 涉及 C1 和 C2,相当于一个包含 abs(abs(c2) - c1 - 1) 的表达式。如果我们将表达式替换为 MC4,那么查询可以简化为 SQRT(MC4 + MC3) + 1。这种简化方式不仅降低了计算代价,也是表达式重写中的优化策略。

                                          在优化时,选择最优表达式进行替换至关重要。例如,如果不充分考虑整体表达式而直接将 C1 替换为 MC1,最终可能得到 abs(MC2) - MC1 - 1。这种替换会导致后续步骤中缺少匹配的表达式,无法进一步优化,甚至会失去物化视图的作用。因此,确保选择最优表达式对正确的重写至关重要。

                                          03 优化物化视图改写策略

                                          总结一下物化视图改写过程,这里我们使用了一个贪心算法来优化表达式:

                                          1. 复杂度排序:首先对投影列表中的表达式按复杂度进行排序,将复杂度较高的表达式放在前面。这样在替换过程中可以优先使用复杂的表达式,不需要严格的复杂度衡量标准,只要相对复杂的排在前面即可。

                                          2. 递归匹配:按排序顺序递归匹配查询中的表达式。只需确保如果表达式 A 是表达式 B 的一部分,则 A 排在 B 之后。跳过无变量的表达式(如常量或简化为常量的函数)。

                                            原始列顺序:mc1, mc2, mc3, mc4

                                            排序后顺序:mc4, mc3, mc1, mc2

                                          3. 递归实现:我们使用了一种简单的遍历方法,将表达式树的层级或遍历深度作为复杂度依据。这种方法高效,能够快速确定复杂度较高的表达式,使替换过程更顺畅。

                                          通过这种贪心算法和逐步替换的策略,我们能够有效地将复杂表达式简化,并最大限度地利用物化视图的性能优势。在处理多列、多层嵌套表达式时,选择最佳的替换方案是至关重要的,不仅能降低计算成本,还能确保查询改写的正确性和效率。

                                          这里有几个要求需要明确:

                                          1. 元素的无限使用:在改写表达式时,表达式中所有元素(例如列、函数等)可以无限次地使用。这是合理的,因为这些元素是可重复使用的,类似于在数学中我们可以多次引用同一个变量。

                                          2. 特定顺序填充:为了优化表达式,我们需要按照一定的复杂度顺序来进行填充。这意味着我们要首先对表达式的复杂度进行排序,优先匹配最复杂的部分,逐步填充整个表达式树。如果某个复杂的部分无法匹配,我们就换下一个元素进行替换,直到所有空白部分都填充完毕。

                                          3. 处理常量和其他元素:填充过程中,可能会有一些空白部分。这些空隙往往是常量或者简单的函数,比如加一等。这些常量可以很容易地被填充进表达式。在填充完所有投影列之后,我们再检查这些空隙是否可以通过常量、函数或其他简单的元素来替换。如果这些空隙中没有涉及任何列的计算,我们就可以确认这是一种有效的改写。

                                          4. 处理行条件 (Postquals):除了列的改写之外,我们还需要处理一些行条件,比如 WHERE 子句中的过滤条件。这些行条件实际上可以用类似的逻辑处理。我们只需要检查这些条件是否能够通过物化视图中的列和表达式来改写即可。行条件和列的改写流程是一致的。

                                          处理其他表达式节点

                                          实际上,在完成列的变换后,行的变换(即 quals 的变换)是类似的。我们只需检查 post_quals 中的条件即可。

                                          post_quals 的所有表达式可以从物化视图查询的目标列表中计算得出。接下来,我们演示下使用物化视图查询的目标列表重写 post_quals 中的每个表达式。

                                          在物化视图上计算聚合

                                          计算物化视图上的聚合与普通查询类似,但要求所有查询的目标列表(包括无用目标项)必须在视图的物理列中存在。无用目标项可以用于 ORDER BY、GROUP BY 等操作。

                                          View

                                            create table t1 (c1 int, c2 int, c3 int);
                                            create incremental materialized view mv1 as
                                            select c2 as mc1 from t1 where c1 = 2;
                                            create incremental materialized view mv2 as
                                            select c1, c2 as mc1 from t1 where c1 = 2;

                                            Query

                                              select c2 from t1 where c1 = 2 order by c1;
                                              mv1 不能用于此查询,因为 ORDER BY c1 中的 c1 不存在于 mv1 的列中。而 mv2 则可以改写为:
                                                select c2 from mv2 order by c1;

                                                虽然上述示例中没有聚合运算,但实际上可以在没有聚合运算的视图上进行聚合计算。这时可能会有额外的要求,比如在表达式中可能涉及一些“无用列”,它们可能包含在 ORDER BY 或 GROUP BY 的语义中,即使它们不在最终的数据中。

                                                例如,如果表中有三列,物化视图为 C2 和 SMC1,并加上条件限制。第二个物化视图选择了 C1 和 C2 两个列。若查询如下:

                                                  SELECT C2 FROM t1 WHERE c1 = 2 ORDER BY C1;

                                                  由于 ORDER BY C1 的额外条件,虽然第一个物化视图满足限制条件,但由于其投影列只有 C2,无法进行排序。因此,第二个物化视图可被用于改写,并且可以直接从物化视图中选择 C1 进行排序。

                                                  基于代价的优化

                                                  在有多个有效候选视图的情况下,选择最佳的重写方式,由规划器决定。

                                                  View

                                                    create table t1 (c1 int, c2 int, c3 int);
                                                    create incremental materialized view mv1 as
                                                    select c2, c3 as mc1 from t1 where c1 = 2;
                                                    create incremental materialized view mv2 as
                                                    select c2, c3 as mc1 from t1 where c1 = 2 and c2 > 3;

                                                    Query

                                                      select c2, c3 from t1 where c1 = 2 and c2 > 3;

                                                      通过 mv1 重写为:

                                                        select c2, c3 from mv1 where c2 > 3;

                                                        通过 mv2 重写为:

                                                          select c2, c3 from mv2; -- 更优,原则上行数更少。
                                                          在这种情况下,mv2 完全匹配查询,而从 mv1 重写需要加上额外的过滤条件。因此,第二个重写方案显然更优,因为不需要额外的过滤条件。

                                                          04 总结

                                                          在 Apache Cloudberry™ (Incubating) 中,我们已经在利用物化视图优化查询方面取得了显著进展,支持了包括复杂计算、聚合和排序在内的多种操作,但仍存在多重功能限制,如复杂的 Join、Window Function 和 Sublink 等尚未实现。未来,我们将继续扩展物化视图优化查询的能力,引入更多高级功能,如聚合操作的开源支持、连接操作的优化、等价类的应用,以及物化视图的快速过滤和更灵活的数据选择能力,更好满足用户朋友们的分析需求。

                                                          推荐阅读


                                                          👇🏻️扫码加入 Apache Cloudberry 交流群👇🏻️

                                                          文章转载自HashData,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                                          评论