Impala 3.4 SQL查询之重写(二)

2022-05-20 08:32:40 浏览数 (2)

在上一篇文章中,我们介绍了Impala基本的SQL解析流程。本文我们将跟大家一起看下Impala中的一些SQL重写规则。这里,我们首先回顾下关于Analyzer的几个类的关系图,如下所示:

当SQL被解析为特定的StatementBase之后,紧接着会构造一个AnalysisContext对象,这个类可以理解为整个SQL解析过程的封装,包括了:parsing, analyzing and rewriting这几个过程。在AnalysisContext的analyze方法中,我们构造了Analyzer变量,完成了对StatementBase的analyze(在上篇文章中也已经介绍过)。相关函数调用过程如下所示:

代码语言:javascript复制
Frontend.doCreateExecRequest()
-AnalysisContext.analyzeAndAuthorize()
--AnalysisContext.analyze()
---AnalysisContext.createAnalyzer()
----Analyzer.ctor()
-----GlobalState.ctor()
---StatementBase.analyze()
---StatementBase.rewriteExprs()

最终我们在Analyzer.GlobalState的构造函数中,将各种重写规则加入到了Analyzer中,相关代码如下所示:

代码语言:javascript复制
    // Analyzer.java
    public GlobalState(StmtTableCache stmtTableCache, TQueryCtx queryCtx,
        AuthorizationFactory authzFactory) {
      this.stmtTableCache = stmtTableCache;
      this.queryCtx = queryCtx;
      this.authzFactory = authzFactory;
      this.lineageGraph = new ColumnLineageGraph();
      List<ExprRewriteRule> rules = new ArrayList<>();
      // BetweenPredicates must be rewritten to be executable. Other non-essential
      // expr rewrites can be disabled via a query option. When rewrites are enabled
      // BetweenPredicates should be rewritten first to help trigger other rules.
      rules.add(BetweenToCompoundRule.INSTANCE);
      // Binary predicates must be rewritten to a canonical form for both Kudu predicate
      // pushdown and Parquet row group pruning based on min/max statistics.
      rules.add(NormalizeBinaryPredicatesRule.INSTANCE);
      if (queryCtx.getClient_request().getQuery_options().enable_expr_rewrites) {
        rules.add(FoldConstantsRule.INSTANCE);
        rules.add(NormalizeExprsRule.INSTANCE);
        rules.add(ExtractCommonConjunctRule.INSTANCE);
        // Relies on FoldConstantsRule and NormalizeExprsRule.
        rules.add(SimplifyConditionalsRule.INSTANCE);
        rules.add(EqualityDisjunctsToInRule.INSTANCE);
        rules.add(NormalizeCountStarRule.INSTANCE);
        rules.add(SimplifyDistinctFromRule.INSTANCE);
        rules.add(SimplifyCastStringToTimestamp.INSTANCE);
      }
      exprRewriter_ = new ExprRewriter(rules);
    }

这个构造函数里面添加了很多重写规则,这些规则最终都会被应用于SQL的重写中。Impala目前包含了很多重写规则,相关类图如下所示:

所有的重写规则都实现了ExprRewriteRule这个接口,接口本身只包含一个方法apply,接收一个Expr和Analyzer,返回是一个修改之后的Expr。关于Expr,我们在上篇文章中也已经提到了过了,这里就不再展开描述。需要注意的是,Impala还提供了一个query option,叫ENABLE_EXPR_REWRITES,默认为true,会启用更多的重写规则,对于SQL的查询性能提升有很大的帮助。 通过上述代码可以看到,在构造GlobalState成员变量的时候,会将所有的重写规则放到一个数组当中,然后构造一个ExprRewriter类,这个类的作用就是:使用重写规则的数组,对指定的Expr进行重写操作。在完成对应的Analyzer构造和StatementBase的解析之后,会调用StatementBase的rewriteExprs方法,来对这个statement的所有Exprs进行重写,这里我们以SelectStmt为例(StatementBase本身是抽象类,并没有实现这个方法),来看一下是如何对Expr进行重写的:

