SQL 執行 - 執行器最佳化

KaiwuDB發表於2023-11-14

本期技術貼主要介紹查詢執行引擎的最佳化。查詢執行引擎負責將  SQL  最佳化器生成的執行計劃進行解釋,透過任務排程執行從儲存引擎裡面把資料讀取出來,計算出結果集,然後返回給客戶。

關係型資料庫 發展的早期,受制於計算機  IO  能力的約束,計算在查詢整體的耗時佔比並不明顯,這個時候關注重點主要放在對於查詢的最佳化。最佳化器的好壞,對於執行計劃的優劣有著重要的意義,查詢執行引擎的作用在資料庫最佳化中對應等級是相對弱化的。但隨著計算機硬體的發展,查詢執行引擎也逐漸展現出他們 重要地位。

本篇部落格結合 KaiwuDB 的部分原始碼和例項,介紹其如何充分發揮底層硬體的能力,最佳化查詢執行引擎,從而提升 資料庫系統 的效能。查詢執行引擎是否高效與其採用的模型有直接關係, 1990 年,論文 " Volcano, an Extensible and Parallel Query Evaluation System " 提出了火山模型,這也是 KaiwuDB 查詢執行引擎的基礎。

一、火山模型/迭代模型

火山模型作為經典的查詢執行模型被諸如 Oracle、MySQL 等主流關係數 據庫採用。該模型將關係代數中每一種運算元抽象為一個  Operator (迭代器),每個 Operator 都提供一個介面 Next(),呼叫該介面會返回該運算元產生/處理的一行資料(Tuple)。透過在查詢樹根節點自頂向下地呼叫 Next(),資料自底向上地被拉取處理,因而火山模型也稱為拉取執行模型(Pull Based)


火山模型


以一個兩表連線的查詢為例,讓我們留意圖中的第 ④ 步,Select 運算子。


呼叫 Next() 方法從其子運算子請求下一行,並檢查它是否透過了篩選條件。如果是,則該行將返回到其父運算子;否則,將丟棄該行並重復該過程。

// RunFilter runs a filter  expression and returns whether the filter passes.
func RunFilter(filter tree.TypedExpr, evalCtx *tree.EvalContext)(bool, error){
    if filter == nil{
        return true, nil
    }
    
    d, err := filter.Eval(evalCtx)
    if err != nil{
        return false,err
    }
    return d == tree.DBoolTrue, nil
}

上述即 KaiwuDB 在處理行時進行過濾的函式,引數 Filter 型別為 tree.TypedExpr,意為一個通用表示式。也就是說,對於每一行,都會呼叫一個完全通用的標量表示式的過濾器。表示式可以是任何東西:乘法、除法、相等檢查或內建函式,它甚至可以是由上述表示式組成的樹。由於這種通用性,計算機在過濾每一行時都有很多工作要做,它必須在做任何工作之前檢查表示式是什麼,這與解釋型語言的邏輯相同(與編譯型語言相比)。

儘管火山模型簡單、直觀、易用, 只需將 Oper ator 自由地 組裝,且 每個 Operator 只關心自己的處理邏輯,執行引擎並不感知。但是,在執行過程中,迭代一次只處理一行資料,資料區域性性差,很容易使 CPU cache 失效,並且呼叫 Next() 函式( 虛擬函式 )次數太多,開銷較大,使得 CPU 執行效率不高。

二、運算元融合

將經常出現的 Operator(如 Project 和 Filter)融合在其他 Operator 中能夠一定程度上減少虛擬函式的呼叫,提高單個 Operator 的處理能力和資料區域性性。以 KaiwuDB 的 tablereader 運算元為例,在掃表時便能夠對資料行進行過濾和投影,其 Next() 函式中實現了相關邏輯。

下圖是 tablereader 運算元 Next() 函式呼叫的簡化 時序圖 ,可以看到,在讀取一條資料進行處理時會判斷 Filter 和 outputCols 決定是否進行 Filter 和 Projection 操作。

TableReader 運算元 Next() 函式呼叫的簡化時序圖

下圖展示了一條查詢語句示例的物理計劃,也可以看到在 TableReader 運算元中對範圍 Spans 和輸出列 Out 進行了限制。


查詢的計劃示例


三、向量化模型

不同於火山模型按行迭代的方式,如下圖所示,向量化模型採用批次迭代,在運算元間一次傳遞一批資料。透過更改資料方向(從行到列),把從列到元組的轉化推遲到較晚的時候執行,來更有效地利用現代 CPU。連續的資料有利於 CPU cache 的命中,減少 memory stall 現象;除此之外,透過 SIMD 指令一次處理多個資料,可以充分利用 CPU 的計算能力。


火山模型和向量化模型迭代資料的差異

向量化模型整體架構與火山模型類似,依然採用了拉取式模型。考慮一個具有 Id,Name 和 Age 三列的表 People, batch 將由 Id 的整型陣列、Name 的字串陣列以及 Age 的整型陣列組成,面對查詢 SELECT Name, (Age - 30) * 50 AS Bonus FROM People WHERE Age > 30; 其向量化模型大致下圖所示。

