上一篇主要對Calcite的背景,技術特點,SQL的RBO和CBO等做了一個初步的介紹。 深入淺出Calcite與SQL CBO(Cost-Based Optimizer)優化
這一篇會從Hive入手,介紹Hive如何使用Calcite來優化自己的SQL,主要從源碼的角度進行介紹。文末附有一篇其他博主的文章,從其他角度闡述Hive CBO的,可供參考。
另外,上一篇中有提到我整理了Calcite的各種樣例,Calcite的一些使用樣例整理成到github,https://github.com/shezhiming/calcite-demo。其中自定義rule,Relnode等內容有部分參照自Hive。在介紹的時候可能也會稍微講到。
最后會從Hive這個例子延伸,看看自己可以怎么借助Calcite來優化SQL。
Hive SQL執行流程
Hive debug簡單介紹
在開始介紹之前,本着授人以漁的精深,先說下如何使用Hive debug查看源碼執行流程。具體流程可以參照這篇:
簡單說就是搭建個hive環境,通過 hive --debug -hiveconf hive.root.logger=DEBUG,console語句開啟 debug 模式,開啟后 hive 會監聽 8000 端口並等待輸入,此時從本地的 hive 源碼項目中配置遠程 debug 就可以通過 debug 的方式追蹤 hive 執行流程。
debug過程中,執行SQL的入口是在CliDriver.executeDriver()
這個方法,可以在這個地方打一個斷點,然后就可以調試跟蹤了。如下圖:
搭建hive服務的話,建議使用docker,搭建起來會比較方便一些。
PS:這里介紹用的Hive的版本是2.3.x。
Hive SQL執行流程
前面說到,debug輸入語句的入口的類是org.apache.hadoop.hive.cli.CliDriver
。而實際執行SQL語句邏輯的主要模塊是ql(Query Language) 模塊的Driver
類(org.apache.hadoop.hive.ql.Driver)。Driver
主要邏輯,是先調用compile(String command, boolean resetTaskIds, boolean deferClose)
方法,對 SQL 進行編譯,然后Driver
調用execute()
方法,執行對應的MR任務。我們的關注點主要放在compile()方法的執行過程。
在compile()
方法中,整個SQL執行流程如下圖:
即先將SQL解析成AST Node,然后轉換成QB,再轉換成Operator tree,最后進行邏輯優化和物理優化后,就編程一個可執行的MR任務了。對應階段的入口,我也在上面的圖中標注出來了。
其中較為核心的,從AST Node到Phsical Optimize這幾個階段,都是在SemanticAnalyzer.analyzeInternal()
方法中進行的。這個方法中的注釋已經跟我們說明了SQL執行的主要流程,我這里貼一下:
- Generate Resolved Parse tree from syntax tree
- Gen OP Tree from resolved Parse Tree
- Deduce Resultset Schema
- Generate Parse Context for Optimizer & Physical compiler
- Take care of view creation
- Generate table access stats if required
- Perform Logical optimization
- Generate column access stats if required - wait until column pruning takes place during optimization
- Optimize Physical op tree & Translate to target execution engine (MR, TEZ..)
- put accessed columns to readEntity
- if desired check we're not going over partition scan limits
大致的流程和圖里面介紹的差不多,不過會多一些細節上的補充,感興趣的童鞋可以實際執行一下看看執行流程。我這里簡單介紹下,前幾個步驟就是根據AST Node生成QB,然后再轉換成Operator Tree,然后處理視圖和生成統計信息。最后執行邏輯優化和物理優化並生成MapReduce Task。
上述流程有一個比較容易讓人疑惑的點,無論是AST Node,Operator Tree都比較好理解,后面的邏輯優化和物理優化也都是SQL解析的常規套路,但為什么中間會插入一個QB的階段?
其實這里插入一個QB,一個主要的目的,是為了讓Calcite來進行優化。
Hive 使用Calcite優化
Hive Calcite優化流程
在Hive中,使用Calcite來進行核心優化,它將AST Node轉換成QB,又將QB轉換成Calcite的RelNode,在Calcite優化完成后,又會將RelNode轉換成Operator Tree,說起來很簡單,但這又是一條很長的調用鏈。
Calcite優化的主要類是CalcitePlanner
,更加細節點,是在CalcitePlannerAction.apply()
這個方法,CalcitePlannerAction
是一個內部類,包括將QB轉換成RelNode,優化具體操作都是在這個方法中進行的。
這個方法的注釋也給出了主要操作步驟,這里也貼一下流程:
- Gen Calcite Plan
- Apply pre-join order optimizations
- Apply join order optimizations: reordering MST algorithm
If join optimizations failed because of missing stats, we continue with the rest of optimizations - Run other optimizations that do not need stats
- Materialized view based rewriting
We disable it for CTAS and MV creation queries (trying to avoid any problem due to data freshness) - Run aggregate-join transpose (cost based)
If it failed because of missing stats, we continue with the rest of optimizations
7.convert Join + GBy to semijoin - Run rule to fix windowing issue when it is done over aggregation columns
- Apply Druid transformation rules
- Run rules to aid in translation from Calcite tree to Hive tree
10.1. Merge join into multijoin operators (if possible)
10.2. Introduce exchange operators below join/multijoin operators
簡單說下,就是先生成RelNode(根據QB),然后進行一系列的優化。這里的優化最主要的還是跟join有關的優化,上面流程步驟中的2~7步都是join相關的優化。然后才是根據各個rule進行優化。最后再轉換成Operator Tree,這就是最上面圖片中QB->Operator Tree的流程。
接下來我們就深入這個流程,看看Hive是如何使用Calcite做SQL優化的。
Hive Calcite使用細則
要介紹Hive如何利用Calcite做優化,我們還是先轉頭看看Calcite優化需要哪些東西。先貼一下上一篇中介紹到的,Calcite的架構圖:
從圖中可以明顯發現,跟QUery Optimizer
(優化器)有關的模塊有三個,Operator Expressions
,Metadata Providers
和Pluggable Rules
,三者分別是關系表達樹(由RelNode節點組成),元數據提供器,還有Rule。
其中關系表達樹是Calcite將SQL解析校驗后產生的一種關系樹,樹的節點即是RelNode(關系代數節點),RelNode又有多種類型,比如TableScan代表最底層的表輸入,Filter表示Where(關系代數的過濾),Project表示select(關系代數的投影),即大部分的RelNode都會和關系代數中的操作對應。以一條SQL為例,一條簡單的SQL編程RelNode就會是下面這個樣子:
select * from TEST_CSV.TEST01 where TEST01.NAME1='hello';
//RelNode關系樹
Project(ID=[$0], NAME1=[$1], NAME2=[$2])
Filter(condition=[=($1, 'hello')])
TableScan(table=[[TEST_CSV, TEST01]])
再來說說元數據提供器,所謂元數據,就是跟表有關的那些信息,rowcount,表字段等信息。其中rowcount這類信息跟計算cost有關,Calcite有自己的默認的元數據提供器,但做的比較粗糙,如果有需要應該自己提供一個元數據提供器提供自己的元數據信息。
最后就是Rules,這塊Calcite默認已經有非常多的Rules,當然我們也可以定義自己的Rule再添加進去。不過通常基本的SQL優化使用Calcite的Rule就足夠。這里說下怎么在idea里面查看Calcite提供的Rule,先找到RelOptRule
這個類,然后按下查看類繼承關系的快捷鍵(Mac上是Ctrl+h),就能看到多條Rule,如果要自己實現也可以照着其中實現。
稍微總結一下,Calcite已經基本提供了所需要的Rule,所以要使用Calcite優化SQL,我們需要的,是提供SQL對應的RelNode,以及通過元數據提供器提供自身的元數據。
Hive要使用Calcite優化,也無外乎就是提供上述的兩部分內容。
用過Hive的童鞋應該知道,Hive可以通過外部存儲組件存儲數據庫和表元數據信息,包括rowcount,input size等(需要執行Analyze語句或DML才會計算並元數據到Mysql)。Hive要做的就是將這些信息,提供給Calcite。
Hive向Calcite提供元數據
需要先明確的一點是,元數據提供器需要提供的一個比較重要的數據,是rowcount,在進行CBO計算Cost的過程中,CPU,IO等信息也基本都是從rowcount加工而來的。且元數據重要的一個用途,也是進行CBO優化,輸入的元數據可以等價於CBO要用到的Cost數據。
繼續深入CBO的Cost,通過前面的例子,可以知道SQL在Calcite會被解析成RelNode樹,RelNode樹上層節點(Project等)的Cost信息,是由下層的信息計算而得到的。我們的目標是要自定義Cost信息,那么就需要將Hive的元數據注入最底層的TableScan的Cost信息,同時要能夠自定義每個節點的Cost計算方式。
還記得前面說到Calcite默認的元數據提供器比較粗糙嗎,就是體現在它的TableScan的rowcount默認是100,而每個節點的計算邏輯也比較簡單。
所以重點有兩個,一個是最底層TableScan的cost信息注入方式,另一個是如何每種RelNode類型定義計算邏輯的方式。
辦法有兩種,一種是比較上層的,通過自定義RelNode,修改其中的computeSelfCost()
方法和estimateRowCount
方法,這兩個方法,一個是計算Cost信息,另一個是計算行數。這種辦法可以直接解決TableScan的cost注入,和自定義每種RelNode類型的計算邏輯。但這種辦法忽了元數據提供器,算是比較簡單粗暴的方法。
就像這樣:
代碼見:https://github.com/shezhiming/calcite-demo/blob/master/src/main/java/pers/shezm/calcite/optimizer/reloperators/CSVTableScan.java
public class CSVTableScan extends TableScan implements CSVRel {
private RelOptCost cost;
public CSVTableScan(RelOptCluster cluster, RelTraitSet traitSet, RelOptTable table) {
super(cluster, traitSet, table);
}
@Override public double estimateRowCount(RelMetadataQuery mq) {
return 50;
}
@Override
public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
//return super.computeSelfCo(planner, mq);
if (cost != null) {
return cost;
}
//通過工廠生成 RelOptCost ,注入自定義 cost 值並返回
cost = planner.getCostFactory().makeCost(1, 1, 0);
return cost;
}
}
另一種方法則更加底層一些,TableScan的元數據信息,是通過內部變量RelOptTable獲取,那么就自定義RelOptTable實現元數據注入。然后通過實現MetadataDef<BuiltInMetadata.RowCount>
系列的接口,在其中添加自己的計算邏輯,將這些自定義的類都加載到RelMetadataProvider
中(元數據提供器,可以在其中提供自定義的元數據和計算邏輯),再注入到Calcite中就可以實現自己的Cost計算邏輯。這也是Hive的實現方式。
我們從TableScan注入,和RelMetadataProvider這兩方面看看Hive是怎么做。
TableScan的注入元數據
首先,Hive自定義了Calcite的TableScan
,在org.apache.hadoop.hive.ql.optimizer.calcite.reloperators.HiveTableScan
。但這里並不涉及元數據,我們觀察下TableScan
的源碼,
public abstract class TableScan extends AbstractRelNode {
//~ Instance fields --------------------------------------------------------
/**
* The table definition.
*/
protected final RelOptTable table;
//生成 cost 信息
@Override public RelOptCost computeSelfCost(RelOptPlanner planner,
RelMetadataQuery mq) {
double dRows = table.getRowCount();
double dCpu = dRows + 1; // ensure non-zero cost
double dIo = 0;
return planner.getCostFactory().makeCost(dRows, dCpu, dIo);
}
//生成 rowcount 信息
@Override public double estimateRowCount(RelMetadataQuery mq) {
return table.getRowCount();
}
}
順便說下,上面說過,Cost信息和rowcount息息相關,這里就可以看出來了,Cpu直接就用rowcount加一。並且這里也可以看出默認的元數據提供器比較粗糙。
不過我們重點不在這,通過代碼可以發現它主要是通過table這個變量獲取表元數據信息。而hive也自定義了相關的類,就是繼承自RelOptTable
的RelOptHiveTable
。這個類在HiveTableScan
初始化的時候,會作為參數傳遞進去。而它的元數據則是通過QB獲取,這個過程也是在CalcitePlannerAction.apply()
中完成的,至於QB的元數據,則是在初始化的時候通過Mysql獲取到的。聽起來挺繞,稍微按順序整理下:
- QB初始化的時候,通過Mysql獲取元數據信息並注入
- QB轉成RelNode的時候,將元數據傳遞到
RelOptHiveTable
RelOptHiveTable
作為參數新建HiveTableScan
以上就是Hive完成TableScan元數據注入的過程。
自定義RelMetadataProvider
再來說說如何提供RelMetadataProvider
。這個主要是通過繼承MetadataHandler
實現的,這里貼一下就能清楚metadata有哪些類型,以及Hive實現了哪些:
這里可以清楚看到,metadata除了之前提到的rowcount,cost,還有size,Distribution等等,其中白色的就是Hive實現的。
而之前一直提到的rowcount和cost,對應的就是HiveRelMdRowCount
和HiveRelMdCost
(這個真正的cost模型實現,是在HiveCostModel
)。這里貼一下HiveCostModel
中Join的Cost自定義計算邏輯,因為join優化是一個重點,所以這里會根據不同實現類去計算cost,相比Calcite默認實現,精細很多了。
public abstract class HiveCostModel {
......其他代碼
public RelOptCost getJoinCost(HiveJoin join) {
// Select algorithm with min cost
JoinAlgorithm joinAlgorithm = null;
RelOptCost minJoinCost = null;
if (LOG.isTraceEnabled()) {
LOG.trace("Join algorithm selection for:\n" + RelOptUtil.toString(join));
}
for (JoinAlgorithm possibleAlgorithm : this.joinAlgorithms) {
if (!possibleAlgorithm.isExecutable(join)) {
continue;
}
RelOptCost joinCost = possibleAlgorithm.getCost(join);
if (LOG.isTraceEnabled()) {
LOG.trace(possibleAlgorithm + " cost: " + joinCost);
}
if (minJoinCost == null || joinCost.isLt(minJoinCost) ) {
joinAlgorithm = possibleAlgorithm;
minJoinCost = joinCost;
}
}
if (LOG.isTraceEnabled()) {
LOG.trace(joinAlgorithm + " selected");
}
join.setJoinAlgorithm(joinAlgorithm);
join.setJoinCost(minJoinCost);
return minJoinCost;
}
......其他代碼
}
其他的也和這個差不多,就是更加精細的自定義Cost計算,就不多展示了。
OK,說完上面這些,Hive的優化也就差不多介紹完了,這里重點還是介紹了Hive如何向Calcite中注入元數據信息以及實現自定義的RelNode計算邏輯。至於Calcite進行RBO和CBO優化的更多細節,我上一篇有提到,也有給出相關資料,這里就不多介紹。
深入淺出Calcite與SQL CBO(Cost-Based Optimizer)優化
還有另一個點是編寫自定義的rule實現自定義優化,這一點以后與機會再說。
另外我最上方的github中,也有簡單照着hive,實現了自己注入元數據和自定義RelNode的計算方式,基本都是從最簡單的CSV的例子延伸而言,方便理解,有興趣的朋友可以看看,如果有幫助不妨點個star。
以上~