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


上一篇主要對Calcite的背景,技術特色,SQL的RBO和CBO等作了一個初步的介紹。 深刻淺出Calcite與SQL CBO(Cost-Based Optimizer)優化

這一篇會從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 SQL執行流程

Hive debug簡單介紹

在開始介紹以前,本着授人以漁的精深,先說下如何使用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源碼入口

搭建hive服務的話,建議使用docker,搭建起來會比較方便一些。數據庫

PS:這裏介紹用的Hive的版本是2.3.x。apache

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 是怎樣作基於代價的優化的?

相關文章
相關標籤/搜索