向量化模型


顯然向量化模型與列式儲存搭配使用可以獲得更好的效果,但非列式儲存也可以採用折中的方式來實現向量化模型。KaiwuDB 使用的是行儲存引擎,在其向量化執行模型中,在底層 Operator 中實現了多行到向量塊的轉化,上層的 Operator 則以向量塊作為輸入進行處理,最後再由頂層的 Operator 進行向量塊到行資料的轉化。

除此之外,為了避免上文提到的火山模型下由於 Filter 所使用標量表示式的通用性帶來的額外計算開銷,在 KaiwuDB 的向量化執行模型中,每個向量化 Operator 在執行期間不允許任何自由度或執行時選擇。

這意味著,對於資料型別、屬性和工作任務的任意組合,都有一個專門的 Operator 負責工作。 執行引擎從 Operator 鏈請求 batch :每個 Operator 從其子級 Operator 請求一個 batch ,執行其特定工作任務,然後將 batch 返回到其父級 Operator

因此對於示例查詢 SELECT Name, (Age - 30) * 50 AS Bonus FROM People WHERE Age > 30;   實際向量化模型比上述的內容更復雜,具體如下圖所示。

具體向量化模型

SelectIntGreaterThanInt Operator 在獲取 People 表 的 batch 後將選擇所有 Age 大於 30 的值;然後,這個新的 sel_age batch 將傳遞給 ProjectSubIntInt Operator,該 Operator 執行簡單的減法以生成 tmp batc h;最後,這個 tmp batch 被傳遞給 ProjectMultIntInt Operator,該 Operator 計算最終的 Bonus=(Age - 30)* 50。

為了具體實現這些向量化 Operator,KaiwuDB 將流程分解為單個列上的緊密 for 迴圈。以下的程式碼段(有刪減)實現了 SelectIntGreaterThanInt Operator 的部分功能。該函式從其子項 Operator 中檢索 batch,並迴圈訪問列的每個元素,同時將大於 30(p.constArg)   的值選中標記。然後,將 batch 及其選中向量返回給父級 Operator 進行進一步處理。這段程式碼雖然簡單但卻非常有效,for 迴圈迭代了一個 int64 的切片,將每個切片元素與另一個 int64 常量進行比較,並將結果儲存在另一個 int32 切片中,從而實現了一個快速的迴圈。

func (p *selGTInt64Int64Const0p) Next(ctx context,Context) coldata,Batch {
    // In order to inline the templated code of overloads, we need to have a
    // 'decimalScratch' local variable of type 'decimalOverloadScratch'.
    decimalScratch := p.decimalScratch
    // However, the scratch is not used in all of the selection operators, so
    // we add this to go around "unused" error.
    _ = decimalScratch
    for{
        batch := p.input.Next(ctx)
        if batch.Length() == 0 {
          return batch
    }
    vec := batch.ColVec(p.colIdx)
    col := vec.Int64()
    var idx int
    n := batch.Length()
    if sel := batch,Selection(); sel != nil {
      sel = sel[:n]
      for _, i := range sel {
        var cmp bool
        arg := col[i]
        {
          var cmpResult int
          {
            a, b := int64(arg), int64(p.constArg)
            if a < b {
              cmpResult = -1
            } else if a > b {
              cmpResult = 1 
            } else {
              cmpResult = 0
            }
          }
          cmp = cmpResult > 0
        }
        isNull := false
        if cmp && !isNull{
          sel[idx] = i
          idx++
        }
      }
    }
    if idx > 0 {
      batch.SetLength(idx)
      return batch
    }
  }
}

KaiwuDB 使用 kv 儲存引擎 rocksdb 作為底層儲存。透過從儲存讀取行後將行轉換為列式資料的 batch,然後將這些 batch 送到向量化執行引擎中處理,對於處理大量資料時有可觀的效能提升,但在資料量小時向量化是沒有優勢的,因為向量化的過程會帶來額外的開銷。

因此,面向行的執行模型可以為聯機事務處理(OLTP)查詢提供良好的效能,而向量化執行模式往往更適用於涉及海量資料的聯機分析處理(OLAP)查詢。KaiwuDB 在執行計劃時會根據估計的 tablereader 輸出的最大行數,與 SessionData 中的 VectorizeRowCountThreshold 欄位比較來判斷是否需要向量化執行。

KaiwuDB 預設開啟向量化執行引擎,使用者也可以選擇關閉。關閉和開啟向量化執行引擎可以透過 SET 進行設定,如下圖所示。除此之外,使用 EXPLAIN(VEC)語句可用於檢視查詢的向量化執行計劃。

透過 SET 設定關閉向量化執行引擎



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70027415/viewspace-2995170/,如需轉載,請註明出處,否則將追究法律責任。

相關文章