什么是查詢優(yōu)化器
查詢優(yōu)化器是傳統(tǒng)數(shù)據(jù)庫(kù)的核心模塊,也是大數(shù)據(jù)計(jì)算引擎的核心模塊,開源大數(shù)據(jù)引擎如 Impala、Presto、Drill、HAWQ、 Spark、Hive 等都有自己的查詢優(yōu)化器。Calcite 就是從 Hive 的優(yōu)化器演化而來(lái)的。
優(yōu)化器的作用:將解析器生成的關(guān)系代數(shù)表達(dá)式轉(zhuǎn)換成執(zhí)行計(jì)劃,供執(zhí)行引擎執(zhí)行,在這個(gè)過(guò)程中,會(huì)應(yīng)用一些規(guī)則優(yōu)化,以幫助生成更高效的執(zhí)行計(jì)劃。
基于成本優(yōu)化(CBO)
基于代價(jià)的優(yōu)化器(Cost-Based Optimizer,CBO):根據(jù)優(yōu)化規(guī)則對(duì)關(guān)系表達(dá)式進(jìn)行轉(zhuǎn)換,這里的轉(zhuǎn)換是說(shuō)一個(gè)關(guān)系表達(dá)式經(jīng)過(guò)優(yōu)化規(guī)則后會(huì)生成另外一個(gè)關(guān)系表達(dá)式,同時(shí)原有表達(dá)式也會(huì)保留,經(jīng)過(guò)一系列轉(zhuǎn)換后會(huì)生成多個(gè)執(zhí)行計(jì)劃,然后 CBO 會(huì)根據(jù)統(tǒng)計(jì)信息和代價(jià)模型 (Cost Model) 計(jì)算每個(gè)執(zhí)行計(jì)劃的 Cost,從中挑選 Cost 最小的執(zhí)行計(jì)劃。
由上可知,CBO 中有兩個(gè)依賴:統(tǒng)計(jì)信息和代價(jià)模型。統(tǒng)計(jì)信息的準(zhǔn)確與否、代價(jià)模型的合理與否都會(huì)影響 CBO 選擇最優(yōu)計(jì)劃。 從上述描述可知,CBO 是優(yōu)于 RBO 的,原因是 RBO 是一種只認(rèn)規(guī)則,對(duì)數(shù)據(jù)不敏感的呆板的優(yōu)化器,而在實(shí)際過(guò)程中,數(shù)據(jù)往往是有變化的,通過(guò) RBO 生成的執(zhí)行計(jì)劃很有可能不是最優(yōu)的。事實(shí)上目前各大數(shù)據(jù)庫(kù)和大數(shù)據(jù)計(jì)算引擎都傾向于使用 CBO,但是對(duì)于流式計(jì)算引擎來(lái)說(shuō),使用 CBO 還是有很大難度的,因?yàn)椴⒉荒芴崆邦A(yù)知數(shù)據(jù)量等信息,這會(huì)極大地影響優(yōu)化效果,CBO 主要還是應(yīng)用在離線的場(chǎng)景。
基于規(guī)則優(yōu)化(RBO)
基于規(guī)則的優(yōu)化器(Rule-Based Optimizer,RBO):根據(jù)優(yōu)化規(guī)則對(duì)關(guān)系表達(dá)式進(jìn)行轉(zhuǎn)換,這里的轉(zhuǎn)換是說(shuō)一個(gè)關(guān)系表達(dá)式經(jīng)過(guò)優(yōu)化規(guī)則后會(huì)變成另外一個(gè)關(guān)系表達(dá)式,同時(shí)原有表達(dá)式會(huì)被裁剪掉,經(jīng)過(guò)一系列轉(zhuǎn)換后生成最終的執(zhí)行計(jì)劃。
RBO 中包含了一套有著嚴(yán)格順序的優(yōu)化規(guī)則,同樣一條 SQL,無(wú)論讀取的表中數(shù)據(jù)是怎么樣的,最后生成的執(zhí)行計(jì)劃都是一樣的。同時(shí),在 RBO 中 SQL 寫法的不同很有可能影響最終的執(zhí)行計(jì)劃,從而影響執(zhí)行計(jì)劃的性能。
優(yōu)化規(guī)則
無(wú)論是 RBO,還是 CBO 都包含了一系列優(yōu)化規(guī)則,這些優(yōu)化規(guī)則可以對(duì)關(guān)系表達(dá)式進(jìn)行等價(jià)轉(zhuǎn)換,常見的優(yōu)化規(guī)則包含:
- 謂詞下推 Predicate Pushdown
- 常量折疊 Constant Folding
- 列裁剪
- 其他
在 Calcite 的代碼里,有一個(gè)測(cè)試類(org.apache.calcite.test.RelOptRulesTest)匯集了對(duì)目前內(nèi)置所有 Rules 的測(cè)試 case,這個(gè)測(cè)試類可以方便我們了解各個(gè) Rule 的作用。
RBO的規(guī)則在Calite 1.24版本的時(shí)候,集中在org.apache.calcite.rel.rules.CoreRules中
在這里有下面一條 SQL,通過(guò)這條語(yǔ)句來(lái)說(shuō)明一下上面介紹的這些種規(guī)則。
select 10 + 30, users.name, users.age
from users join jobs on users.id= user.id
where users.age > 30 and jobs.id>10
謂詞下推(Predicate Pushdown)
關(guān)于謂詞下推,它主要還是從關(guān)系型數(shù)據(jù)庫(kù)借鑒而來(lái),關(guān)系型數(shù)據(jù)中將謂詞下推到外部數(shù)據(jù)庫(kù)用以減少數(shù)據(jù)傳輸;屬于邏輯優(yōu)化,優(yōu)化器將謂詞過(guò)濾下推到數(shù)據(jù)源,使物理執(zhí)行跳過(guò)無(wú)關(guān)數(shù)據(jù)。最常見的例子就是 join 與 filter 操作一起出現(xiàn)時(shí),提前執(zhí)行 filter 操作以減少處理的數(shù)據(jù)量,將 filter 操作下推,以上面例子為例,示意圖如下
(對(duì)應(yīng) Calcite 中的CoreRules.FILTER_INTO_JOIN):
在進(jìn)行 join 前進(jìn)行相應(yīng)的過(guò)濾操作,可以極大地減少參加 join 的數(shù)據(jù)量。
常量折疊(Constant Folding)
常量折疊也是常見的優(yōu)化策略,這個(gè)比較簡(jiǎn)單、也很好理解,可以看下 編譯器優(yōu)化 – 常量折疊 這篇文章,基本不用動(dòng)腦筋就能理解,對(duì)于我們這里的示例,有一個(gè)常量表達(dá)式 10 + 30,如果不進(jìn)行常量折疊,那么每行數(shù)據(jù)都需要進(jìn)行計(jì)算,進(jìn)行常量折疊后的結(jié)果如下圖所示
( 對(duì)應(yīng) Calcite 中的 中的CoreRules.PROJECT_REDUCE_EXPRESSIONS Rule):
列裁剪(Column Pruning)
列裁剪也是一個(gè)經(jīng)典的優(yōu)化規(guī)則,在本示例中對(duì)于jobs 表來(lái)說(shuō),并不需要掃描它的所有列值,而只需要列值 id,所以在掃描 jobs 之后需要將其他列進(jìn)行裁剪,只留下列 id。這個(gè)優(yōu)化帶來(lái)的好處很明顯,大幅度減少了網(wǎng)絡(luò) IO、內(nèi)存數(shù)據(jù)量的消耗。裁剪前后的示意圖如下(不過(guò)并沒(méi)有找到 Calcite 對(duì)應(yīng)的 Rule):
如何使用RBO
public class CalciteRuleTest {
public static void main(String[] args) throws SqlParseException, ValidationException, RelConversionException {
Planner planner = getPlanner();
SqlNode sqlNode = planner.parse("SELECT DATE_CD,SUM(IB0002001_CN000) FROM \n" +
"(SELECT IDX_ID,CUBE2L_IB00040010_CN000.DATE_CD,SUM(CUBE2L_IB00040010_CN000.IDX_VAL) AS IB0002001_CN000 FROM " +
"CUBE2L_IB00040010_CN000" +
" GROUP BY CUBE2L_IB00040010_CN000.DATE_CD,IDX_ID) IB0002001_CN000 WHERE IDX_ID IN ('IB0002001_CN000') AND DATE_CD = '2020-05-31' GROUP BY DATE_CD ");
System.out.println(sqlNode.toString());
planner.validate(sqlNode);
RelRoot relRoot = planner.rel(sqlNode);
RelNode relNode = relRoot.project();
System.out.println();
System.out.print(RelOptUtil.toString(relNode));
HepProgramBuilder builder = new HepProgramBuilder();
// builder.addRuleInstance(CoreRules.FILTER_MERGE); //note: 添加 rule
// builder.addRuleInstance(CoreRules.PROJECT_FILTER_TRANSPOSE);
// builder.addRuleInstance(CoreRules.FILTER_AGGREGATE_TRANSPOSE);
// builder.addRuleInstance(CoreRules.FILTER_PROJECT_TRANSPOSE);
// builder.addRuleInstance(CoreRules.FILTER_AGGREGATE_TRANSPOSE);
builder.addRuleInstance(CoreRules.AGGREGATE_REMOVE);
builder.addRuleInstance(CoreRules.PROJECT_FILTER_TRANSPOSE);
HepPlanner hepPlanner = new HepPlanner(builder.build());
hepPlanner.setRoot(relNode);
relNode = hepPlanner.findBestExp();
System.out.println();
System.out.println("After --------------------");
System.out.println();
System.out.print(RelOptUtil.toString(relNode));
RelToSqlConverter relToSqlConverter = new RelToSqlConverter(MysqlSqlDialect.DEFAULT);
SqlImplementor.Result result = relToSqlConverter.visitRoot(relNode);
System.out.println();
System.out.println(result.asSelect().toString());
}
private static Planner getPlanner() {
SchemaPlus rootSchema = Frameworks.createRootSchema(true);
// 添加表的schema信息,才可以通過(guò)validate
rootSchema.add("CUBE2L_IB00040010_CN000", new AbstractTable() {
public RelDataType getRowType(final RelDataTypeFactory typeFactory) {
RelDataTypeFactory.Builder builder = typeFactory.builder();
builder.add("DATE_CD", typeFactory.createTypeWithNullability(typeFactory.createSqlType(SqlTypeName.DATE), true));
builder.add("IDX_VAL", typeFactory.createTypeWithNullability(typeFactory.createSqlType(SqlTypeName.BIGINT), true));
builder.add("test", typeFactory.createTypeWithNullability(typeFactory.createSqlType(SqlTypeName.BIGINT), true));
builder.add("IDX_ID", typeFactory.createTypeWithNullability(typeFactory.createSqlType(SqlTypeName.VARCHAR), true));
return builder.build();
}
});
SqlParser.ConfigBuilder parserConfig = SqlParser.configBuilder();
SqlParser.Config build = parserConfig.setCaseSensitive(false).setLex(Lex.MYSQL).build();
FrameworkConfig frameworkConfig = Frameworks.newConfigBuilder()
.parserConfig(build)
.defaultSchema(rootSchema)
.build();
Planner planner = Frameworks.getPlanner(frameworkConfig);
return planner;
}
上面的代碼總共分為三步:
- 初始化 HepProgram 對(duì)象;
- 初始化 HepPlanner 對(duì)象,并通過(guò) setRoot() 方法將 RelNode 樹轉(zhuǎn)換成 HepPlanner 內(nèi)部使用的 Graph;
- 通過(guò) findBestExp() 找到最優(yōu)的 plan,規(guī)則的匹配都是在這里進(jìn)行。
1. 初始化 HepProgram
這幾步代碼實(shí)現(xiàn)沒(méi)有太多需要介紹的地方,先初始化 HepProgramBuilder 也是為了后面初始化 HepProgram 做準(zhǔn)備,HepProgramBuilder 主要也就是提供了一些配置設(shè)置和添加規(guī)則的方法等,常用的方法如下:
- addRuleInstance():注冊(cè)相應(yīng)的規(guī)則;
- addRuleCollection():這里是注冊(cè)一個(gè)規(guī)則集合,先把規(guī)則放在一個(gè)集合里,再注冊(cè)整個(gè)集合,如果規(guī)則多的話,一般是這種方式;
- addMatchLimit():設(shè)置 MatchLimit,這個(gè) rule match 次數(shù)的最大限制;
- addMatchOrder() Rule 匹配的順序
HepProgram 這個(gè)類對(duì)于后面 HepPlanner 的優(yōu)化很重要,它定義 Rule 匹配的順序,默認(rèn)按【深度優(yōu)先】順序,它可以提供以下幾種(見 HepMatchOrder 類):
- ARBITRARY:按任意順序匹配(因?yàn)樗怯行У?,而且大部分?Rule 并不關(guān)心匹配順序);
- BOTTOM_UP:自下而上,先從子節(jié)點(diǎn)開始匹配;
- TOP_DOWN:自上而下,先從父節(jié)點(diǎn)開始匹配;
- DEPTH_FIRST:深度優(yōu)先匹配,某些情況下比 ARBITRARY 高效(為了避免新的 vertex 產(chǎn)生后又從 root 節(jié)點(diǎn)開始匹配)。
這個(gè)匹配順序到底是什么呢?對(duì)于規(guī)則集合 rules,HepPlanner 的算法是:從一個(gè)節(jié)點(diǎn)開始,跟 rules 的所有 Rule 進(jìn)行匹配,匹配上就進(jìn)行轉(zhuǎn)換操作,這個(gè)節(jié)點(diǎn)操作完,再進(jìn)行下一個(gè)節(jié)點(diǎn),這里的匹配順序就是指的節(jié)點(diǎn)遍歷順序(這種方式的優(yōu)劣,我們下面再說(shuō))。
分析現(xiàn)有rule
最簡(jiǎn)單的例子,Join結(jié)合律,JoinAssociateRule
首先所有的Rule都繼承RelOptRule類
/**
* Planner rule that changes a join based on the associativity rule.
*
* <p>((a JOIN b) JOIN c) → (a JOIN (b JOIN c))</p>
*
* <p>We do not need a rule to convert (a JOIN (b JOIN c)) →
* ((a JOIN b) JOIN c) because we have
* {@link JoinCommuteRule}.
*
* @see JoinCommuteRule
*/
public class JoinAssociateRule extends RelOptRule implements TransformationRule
對(duì)于Join結(jié)合律,調(diào)用super,即RelOptRule的構(gòu)造函數(shù),匹配的樹狀為
join
join
any
/**
* Creates a JoinAssociateRule.
*/
public JoinAssociateRule(RelBuilderFactory relBuilderFactory) {
super(
operand(Join.class,
operand(Join.class, any()),
operand(RelSubset.class, any())),
relBuilderFactory, null);
}
operand也是一個(gè)樹形結(jié)構(gòu),Top的Operand的類型是Join,他有兩個(gè)children,其中一個(gè)也是join,另一個(gè)是RelSubset
他們的children是any()
public void onMatch(final RelOptRuleCall call) {
final Join topJoin = call.rel(0);
final Join bottomJoin = call.rel(1);
final RelNode relA = bottomJoin.getLeft();
final RelNode relB = bottomJoin.getRight();
final RelSubset relC = call.rel(2);
final RelOptCluster cluster = topJoin.getCluster();
final RexBuilder rexBuilder = cluster.getRexBuilder();
if (relC.getConvention() != relA.getConvention()) {
// relC could have any trait-set. But if we're matching say
// EnumerableConvention, we're only interested in enumerable subsets.
return;
}
// topJoin
// / \
// bottomJoin C
// / \
// A B
final int aCount = relA.getRowType().getFieldCount();
final int bCount = relB.getRowType().getFieldCount();
final int cCount = relC.getRowType().getFieldCount();
final ImmutableBitSet aBitSet = ImmutableBitSet.range(0, aCount);
final ImmutableBitSet bBitSet =
ImmutableBitSet.range(aCount, aCount + bCount);
if (!topJoin.getSystemFieldList().isEmpty()) {
// FIXME Enable this rule for joins with system fields
return;
}
// If either join is not inner, we cannot proceed.
// (Is this too strict?)
if (topJoin.getJoinType() != JoinRelType.INNER
|| bottomJoin.getJoinType() != JoinRelType.INNER) {
return;
}
// Goal is to transform to
//
// newTopJoin
// / \
// A newBottomJoin
// / \
// B C
// Split the condition of topJoin and bottomJoin into a conjunctions. A
// condition can be pushed down if it does not use columns from A.
final List<RexNode> top = new ArrayList<>();
final List<RexNode> bottom = new ArrayList<>();
JoinPushThroughJoinRule.split(topJoin.getCondition(), aBitSet, top, bottom);
JoinPushThroughJoinRule.split(bottomJoin.getCondition(), aBitSet, top,
bottom);
// Mapping for moving conditions from topJoin or bottomJoin to
// newBottomJoin.
// target: | B | C |
// source: | A | B | C |
final Mappings.TargetMapping bottomMapping =
Mappings.createShiftMapping(
aCount + bCount + cCount,
0, aCount, bCount,
bCount, aCount + bCount, cCount);
final List<RexNode> newBottomList =
new RexPermuteInputsShuttle(bottomMapping, relB, relC)
.visitList(bottom);
RexNode newBottomCondition =
RexUtil.composeConjunction(rexBuilder, newBottomList);
final Join newBottomJoin =
bottomJoin.copy(bottomJoin.getTraitSet(), newBottomCondition, relB,
relC, JoinRelType.INNER, false);
// Condition for newTopJoin consists of pieces from bottomJoin and topJoin.
// Field ordinals do not need to be changed.
RexNode newTopCondition = RexUtil.composeConjunction(rexBuilder, top);
@SuppressWarnings("SuspiciousNameCombination")
final Join newTopJoin =
topJoin.copy(topJoin.getTraitSet(), newTopCondition, relA,
newBottomJoin, JoinRelType.INNER, false);
call.transformTo(newTopJoin);
}
自定義rule
補(bǔ)充下基礎(chǔ)概念
RexLiteral表示常量,RexVariable表示變量,RexCall表示操作來(lái)連接Literal和Variable
自定義rule,有三個(gè)主要的方法,一個(gè)是matches,一個(gè)是onMatch,一個(gè)是構(gòu)造函數(shù)
判斷一個(gè)tree是否滿足該rule,先從構(gòu)造函數(shù)開始匹配,滿足構(gòu)造函數(shù)的tree,在滿足matches方法,就可以進(jìn)入到onMatch方法。
matches方法默認(rèn)是返回true,可以進(jìn)行改寫
@Override
public boolean matches(RelOptRuleCall call) {
return super.matches(call);
}
假設(shè)想要匹配
join
project
構(gòu)造函數(shù)得這么寫
super(
operand(Join.class,
operand(Project.class, any())),
relBuilderFactory, null);
在onMatch中可以通過(guò)call.rel(x)獲取構(gòu)造函數(shù)中匹配的relNode
Join join = call.rel(0);
Project project = call.rel(1);
通過(guò)一些強(qiáng)轉(zhuǎn)可以獲取對(duì)應(yīng)的節(jié)點(diǎn)
((HepRelVertex)rel.getInput(0)).getCurrentRel()
創(chuàng)建rebuild,生成node
RelBuilder relBuilder = call.builder();
RelNode build = relBuilder.build();
對(duì)relBuilder操作
relBuilder.push 加入數(shù)據(jù)源
relbuilder.project 加入投影 還可以agg filter 等等
通過(guò)call替換掉原來(lái)的tree
call.transformTo(node);
常用函數(shù)
RexUtil.composeDisjunction 以or合并兩個(gè)規(guī)則
RexUtil.composeConjunction 以and合并兩個(gè)規(guī)則
((HepRelVertex)rel.getInput(0)).getCurrentRel() 強(qiáng)轉(zhuǎn)獲取節(jié)點(diǎn)
node.getCluster()可以繼續(xù)get出很多東西getPlanner()、getRexBuilder()、getTypeFactory()
RelDataType sqlType = typeFactory.createSqlType(SqlTypeName.ANY) 創(chuàng)建對(duì)應(yīng)的類型
rexBuilder.makeLiteral 創(chuàng)建常量 makeCall創(chuàng)建操作符等等
SqlStdOperatorTable可以獲取函數(shù)集合 SUM MIN MAX