目錄
- Hive SQL執行流程
- Hive debug簡單介紹
- Hive SQL執行流程
- Hive 使用Calcite優化
- Hive Calcite優化流程
- Hive Calcite使用細則
- Hive向Calcite提供元資料
上一篇主要對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環境,通過 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,
以上~
參考文章:
Apache Hive 是怎樣做基于代價的優化的?
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/107539.html
標籤:其他
上一篇:性能測驗-Locust分布式執行
下一篇:404左葉子之和
