Hive使用Calcite CBO優化流程及SQL優化實戰



上一篇主要對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源碼入口

搭建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執行流程如下圖:
Hive SQL執行流程

即先將SQL解析成AST Node,然后轉換成QB,再轉換成Operator tree,最后進行邏輯優化和物理優化后,就編程一個可執行的MR任務了。對應階段的入口,我也在上面的圖中標注出來了。

其中較為核心的,從AST Node到Phsical Optimize這幾個階段,都是在SemanticAnalyzer.analyzeInternal()方法中進行的。這個方法中的注釋已經跟我們說明了SQL執行的主要流程,我這里貼一下:

  1. Generate Resolved Parse tree from syntax tree
  2. Gen OP Tree from resolved Parse Tree
  3. Deduce Resultset Schema
  4. Generate Parse Context for Optimizer & Physical compiler
  5. Take care of view creation
  6. Generate table access stats if required
  7. Perform Logical optimization
  8. Generate column access stats if required - wait until column pruning takes place during optimization
  9. Optimize Physical op tree & Translate to target execution engine (MR, TEZ..)
  10. put accessed columns to readEntity
  11. 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,優化具體操作都是在這個方法中進行的。

這個方法的注釋也給出了主要操作步驟,這里也貼一下流程:

  1. Gen Calcite Plan
  2. Apply pre-join order optimizations
  3. Apply join order optimizations: reordering MST algorithm
    If join optimizations failed because of missing stats, we continue with the rest of optimizations
  4. Run other optimizations that do not need stats
  5. Materialized view based rewriting
    We disable it for CTAS and MV creation queries (trying to avoid any problem due to data freshness)
  6. 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
  7. Run rule to fix windowing issue when it is done over aggregation columns
  8. Apply Druid transformation rules
  9. 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的架構圖:
Calcite架構

從圖中可以明顯發現,跟QUery Optimizer(優化器)有關的模塊有三個,Operator ExpressionsMetadata ProvidersPluggable 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也自定義了相關的類,就是繼承自RelOptTableRelOptHiveTable。這個類在HiveTableScan初始化的時候,會作為參數傳遞進去。而它的元數據則是通過QB獲取,這個過程也是在CalcitePlannerAction.apply()中完成的,至於QB的元數據,則是在初始化的時候通過Mysql獲取到的。聽起來挺繞,稍微按順序整理下:

  1. QB初始化的時候,通過Mysql獲取元數據信息並注入
  2. QB轉成RelNode的時候,將元數據傳遞到RelOptHiveTable
  3. RelOptHiveTable作為參數新建HiveTableScan

以上就是Hive完成TableScan元數據注入的過程。
自定義RelMetadataProvider
再來說說如何提供RelMetadataProvider。這個主要是通過繼承MetadataHandler實現的,這里貼一下就能清楚metadata有哪些類型,以及Hive實現了哪些:

Hive metadata

這里可以清楚看到,metadata除了之前提到的rowcount,cost,還有size,Distribution等等,其中白色的就是Hive實現的。

而之前一直提到的rowcount和cost,對應的就是HiveRelMdRowCountHiveRelMdCost(這個真正的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 是怎樣做基於代價的優化的?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM