深入浅出GPORCA 优化器Transform流程

荣光因缘来
发布于 2022-8-3 17:46
浏览
0收藏

 

优化器是数据库的关键组件,GPORCA是Greenplum中的强大的模块化查询优化器,帮助用户对SQL进行优化,生成高效的查询计划,提高查询效率。GPORCA优化器架构是基于Cascades 模型,本文将对GPORCA优化器的Transform流程进行详细介绍。

优化器简介

SQL是一种描述性语言。对于一个复制的SQL语句,可能生成几十上百个等价的执行计划。实际上,选择最优执行计划的问题,已经被证明是一个NP-HARD问题。因此,人为的把SQL推导成执行计划,并从众多的执行计划中获取一个最优的,几乎是不可能的。

 


优化器作为数据库的关键组件不可或缺。它接收语法树,推导出等价执行计划,并选出最优的执行计划,交给执行器执行。

 


优化器一般使用动态规划实现,动态规划可以提供的特性:

 


无后效性:即子问题的解一旦确定,就不再变,不受这之后、包含它的更大的问题的求解决策影响。
重叠子问题:求解时,可能会遇到重复的子问题,记录子问题的解可以避免重复计算。

 

Cascades 优化器

 

我们知道,优化过程就是递归处理整个语法树的过程。遍历树可以从根节点开始,自上而下地访问。也可以从叶子节点自下而上的访问。Cascades作为新一代的优化器,使用的是自上而下的策略进行优化的。

 

 

Cascades 的关键组件


下面是Cascades的关键组件构成:

  • Memo:为了执行优化,需要提供搜索空间
  • Group:每个 Group 保存的是逻辑等价的 Group Expression Group
  • Expression:作为表达式在Cascades中的管理器,真正的优化单元。

如下图所示,是一个被初始化好后的Memo。

深入浅出GPORCA 优化器Transform流程-鸿蒙开发者社区

Group0中的Selection,Join是等价的Group Expression,作为Group0的子Group Expression


每个Group Expression又引用了其他的Group,作为这个表达式的子Group。


把上图的Memo的Group和Group Expression中的父子关系串联起来,就可得到图2的两个等价的表达式树。

深入浅出GPORCA 优化器Transform流程-鸿蒙开发者社区

Cascades的优化流程

  • Exploration:主要负责推导等价的逻辑表达式。
  • Implementation:主要负责把逻辑表达式转化为物理表达式
  • Optimization:主要负责搜索最优的执行计划。

Exploration和Implementation都是把一个表达式,转换为另一个等价的表达式,统称为Transformation;转换是基于规则进行的,称为基于规则的优化(Role Based Optimization,RBO)


Optimization主要是基于代价,比较不同的等价物理表达式树的代价,把总代价为最小的执行计划,作为最优解,输出给执行器执行;优化是基于代价的,称为基于代价的优化(Cost Based Optimization,CBO)

 

Cascades扩展机制

 

  • Cascasdes的设计中,设计了Property Enforcer(排序,Hash重分布)。
  • 我们根据Enforcer Property信息,决定是否添加Enforcer节点。
  • 在Optimization阶段添加Enforcer节点。

下图中的黑色节点,就是Enforcer的节点:

深入浅出GPORCA 优化器Transform流程-鸿蒙开发者社区

GPORCA Transformation 的实现

 

这篇文章,主要介绍基于规则的优化,我们称为Transformation。

 

初始化Memo

为了简单,我们这里的例子选取了简单的语句
gpadmin=# \d test
                Table "public.test"
 Column |  Type   | Collation | Nullable | Default 
--------+---------+-----------+----------+---------
 a      | integer |           |          | 
Distributed by: (a)

例子语句:
select a from test;

优化前,需要把语法树,放入Memo中,放入的规则是这样的:

 

  • 为语法树的每个表达式,创建一个Group,为表达式中的操作符,创建一个Group Expression,放入这个Group中。
  • 原来表达式的父子关系,转变为父的Group Expression,和子表达式所在Group的引用关系。
  • 把表达式中的派生属性,放入Group中。
Greenplum的Log信息

输入的表达式树:
Algebrized query: 
+--CLogicalGet ""test"" (""test""), Columns: [""a"" (0), ......]---顺序扫描的逻辑操作符

