這一篇會從Hive入手,介紹Hive如何使用Calcite來優化本身的SQL,主要從源碼的角度進行介紹。文末附有一篇其餘博主的文章,從其餘角度闡述Hive CBO的,可供參考。java
另外,上一篇中有提到我整理了Calcite的各類樣例,Calcite的一些使用樣例整理成到github,https://github.com/shezhiming/calcite-demo。其中自定義rule,Relnode等內容有部分參照自Hive。在介紹的時候可能也會稍微講到。node
最後會從Hive這個例子延伸,看看本身能夠怎麼藉助Calcite來優化SQL。git
在開始介紹以前,本着授人以漁的精深,先說下如何使用Hive debug查看源碼執行流程。具體流程能夠參照這篇:github
簡單說就是搭建個hive環境,經過 hive --debug -hiveconf hive.root.logger=DEBUG,console語句開啓 debug 模式,開啓後 hive 會監聽 8000 端口並等待輸入,此時從本地的 hive 源碼項目中配置遠程 debug 就能夠經過 debug 的方式追蹤 hive 執行流程。sql
debug過程當中,執行SQL的入口是在CliDriver.executeDriver()
這個方法,能夠在這個地方打一個斷點,而後就能夠調試跟蹤了。以下圖:docker
搭建hive服務的話,建議使用docker,搭建起來會比較方便一些。數據庫
PS:這裏介紹用的Hive的版本是2.3.x。apache
前面說到,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執行的主要流程,我這裏貼一下:
大體的流程和圖裏面介紹的差很少,不過會多一些細節上的補充,感興趣的童鞋能夠實際執行一下看看執行流程。我這裏簡單介紹下,前幾個步驟就是根據AST Node生成QB,而後再轉換成Operator Tree,而後處理視圖和生成統計信息。最後執行邏輯優化和物理優化並生成MapReduce Task。
上述流程有一個比較容易讓人疑惑的點,不管是AST Node,Operator Tree都比較好理解,後面的邏輯優化和物理優化也都是SQL解析的常規套路,但爲何中間會插入一個QB的階段?
其實這裏插入一個QB,一個主要的目的,是爲了讓Calcite來進行優化。
在Hive中,使用Calcite來進行核心優化,它將AST Node轉換成QB,又將QB轉換成Calcite的RelNode,在Calcite優化完成後,又會將RelNode轉換成Operator Tree,提及來很簡單,但這又是一條很長的調用鏈。
Calcite優化的主要類是CalcitePlanner
,更加細節點,是在CalcitePlannerAction.apply()
這個方法,CalcitePlannerAction
是一個內部類,包括將QB轉換成RelNode,優化具體操做都是在這個方法中進行的。
這個方法的註釋也給出了主要操做步驟,這裏也貼一下流程:
簡單說下,就是先生成RelNode(根據QB),而後進行一系列的優化。這裏的優化最主要的仍是跟join有關的優化,上面流程步驟中的2~7步都是join相關的優化。而後纔是根據各個rule進行優化。最後再轉換成Operator Tree,這就是最上面圖片中QB->Operator Tree的流程。
接下來咱們就深刻這個流程,看看Hive是如何使用Calcite作SQL優化的。
要介紹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。
須要先明確的一點是,元數據提供器須要提供的一個比較重要的數據,是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獲取到的。聽起來挺繞,稍微按順序整理下:
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。
以上~