SQL 執行 - 執行器最佳化
本期技術貼主要介紹查詢執行引擎的最佳化。查詢執行引擎負責將 SQL 最佳化器生成的執行計劃進行解釋,透過任務排程執行從儲存引擎裡面把資料讀取出來,計算出結果集,然後返回給客戶。
在 關係型資料庫 發展的早期,受制於計算機 IO 能力的約束,計算在查詢整體的耗時佔比並不明顯,這個時候關注重點主要放在對於查詢的最佳化。最佳化器的好壞,對於執行計劃的優劣有著重要的意義,查詢執行引擎的作用在資料庫最佳化中對應等級是相對弱化的。但隨著計算機硬體的發展,查詢執行引擎也逐漸展現出他們 的重要地位。
本篇部落格結合 KaiwuDB 的部分原始碼和例項,介紹其如何充分發揮底層硬體的能力,最佳化查詢執行引擎,從而提升 資料庫系統 的效能。查詢執行引擎是否高效與其採用的模型有直接關係, 1990 年,論文 " Volcano, an Extensible and Parallel Query Evaluation System " 提出了火山模型,這也是 KaiwuDB 查詢執行引擎的基礎。
一、火山模型/迭代模型
火山模型作為經典的查詢執行模型被諸如 Oracle、MySQL 等主流關係數 據庫採用。該模型將關係代數中每一種運算元抽象為一個 Operator (迭代器),每個 Operator 都提供一個介面 Next(),呼叫該介面會返回該運算元產生/處理的一行資料(Tuple)。透過在查詢樹根節點自頂向下地呼叫 Next(),資料自底向上地被拉取處理,因而火山模型也稱為拉取執行模型(Pull Based)
火山模型
以一個兩表連線的查詢為例,讓我們留意圖中的第 ④ 步,Select 運算子。
// 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() 函式呼叫的簡化時序圖
查詢的計劃示例
三、向量化模型
不同於火山模型按行迭代的方式,如下圖所示,向量化模型採用批次迭代,在運算元間一次傳遞一批資料。透過更改資料方向(從行到列),把從列到元組的轉化推遲到較晚的時候執行,來更有效地利用現代 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/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Oracle 最佳化器與sql查詢執行順序OracleSQL
- SQL最佳化 —— 讀懂執行計劃SQL
- MyBatis SQL執行MyBatisSQL
- [ORACLE] SQL執行OracleSQL
- SQL的執行SQL
- PHP執行sqlPHPSQL
- 執行器
- 自適應查詢執行:在執行時提升Spark SQL執行效能SparkSQL
- Sql執行順序SQL
- peewee 執行原生 sqlSQL
- PostgreSQL SQL執行流程SQL
- sql 執行計劃SQL
- yii直接執行sqlSQL
- sql 執行過程SQL
- SingleThreadExecutor(單執行緒執行器)thread執行緒
- SQL最佳化案例-從執行計劃定位SQL問題(三)SQL
- Oracle中檢視已執行sql的執行計劃OracleSQL
- oracle查詢sql執行耗時、執行時間、sql_idOracleSQL
- MySQL 5.7獲取指定執行緒正在執行SQL的執行計劃資訊MySql執行緒
- Oracle - 執行過的SQL、正在執行的SQL、消耗資源最多的SQLOracleSQL
- java中執行sql與pl/sql dev中執行sql快慢差距大原因JavaSQLdev
- Java 併發:執行緒、執行緒池和執行器全面教程Java執行緒
- Java中命名執行器服務執行緒和執行緒池Java執行緒
- spark sql語句效能最佳化及執行計劃SparkSQL
- PL/SQL執行動態SQLSQL
- 分析執行計劃最佳化SQLORACLE的執行計劃(轉)SQLOracle
- 在 Docker 裡執行 Microsoft SQL 伺服器DockerROSSQL伺服器
- 分析執行計劃最佳化SQLORACLE的最佳化器(轉)SQLOracle
- ORACLE最佳化器工作原理及及執行方式Oracle
- sql最佳化:使用儲存提綱穩定sql執行計劃SQL
- postgresql怎麼執行sqlSQL
- mybatis執行sql指令碼MyBatisSQL指令碼
- Oracle sql執行計劃OracleSQL
- SQL是如何執行的SQL
- SQL 解析與執行流程SQL
- 用thinkphp執行原生sqlPHPSQL
- SQL Server執行計劃SQLServer
- SQL SERVER執行指令碼SQLServer指令碼