Initinal Memo:
ROOT ----语法树的根表达式所在的组
Group 0 (#GExprs: 1):
  0: CLogicalGet ""test"" (""test""), Columns: [""a"" (0), ......]---顺序扫描的逻辑操作符
  • 表达式树中的CLogicalGet, 实际上是作为CExpression对象的操作符存在的。
  • Group中的CLogicalGet, 实际上是作为CGroupExpression对象的操作符存在的。

Transformation


初始化完Memo(搜索空间)后,就会进入Transformation阶段。整个树进行Transform是一个后序遍历的过程。


GPORCA通过规则对表达式做转换,规则分为两类:

  • Exploration,是对逻辑表达式做等价变换的。
  • Implementation,是把逻辑操作符转换为物理操作符。

 

例子语句的完整Transformation流程如下图所示:
深入浅出GPORCA 优化器Transform流程-鸿蒙开发者社区

上图中可以看到,在Transformation阶段,LogicalGet通过规则转换为PhysicalTableScan

下面来看Transformation的Log信息:

Xform: CXformGet2TableScan---->RuleGet2TableScan
Input:
+--CLogicalGet ""test"" (""test""), Columns: [""a"" (0),  ......(系统列)]
Output:
+--CPhysicalTableScan ""test"" (""test"")   cost:-0.500000

这就是例子语句的Transformation的Log信息。CLogicalGet通过规则CXformGet2TableScan转换为CPhysicalTableScan。

这里需要说明,因为例子语句,对应的是顺序扫描,在Exploration阶段,不需要做等价变换,所以直接在Implementation阶段,通过CXformGet2TableScan做了转换。

 

规则管理

 

一个表达式,可能对应多个规则。Cascades的设计中,使用集合来管理规则。工程实现中,GPORCA使用位图来管理规则。

规则的集合分两类:

  • Exploration Set:
    用于Exploration的规则。
  • Implementation Set:
    用于Implementation的规则。

每个操作符,都存有自己需要的规则ID构成的规则的子集,这里面包括了逻辑变换和实现变换的规则。

  • Exploration阶段,拿操作符的规则集和逻辑变换集合取与,得到操作符这个阶段需要的规则

Implementation阶段,拿操作符的规则集和实现变换集合取与,得到操作符这个阶段需要的规则集

深入浅出GPORCA 优化器Transform流程-鸿蒙开发者社区

GPORCA通过规则工程管理规则集,包括Exploration和Implementation。上图中,0和1有8列,表示规则集。为了简单,我们假设CLogicalGet需要的规则ID是4。

 

  • LogicalGet所在的行,左边的规则集记录了LogicalGet需要的规则。可以看到第四位被设置为1,可以知道LogicalGet需要ID为4的规则进行转换。
  • Exploration所在的行的规则集,表示所有的规则的集合。5,6,7,8位被设置为1,所以Exploration的规则集是ID为{5,6,7,8}的规则集。
  • 同样的,Implementation的规则集为{1,2,3,4}

 

规则匹配:

  • Exploration优化阶段,LogicalGet和Exploration的规则集取与的结果为空集,所以直接跳过。
  • Implementation阶段,取与的结果是4,是CXformGet2TableScan的ID,所以应用这个规则

 

绑定

实际上在应用规则前,还涉及到规则和表达式的匹配逻辑,称为绑定流程。

 

通过前面的Memo的介绍可以看到,Group和GroupExpression,形成了一个森林。Group内部,都是等价的表达式,所以理论上说,不同Group中的GroupExpression可以随意组合,形成一个新的操作符树。但是不是所有的树,都有意义,所以在组装操作符树的时候,需要控制信息做指导,以生产新的有意义的操作符树。我们把这个过程称为规则的绑定。

我们把控制规则绑定的对象称为模式,抽象为接口类:CPattern。CPattern被几个子类继承,代表了不同的模式。

 

1.模式的分类


GPORCA设计:

 

◆CPatternLeaf

△ 匹配规则:叶子节点。

△展开规则:只需要展开当前节点。
◆CPatternTree
△匹配规则:操作符树。
△展开规则:先循环遍历子节点,并展开子节点;再展开当前节点。
◆CMultiPatternLeaf
△匹配规则:多于一个叶子节点。
△展开规则:只需要展开多个叶子节点。
◆CMultiPatternTree
△匹配规则:多于一个操作符树。
△展开规则:先循环遍历多个子节点,并展开子节点;再展开当前节点。

 

2. 模式的匹配

模式作为特殊的操作符,多个模式最终组合成一棵模式树,来做模式匹配的工作;模式树的节点也可以是普通的操作符。

  • 非根Group的GroupExpression可以被重复使用
  • Group的子节点必须至少有一个匹配模式树中相应的子节点。
  • 如果模式树中被处理的节点是Pattern节点,则匹配所有Group中的GroupExpression
  • 如果模式树中被处理的节点是一个普通的表达式
1.Pattern.OperatorId==GroupExpression.OperatorId
    1. Pattern.ChildNum == GroupExpression.ChildNum -----> success
       2. Pattern.Child[0] -->MultiPatternLeaf || MultiPatternTree
         1. Pattern.ChildNum == 1 || Pattern.ChildNum == 2
           1. GroupExpression.ChildNum > 1 -----> success

3. 模式的绑定


绑定过程是递归的从Group中取出所有的GroupExpression,以后序遍历的顺序,匹配给定的模式树。

绑定的操作:

  • 展开匹配模式的GroupExpression, 输出为Expression。
  • 这个Expression,会拷贝原始Group的属性。
  • 建立Expression之间的父子关系
  • 把展开后的Expression(表达式树),插入到Memo中。

4. 绑定要解决的问题


◆ 符合模式的父与子可以任意匹配
 △以当前展开的表达式作为Cursor,对一个GroupExpression展开下一个符合Pattern的表达式,直到所有匹配Pattern的Expression被产生,并Transform。


CGroupExpression::Transform(......) 
{
  CExpression *pexprPattern = pxform->PexprPattern();    //模式
  CExpression *pexpr = binding.PexprExtract(......);  //展开第一个表达式。

  while (nullptr != pexpr) 
  {
    // resultNum := len(result.AlternativeExprs)
    pxform->Transform(pxfctxt, pxfres, pexpr);    //应用规则

    CExpression *pexprLast = pexpr;    //设置 Cursor
    pexpr = binding.PexprExtract(......, pexprLast)  //展开下一个表达式,循环处理
  }
}

func (binding *Binding) ExtractGroupExpr(......) *Expression {

  // Cursor:pexprLast,生成下一组子表达式。
  if (!FExtractChildren(mp, pgexpr, pexprPattern, pexprLast, pdrgpexpr)) 
  {
    return nil  //返回空,对应 FExtractChildren 返回 false, 即所有的子Group已经处理完成。
  }

  //生成要被转换的表达式
  CExpression *pexpr = PexprFinalize(mp, pgexpr, pdrgpexpr);
  // GPOS_ASSERT(nil != ret);

  return pexpr;
}

BOOL CBinding::FExtractChildren (.......) bool 
{
  return binding.AdvanceChildCursors(groupExpr, pattern, lastExtractedExpr, exprs)
}

CBinding::FAdvanceChildCursors(......) bool 
{
  //**GroupExpression** 的孩子数
  const ULONG arity = pgexpr->Arity();

  for (ULONG ul = 0; ul < arity; ul++) 
  {
    childPattern := binding.ExpandPattern(......)    //取子 Pattern

    if cursorAdvanced 
    {
      //找到匹配的下一个表达式
      CExpression *pexprPatternChild = PexprExpandPattern(......);
    } 
    else 
    {
      lastExtractedChild := lastExtractedExpr.Children[i]    //取子 Cursor

      // 展开子Group
      child = binding.ExtractGroup(group, childPattern, lastExtractedChild)

      if (nullptr == pexprNewChild) 
      {
        // 从 lastExtractedChild 没有找到匹配的表达式,把 Cursor 重置为空从头开始
        // 重复利用子表达式
        pexprNewChild = PexprExtract(......)

        //增加重置Cursor数
        ulExhaustedCursors++;
      } 
      else
      {
        // 找到匹配节点
        fCursorAdvanced = true;
      }
    }
  }

  //如果相等,则说明所有的子Group都重置了Cursor,说明所有的子Group处理完成
  return ulExhaustedCursors < arity;
}

实际上,因为Cursor本身是一个表达式树,所以Cursor的子表达式作为子Cursor,在绑定子表达式时被用作Cursor,处理绑定。

 

◆  绑定产生的新的Group的处理
 △Memo中的Group,GroupExpression只会增加,不会删除
 △Group的ID,和GroupExpression的ID是递增关系,绑定时,是按照ID的顺序从小到大处理的。这样就能保证新添加的ID一定大于已有元素的ID,在后序的遍历中得到处理。


◆  绑定中所有父节点需要绑定的子节点都存在
△后序遍历,处理GroupExpression的Transform。这样,在父GroupExpression被处理前,所有的子GroupExpression已经做完Transform,新生成的所有符合子Pattern的子Expression被放入Memo,供父GroupExpression使用。


到这里,Transformation的主要流程就介绍完了。后续会介绍GPORCA的属性管理,统计信息管理,优化流程,请多多关注。

 

 

文章转载自公众号:Greenplum中文社区

分类
已于2022-8-3 17:46:17修改
收藏
回复
举报
回复