代码语言:javascript复制
  // SelectStmt.java
  public void rewriteExprs(ExprRewriter rewriter) throws AnalysisException {
    Preconditions.checkState(isAnalyzed());
    selectList_.rewriteExprs(rewriter, analyzer_);
    for (TableRef ref: fromClause_.getTableRefs()) ref.rewriteExprs(rewriter, analyzer_);
    if (whereClause_ != null) {
      whereClause_ = rewriter.rewrite(whereClause_, analyzer_);
      // Also rewrite exprs in the statements of subqueries.
      List<Subquery> subqueryExprs = new ArrayList<>();
      whereClause_.collect(Subquery.class, subqueryExprs);
      for (Subquery s: subqueryExprs) s.getStatement().rewriteExprs(rewriter);
    }
    if (havingClause_ != null) {
      havingClause_ = rewriteCheckOrdinalResult(rewriter, havingClause_);
    }
    if (groupingExprs_ != null) {
      for (int i = 0; i < groupingExprs_.size();   i) {
        groupingExprs_.set(i, rewriteCheckOrdinalResult(
            rewriter, groupingExprs_.get(i)));
      }
    }
    if (orderByElements_ != null) {
      for (OrderByElement orderByElem: orderByElements_) {
        orderByElem.setExpr(rewriteCheckOrdinalResult(rewriter, orderByElem.getExpr()));
      }
    }
  }

从上述代码可以看到,这个函数分别对各个部分调用了rewriteExprs函数,传入rewrite成员r,进行重写,包括:selectList_、fromClause_、whereClause_等,这些正是我们在上篇文章中介绍到的SelectStmt的各个部分。对于selectList_,又调用了SelectList的rewriteExprs方法,如下所示:

代码语言:javascript复制
  // SelectList.java
  public void rewriteExprs(ExprRewriter rewriter, Analyzer analyzer)
      throws AnalysisException {
    for (SelectListItem item: items_) {
      if (item.isStar()) continue;
      item.setExpr(rewriter.rewrite(item.getExpr(), analyzer));
    }
  }

最终,我们可以看到,通过循环处理,对每个SelectListItem中的Expr进行了重写,这个Expr就是通过SelectListItem的getExpr和setExpr进行获取和更新的,其他fromClause_、whereClause_等各个部分,也是类似的处理流程。 除此之外,在3.4.0版本中,Impala还提供了对解析之后的SQL进行展示,我们来看一个简单的例子,原始SQL如下所示:

代码语言:javascript复制
select user as name,count(2) from iceberg_partitioned
where id between 2 and 10 group by user;

执行完成之后,就可以在Impala的web页面看到如下所示的SQL解析之后的输出:

可以看到,解析之后的SQL经过了重写和隐式转换:

  • count(2)被转换成了count(*)
  • between被转换成了>=和<=
  • 常量2和10加上了CAST的操作

输出的格式主要是通过如下的这个enum来控制的:

代码语言:javascript复制
public enum ToSqlOptions {
  /**
   * The default way of displaying the original SQL query without rewrites.
   */
  DEFAULT(false, false),

  /**
   * Show rewritten query if it exists
   */
  REWRITTEN(true, false),

  /**
   * Show Implicit Casts.
   * To see implicit casts we must also show rewrites as otherwise we see original SQL.
   * This does have the consequence that the sql with implict casts may possibly fail
   * to parse if resubmitted as, for example, EXISTS queries that are rewritten as
   * semi-joins are not legal SQL.
   */
  SHOW_IMPLICIT_CASTS(true, true);

  private boolean rewritten_;

  private boolean implictCasts_;
  // 省略余下代码

一共有三个选项:DEFAULT、REWRITTEN和SHOW_IMPLICIT_CASTS,上述截图中的结果,就是使用了SHOW_IMPLICIT_CASTS之后的格式化结果。输出的函数就是我们在上篇文章中提到的ParseNode中的toSql,这个函数有两个版本,不带参数的默认是使用ToSqlOptions.DEFAULT。对于我们的SQL示例,是一个SELECT语句,所以解析后的SQL格式化,最终是由SelectStmt.toSql(ToSqlOptions options)函数完成的,输入参数就是SHOW_IMPLICIT_CASTS。 到这里,关于Impala的SQL规则重写基本就介绍完了,后续有时间的话,会跟大家继续分享Impala的SQL解析的其他知识。

0 人点赞