在上一篇文章中,我们介绍了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解析的其他知识。