緊接上篇文章Apache Calcite 處理流程詳解(一),這里是 Calcite 系列文章的第二篇,后面還會有文章講述 Calcite 的實踐(包括:如何開發用於 SQL 優化的 Rule)。本篇文章主要介紹 Apache Calcite 優化器部分的內容,會先簡單介紹一下 RBO 和 CBO 模型,之后詳細講述 Calcite 關於這兩個優化器的實現 —— HepPlanner 和 VolcanoPlanner,文章內容都是個人的一些理解,由於也是剛接觸這塊,理解有偏差的地方,歡迎指正。
什么是查詢優化器
查詢優化器是傳統數據庫的核心模塊,也是大數據計算引擎的核心模塊,開源大數據引擎如 Impala、Presto、Drill、HAWQ、 Spark、Hive 等都有自己的查詢優化器。Calcite 就是從 Hive 的優化器演化而來的。
優化器的作用:將解析器生成的關系代數表達式轉換成執行計划,供執行引擎執行,在這個過程中,會應用一些規則優化,以幫助生成更高效的執行計划。
關於 Volcano 模型和 Cascades 模型的內容,建議看下相關的論文,這個是 Calcite 優化器的理論基礎,代碼只是把這個模型落地實現而已。
基於規則優化(RBO)
基於規則的優化器(Rule-Based Optimizer,RBO):根據優化規則對關系表達式進行轉換,這里的轉換是說一個關系表達式經過優化規則后會變成另外一個關系表達式,同時原有表達式會被裁剪掉,經過一系列轉換后生成最終的執行計划。
RBO 中包含了一套有着嚴格順序的優化規則,同樣一條 SQL,無論讀取的表中數據是怎么樣的,最后生成的執行計划都是一樣的。同時,在 RBO 中 SQL 寫法的不同很有可能影響最終的執行計划,從而影響執行計划的性能。
基於成本優化(CBO)
基於代價的優化器(Cost-Based Optimizer,CBO):根據優化規則對關系表達式進行轉換,這里的轉換是說一個關系表達式經過優化規則后會生成另外一個關系表達式,同時原有表達式也會保留,經過一系列轉換后會生成多個執行計划,然后 CBO 會根據統計信息和代價模型 (Cost Model) 計算每個執行計划的 Cost,從中挑選 Cost 最小的執行計划。
由上可知,CBO 中有兩個依賴:統計信息和代價模型。統計信息的准確與否、代價模型的合理與否都會影響 CBO 選擇最優計划。 從上述描述可知,CBO 是優於 RBO 的,原因是 RBO 是一種只認規則,對數據不敏感的呆板的優化器,而在實際過程中,數據往往是有變化的,通過 RBO 生成的執行計划很有可能不是最優的。事實上目前各大數據庫和大數據計算引擎都傾向於使用 CBO,但是對於流式計算引擎來說,使用 CBO 還是有很大難度的,因為並不能提前預知數據量等信息,這會極大地影響優化效果,CBO 主要還是應用在離線的場景。
優化規則
無論是 RBO,還是 CBO 都包含了一系列優化規則,這些優化規則可以對關系表達式進行等價轉換,常見的優化規則包含:
- 謂詞下推 Predicate Pushdown
- 常量折疊 Constant Folding
- 列裁剪 Column Pruning
- 其他
在 Calcite 的代碼里,有一個測試類(org.apache.calcite.test.RelOptRulesTest
)匯集了對目前內置所有 Rules 的測試 case,這個測試類可以方便我們了解各個 Rule 的作用。在這里有下面一條 SQL,通過這條語句來說明一下上面介紹的這三種規則。
select 10 + 30, users.name, users.age from users join jobs on users.id= user.id where users.age > 30 and jobs.id>10
謂詞下推(Predicate Pushdown)
關於謂詞下推,它主要還是從關系型數據庫借鑒而來,關系型數據中將謂詞下推到外部數據庫用以減少數據傳輸;屬於邏輯優化,優化器將謂詞過濾下推到數據源,使物理執行跳過無關數據。最常見的例子就是 join 與 filter 操作一起出現時,提前執行 filter 操作以減少處理的數據量,將 filter 操作下推,以上面例子為例,示意圖如下(對應 Calcite 中的 FilterJoinRule.FilterIntoJoinRule.FILTER_ON_JOIN
Rule):
在進行 join 前進行相應的過濾操作,可以極大地減少參加 join 的數據量。
常量折疊(Constant Folding)
常量折疊也是常見的優化策略,這個比較簡單、也很好理解,可以看下 編譯器優化 – 常量折疊 這篇文章,基本不用動腦筋就能理解,對於我們這里的示例,有一個常量表達式 10 + 30
,如果不進行常量折疊,那么每行數據都需要進行計算,進行常量折疊后的結果如下圖所示( 對應 Calcite 中的 ReduceExpressionsRule.PROJECT_INSTANCE
Rule):
列裁剪(Column Pruning)
列裁剪也是一個經典的優化規則,在本示例中對於jobs 表來說,並不需要掃描它的所有列值,而只需要列值 id,所以在掃描 jobs 之后需要將其他列進行裁剪,只留下列 id。這個優化帶來的好處很明顯,大幅度減少了網絡 IO、內存數據量的消耗。裁剪前后的示意圖如下(不過並沒有找到 Calcite 對應的 Rule):
Calcite 中的優化器實現
有了前面的基礎后,這里來看下 Calcite 中優化器的實現,RelOptPlanner 是 Calcite 中優化器的基類,其子類實現如下圖所示:
Calcite 中關於優化器提供了兩種實現:
- HepPlanner:就是前面 RBO 的實現,它是一個啟發式的優化器,按照規則進行匹配,直到達到次數限制(match 次數限制)或者遍歷一遍后不再出現 rule match 的情況才算完成;
- VolcanoPlanner:就是前面 CBO 的實現,它會一直迭代 rules,直到找到 cost 最小的 paln。
前面提到過像calcite這類查詢優化器最核心的兩個問題之一是怎么把優化規則應用到關系代數相關的RelNode Tree上。所以在閱讀calicite的代碼時就得帶着這個問題去看看它的實現過程,然后才能判斷它的代碼實現得是否優雅。
calcite的每種規則實現類(RelOptRule的子類)都會聲明自己應用在哪種RelNode子類上,每個RelNode子類其實都可以看成是一種operator(中文常翻譯成算子)。
VolcanoPlanner就是優化器,用的是動態規划算法,在創建VolcanoPlanner的實例后,通過calcite的標准jdbc接口執行sql時,默認會給這個VolcanoPlanner的實例注冊將近90條優化規則(還不算常量折疊這種最常見的優化),所以看代碼時,知道什么時候注冊可用的優化規則是第一步(調用VolcanoPlanner.addRule實現),這一步比較簡單。
接下來就是如何篩選規則了,當把語法樹轉成RelNode Tree后是沒有必要把前面注冊的90條優化規則都用上的,所以需要有個篩選的過程,因為每種規則是有應用范圍的,按RelNode Tree的不同節點類型就可以篩選出實際需要用到的優化規則了。這一步說起來很簡單,但在calcite的代碼實現里是相當復雜的,也是非常關鍵的一步,是從調用VolcanoPlanner.setRoot方法開始間接觸發的,如果只是靜態的看代碼不跑起來跟蹤調試多半摸不清它的核心流程的。篩選出來的優化規則會封裝成VolcanoRuleMatch,然后扔到RuleQueue里,而這個RuleQueue正是接下來執行動態規划算法要用到的核心類。篩選規則這一步的代碼實現很晦澀。
第三步才到VolcanoPlanner.findBestExp,本質上就是一個動態規划算法的實現,但是最值得關注的還是怎么用第二步篩選出來的規則對RelNode Tree進行變換,變換后的形式還是一棵RelNode Tree,最常見的是把LogicalXXX開頭的RelNode子類換成了EnumerableXXX或BindableXXX,總而言之,看看具體優化規則的實現就對了,都是繁瑣的體力活。
一個優化器,理解了上面所說的三步基本上就抓住重點了。
—— 來自【zhh-4096 】的微博
下面詳細講述一下這兩種 planner 在 Calcite 內部的具體實現。
HepPlanner
使用 HepPlanner 實現的完整代碼見 SqlHepTest。
HepPlanner 中的基本概念
這里先看下 HepPlanner 的一些基本概念,對於后面的理解很有幫助。
HepRelVertex
HepRelVertex 是對 RelNode 進行了簡單封裝。HepPlanner 中的所有節點都是 HepRelVertex,每個 HepRelVertex 都指向了一個真正的 RelNode 節點。
// org.apache.calcite.plan.hep.HepRelVertex /** * HepRelVertex wraps a real {@link RelNode} as a vertex in a DAG representing * the entire query expression. * note:HepRelVertex 將一個 RelNode 封裝為一個 DAG 中的 vertex(DAG 代表整個 query expression) */ public class HepRelVertex extends AbstractRelNode { //~ Instance fields -------------------------------------------------------- /** * Wrapped rel currently chosen for implementation of expression. */ private RelNode currentRel; }
HepInstruction
HepInstruction 是 HepPlanner 對一些內容的封裝,具體的子類實現比較多,其中 RuleInstance 是 HepPlanner 中對 Rule 的一個封裝,注冊的 Rule 最后都會轉換為這種形式。
HepInstruction represents one instruction in a HepProgram.
//org.apache.calcite.plan.hep.HepInstruction /** Instruction that executes a given rule. */ //note: 執行指定 rule 的 Instruction static class RuleInstance extends HepInstruction { /** * Description to look for, or null if rule specified explicitly. */ String ruleDescription; /** * Explicitly specified rule, or rule looked up by planner from * description. * note:設置其 Rule */ RelOptRule rule; void initialize(boolean clearCache) { if (!clearCache) { return; } if (ruleDescription != null) { // Look up anew each run. rule = null; } } void execute(HepPlanner planner) { planner.executeInstruction(this); } }
HepPlanner 處理流程
下面這個示例是上篇文章(Apache Calcite 處理流程詳解(一))的示例,通過這段代碼來看下 HepPlanner 的內部實現機制。
HepProgramBuilder builder = new HepProgramBuilder(); builder.addRuleInstance(FilterJoinRule.FilterIntoJoinRule.FILTER_ON_JOIN); //note: 添加 rule HepPlanner hepPlanner = new HepPlanner(builder.build()); hepPlanner.setRoot(relNode); relNode = hepPlanner.findBestExp();
上面的代碼總共分為三步:
- 初始化 HepProgram 對象;
- 初始化 HepPlanner 對象,並通過
setRoot()
方法將 RelNode 樹轉換成 HepPlanner 內部使用的 Graph; - 通過
findBestExp()
找到最優的 plan,規則的匹配都是在這里進行。
1. 初始化 HepProgram
這幾步代碼實現沒有太多需要介紹的地方,先初始化 HepProgramBuilder 也是為了后面初始化 HepProgram 做准備,HepProgramBuilder 主要也就是提供了一些配置設置和添加規則的方法等,常用的方法如下:
addRuleInstance()
:注冊相應的規則;addRuleCollection()
:這里是注冊一個規則集合,先把規則放在一個集合里,再注冊整個集合,如果規則多的話,一般是這種方式;addMatchLimit()
:設置 MatchLimit,這個 rule match 次數的最大限制;
HepProgram 這個類對於后面 HepPlanner 的優化很重要,它定義 Rule 匹配的順序,默認按【深度優先】順序,它可以提供以下幾種(見 HepMatchOrder 類):
- ARBITRARY:按任意順序匹配(因為它是有效的,而且大部分的 Rule 並不關心匹配順序);
- BOTTOM_UP:自下而上,先從子節點開始匹配;
- TOP_DOWN:自上而下,先從父節點開始匹配;
- DEPTH_FIRST:深度優先匹配,某些情況下比 ARBITRARY 高效(為了避免新的 vertex 產生后又從 root 節點開始匹配)。
這個匹配順序到底是什么呢?對於規則集合 rules,HepPlanner 的算法是:從一個節點開始,跟 rules 的所有 Rule 進行匹配,匹配上就進行轉換操作,這個節點操作完,再進行下一個節點,這里的匹配順序就是指的節點遍歷順序(這種方式的優劣,我們下面再說)。
2. HepPlanner.setRoot(RelNode –> Graph)
先看下 setRoot()
方法的實現:
// org.apache.calcite.plan.hep.HepPlanner public void setRoot(RelNode rel) { //note: 將 RelNode 轉換為 DAG 表示 root = addRelToGraph(rel); //note: 僅僅是在 trace 日志中輸出 Graph 信息 dumpGraph(); }
HepPlanner 會先將所有 relNode tree 轉化為 HepRelVertex,這時就構建了一個 Graph:將所有的 elNode 節點使用 Vertex 表示,Gragh 會記錄每個 HepRelVertex 的 input 信息,這樣就是構成了一張 graph。
在真正的實現時,遞歸逐漸將每個 relNode 轉換為 HepRelVertex,並在 graph
中記錄相關的信息,實現如下:
//org.apache.calcite.plan.hep.HepPlanner //note: 根據 RelNode 構建一個 Graph private HepRelVertex addRelToGraph( RelNode rel) { // Check if a transformation already produced a reference // to an existing vertex. //note: 檢查這個 rel 是否在 graph 中轉換了 if (graph.vertexSet().contains(rel)) { return (HepRelVertex) rel; } // Recursively add children, replacing this rel's inputs // with corresponding child vertices. //note: 遞歸地增加子節點,使用子節點相關的 vertices 代替 rel 的 input final List<RelNode> inputs = rel.getInputs(); final List<RelNode> newInputs = new ArrayList<>(); for (RelNode input1 : inputs) { HepRelVertex childVertex = addRelToGraph(input1); //note: 遞歸進行轉換 newInputs.add(childVertex); //note: 每個 HepRelVertex 只記錄其 Input } if (!Util.equalShallow(inputs, newInputs)) { //note: 不相等的情況下 RelNode oldRel = rel; rel = rel.copy(rel.getTraitSet(), newInputs); onCopy(oldRel, rel); } // Compute digest first time we add to DAG, // otherwise can't get equivVertex for common sub-expression //note: 計算 relNode 的 digest //note: Digest 的意思是: //note: A short description of this relational expression's type, inputs, and //note: other properties. The string uniquely identifies the node; another node //note: is equivalent if and only if it has the same value. rel.recomputeDigest(); // try to find equivalent rel only if DAG is allowed //note: 如果允許 DAG 的話,檢查是否有一個等價的 HepRelVertex,有的話直接返回 if (!noDag) { // Now, check if an equivalent vertex already exists in graph. String digest = rel.getDigest(); HepRelVertex equivVertex = mapDigestToVertex.get(digest); if (equivVertex != null) { //note: 已經存在 // Use existing vertex. return equivVertex; } } // No equivalence: create a new vertex to represent this rel. //note: 創建一個 vertex 代替 rel HepRelVertex newVertex = new HepRelVertex(rel); graph.addVertex(newVertex); //note: 記錄 Vertex updateVertex(newVertex, rel);//note: 更新相關的緩存,比如 mapDigestToVertex map for (RelNode input : rel.getInputs()) { //note: 設置 Edge graph.addEdge(newVertex, (HepRelVertex) input);//note: 記錄與整個 Vertex 先關的 input } nTransformations++; return newVertex; }
到這里 HepPlanner 需要的 gragh 已經構建完成,通過 DEBUG 方式也能看到此時 HepPlanner root 變量的內容:
3. HepPlanner findBestExp 規則優化
//org.apache.calcite.plan.hep.HepPlanner // implement RelOptPlanner //note: 優化器的核心,匹配規則進行優化 public RelNode findBestExp() { assert root != null; //note: 運行 HepProgram 算法(按 HepProgram 中的 instructions 進行相應的優化) executeProgram(mainProgram); // Get rid of everything except what's in the final plan. //note: 垃圾收集 collectGarbage(); return buildFinalPlan(root); //note: 返回最后的結果,還是以 RelNode 表示 }
主要的實現是在 executeProgram()
方法中,如下:
//org.apache.calcite.plan.hep.HepPlanner private void executeProgram(HepProgram program) { HepProgram savedProgram = currentProgram; //note: 保留當前的 Program currentProgram = program; currentProgram.initialize(program == mainProgram);//note: 如果是在同一個 Program 的話,保留上次 cache for (HepInstruction instruction : currentProgram.instructions) { instruction.execute(this); //note: 按 Rule 進行優化(會調用 executeInstruction 方法) int delta = nTransformations - nTransformationsLastGC; if (delta > graphSizeLastGC) { // The number of transformations performed since the last // garbage collection is greater than the number of vertices in // the graph at that time. That means there should be a // reasonable amount of garbage to collect now. We do it this // way to amortize garbage collection cost over multiple // instructions, while keeping the highwater memory usage // proportional to the graph size. //note: 進行轉換的次數已經大於 DAG Graph 中的頂點數,這就意味着已經產生大量垃圾需要進行清理 collectGarbage(); } } currentProgram = savedProgram; }
這里會遍歷 HepProgram 中 instructions(記錄注冊的所有 HepInstruction),然后根據 instruction 的類型執行相應的 executeInstruction()
方法,如果instruction 是 HepInstruction.MatchLimit
類型,會執行 executeInstruction(HepInstruction.MatchLimit instruction)
方法,這個方法就是初始化 matchLimit 變量。對於 HepInstruction.RuleInstance
類型的 instruction 會執行下面的方法(前面的示例注冊規則使用的是 addRuleInstance()
方法,所以返回的 rules 只有一個規則,如果注冊規則的時候使用的是 addRuleCollection()
方法注冊一個規則集合的話,這里會返回的 rules 就是那個規則集合):
//org.apache.calcite.plan.hep.HepPlanner //note: 執行相應的 RuleInstance void executeInstruction( HepInstruction.RuleInstance instruction) { if (skippingGroup()) { return; } if (instruction.rule == null) {//note: 如果 rule 為 null,那么就按照 description 查找具體的 rule assert instruction.ruleDescription != null; instruction.rule = getRuleByDescription(instruction.ruleDescription); LOGGER.trace("Looking up rule with description {}, found {}", instruction.ruleDescription, instruction.rule); } //note: 執行相應的 rule if (instruction.rule != null) { applyRules( Collections.singleton(instruction.rule), true); } }
接下來看 applyRules()
的實現:
//org.apache.calcite.plan.hep.HepPlanner //note: 執行 rule(forceConversions 默認 true) private void applyRules( Collection<RelOptRule> rules, boolean forceConversions) { if (currentProgram.group != null) { assert currentProgram.group.collecting; currentProgram.group.ruleSet.addAll(rules); return; } LOGGER.trace("Applying rule set {}", rules); //note: 當遍歷規則是 ARBITRARY 或 DEPTH_FIRST 時,設置為 false,此時不會從 root 節點開始,否則每次 restart 都從 root 節點開始 boolean fullRestartAfterTransformation = currentProgram.matchOrder != HepMatchOrder.ARBITRARY && currentProgram.matchOrder != HepMatchOrder.DEPTH_FIRST; int nMatches = 0; boolean fixedPoint; //note: 兩種情況會跳出循環,一種是達到 matchLimit 限制,一種是遍歷一遍不會再有新的 transform 產生 do { //note: 按照遍歷規則獲取迭代器 Iterator<HepRelVertex> iter = getGraphIterator(root); fixedPoint = true; while (iter.hasNext()) { HepRelVertex vertex = iter.next();//note: 遍歷每個 HepRelVertex for (RelOptRule rule : rules) {//note: 遍歷每個 rules //note: 進行規制匹配,也是真正進行相關操作的地方 HepRelVertex newVertex = applyRule(rule, vertex, forceConversions); if (newVertex == null || newVertex == vertex) { continue; } ++nMatches; //note: 超過 MatchLimit 的限制 if (nMatches >= currentProgram.matchLimit) { return; } if (fullRestartAfterTransformation) { //note: 發生 transformation 后,從 root 節點再次開始 iter = getGraphIterator(root); } else { // To the extent possible, pick up where we left // off; have to create a new iterator because old // one was invalidated by transformation. //note: 盡可能從上次進行后的節點開始 iter = getGraphIterator(newVertex); if (currentProgram.matchOrder == HepMatchOrder.DEPTH_FIRST) { //note: 這樣做的原因就是為了防止有些 HepRelVertex 遺漏了 rule 的匹配(每次從 root 開始是最簡單的算法),因為可能出現下推 nMatches = depthFirstApply(iter, rules, forceConversions, nMatches); if (nMatches >= currentProgram.matchLimit) { return; } } // Remember to go around again since we're // skipping some stuff. //note: 再來一遍,因為前面有跳過一些節點 fixedPoint = false; } break; } } } while (!fixedPoint); }
在這里會調用 getGraphIterator()
方法獲取 HepRelVertex 的迭代器,迭代的策略(遍歷的策略)跟前面說的順序有關,默認使用的是【深度優先】,這段代碼比較簡單,就是遍歷規則+遍歷節點進行匹配轉換,直到滿足條件再退出,從這里也能看到 HepPlanner 的實現效率不是很高,它也無法保證能找出最優的結果。
總結一下,HepPlanner 在優化過程中,是先遍歷規則,然后再對每個節點進行匹配轉換,直到滿足條件(超過限制次數或者規則遍歷完一遍不會再有新的變化),其方法調用流程如下:
思考
1. 為什么要把 RelNode 轉換 HepRelVertex 進行優化?帶來的收益在哪里?
關於這個,能想到的就是:RelNode 是底層提供的抽象、偏底層一些,在優化器這一層,需要記錄更多的信息,所以又做了一層封裝。
VolcanoPlanner
介紹完 HepPlanner 之后,接下來再來看下基於成本優化(CBO)模型在 Calcite 中是如何實現、如何落地的,關於 Volcano 理論內容建議先看下相關理論知識,否則直接看實現的話可能會有一些頭大。從 Volcano 模型的理論落地到實踐是有很大區別的,這里先看一張 VolcanoPlanner 整體實現圖,如下所示(圖片來自 Cost-based Query Optimization in Apache Phoenix using Apache Calcite):
上面基本展現了 VolcanoPlanner 內部實現的流程,也簡單介紹了 VolcanoPlanner 在實現中的一些關鍵點(有些概念暫時不了解也不要緊,后面會介紹):
- Add Rule matches to Queue:向 Rule Match Queue 中添加相應的 Rule Match;
- Apply Rule match transformations to plan gragh:應用 Rule Match 對 plan graph 做 transformation 優化(Rule specifies an Operator sub-graph to match and logic to generate equivalent better sub-graph);
- Iterate for fixed iterations or until cost doesn’t change:進行相應的迭代,直到 cost 不再變化或者 Rule Match Queue 中 rule match 已經全部應用完成;
- Match importance based on cost of RelNode and height:Rule Match 的 importance 依賴於 RelNode 的 cost 和深度。
使用 VolcanoPlanner 實現的完整代碼見 SqlVolcanoTest。
下面來看下 VolcanoPlanner 實現具體的細節。
VolcanoPlanner 中的基本概念
VolcanoPlanner 在實現中引入了一些基本概念,先明白這些概念對於理解 VolcanoPlanner 的實現非常有幫助。
RelSet
關於 RelSet,源碼中介紹如下:
RelSet is an equivalence-set of expressions that is, a set of expressions which have identical semantics.
We are generally interested in using the expression which has the lowest cost.
All of the expressions in an RelSet have the same calling convention.
它有以下特點:
- 描述一組等價 Relation Expression,所有的 RelNode 會記錄在
rels
中; - have the same calling convention;
- 具有相同物理屬性的 Relational Expression 會記錄在其成員變量
List<RelSubset> subsets
中.
RelSet 中比較重要成員變量如下:
class RelSet { // 記錄屬於這個 RelSet 的所有 RelNode final List<RelNode> rels = new ArrayList<>(); /** * Relational expressions that have a subset in this set as a child. This * is a multi-set. If multiple relational expressions in this set have the * same parent, there will be multiple entries. */ final List<RelNode> parents = new ArrayList<>(); //note: 具體相同物理屬性的子集合(本質上 RelSubset 並不記錄 RelNode,也是通過 RelSet 按物理屬性過濾得到其 RelNode 子集合,見下面的 RelSubset 部分) final List<RelSubset> subsets = new ArrayList<>(); /** * List of {@link AbstractConverter} objects which have not yet been * satisfied. */ final List<AbstractConverter> abstractConverters = new ArrayList<>(); /** * Set to the superseding set when this is found to be equivalent to another * set. * note:當發現與另一個 RelSet 有相同的語義時,設置為替代集合 */ RelSet equivalentSet; RelNode rel; /** * Variables that are set by relational expressions in this set and available for use by parent and child expressions. * note:在這個集合中 relational expression 設置的變量,父類和子類 expression 可用的變量 */ final Set<CorrelationId> variablesPropagated; /** * Variables that are used by relational expressions in this set. * note:在這個集合中被 relational expression 使用的變量 */ final Set<CorrelationId> variablesUsed; final int id; /** * Reentrancy flag. */ boolean inMetadataQuery; }
RelSubset
關於 RelSubset,源碼中介紹如下:
Subset of an equivalence class where all relational expressions have the same physical properties.
它的特點如下:
- 描述一組物理屬性相同的等價 Relation Expression,即它們具有相同的 Physical Properties;
- 每個 RelSubset 都會記錄其所屬的 RelSet;
- RelSubset 繼承自 AbstractRelNode,它也是一種 RelNode,物理屬性記錄在其成員變量 traitSet 中。
RelSubset 一些比較重要的成員變量如下:
public class RelSubset extends AbstractRelNode { /** * cost of best known plan (it may have improved since) * note: 已知最佳 plan 的 cost */ RelOptCost bestCost; /** * The set this subset belongs to. * RelSubset 所屬的 RelSet,在 RelSubset 中並不記錄具體的 RelNode,直接記錄在 RelSet 的 rels 中 */ final RelSet set; /** * best known plan * note: 已知的最佳 plan */ RelNode best; /** * Flag indicating whether this RelSubset's importance was artificially * boosted. * note: 標志這個 RelSubset 的 importance 是否是人為地提高了 */ boolean boosted; //~ Constructors ----------------------------------------------------------- RelSubset( RelOptCluster cluster, RelSet set, RelTraitSet traits) { super(cluster, traits); // 繼承自 AbstractRelNode,會記錄其相應的 traits 信息 this.set = set; this.boosted = false; assert traits.allSimple(); computeBestCost(cluster.getPlanner()); //note: 計算 best recomputeDigest(); //note: 計算 digest } }
每個 RelSubset 都將會記錄其最佳 plan(best
)和最佳 plan 的 cost(bestCost
)信息。
RuleMatch
RuleMatch 是這里對 Rule 和 RelSubset 關系的一個抽象,它會記錄這兩者的信息。
A match of a rule to a particular set of target relational expressions, frozen in time.
importance
importance 決定了在進行 Rule 優化時 Rule 應用的順序,它是一個相對概念,在 VolcanoPlanner 中有兩個 importance,分別是 RelSubset 和 RuleMatch 的 importance,這里先提前介紹一下。
RelSubset 的 importance
RelSubset importance 計算方法見其 api 定義(圖中的 sum 改成 Math.max{}這個地方有誤):
舉個例子:假設一個 RelSubset(記為 s0) 的 cost 是3,對應的 importance 是0.5,這個 RelNode 有兩個輸入(inputs),對應的 RelSubset 記為 s1、s2(假設 s1、s2 不再有輸入 RelNode),其 cost 分別為 2和5,那么 s1 的 importance 為
Importance of s1 = 23+2+5 ⋅
0.5 = 0.1
Importance of s2
= 53+2+5 ⋅
0.5 = 0.25
其中,2代表的是 s1
的 cost,3+2+5 代表的是 s0 的 cost(本節點的 cost 加上其所有 input 的 cost)。下面看下其具體的代碼實現(調用 RuleQueue 中的 recompute()
計算其 importance):
//org.apache.calcite.plan.volcano.RuleQueue /** * Recomputes the importance of the given RelSubset. * note:重新計算指定的 RelSubset 的 importance * note:如果為 true,即使 subset 沒有注冊,也會強制 importance 更新 * * @param subset RelSubset whose importance is to be recomputed * @param force if true, forces an importance update even if the subset has * not been registered */ public void recompute(RelSubset subset, boolean force) { Double previousImportance = subsetImportances.get(subset); if (previousImportance == null) { //note: subset 還沒有注冊的情況下 if (!force) { //note: 如果不是強制,可以直接先返回 // Subset has not been registered yet. Don't worry about it. return; } previousImportance = Double.NEGATIVE_INFINITY; } //note: 計算器 importance 值 double importance = computeImportance(subset); if (previousImportance == importance) { return; } //note: 緩存中更新其 importance updateImportance(subset, importance); } // 計算一個節點的 importance double computeImportance(RelSubset subset) { double importance; if (subset == planner.root) { // The root always has importance = 1 //note: root RelSubset 的 importance 為1 importance = 1.0; } else { final RelMetadataQuery mq = subset.getCluster().getMetadataQuery(); // The importance of a subset is the max of its importance to its // parents //note: 計算其相對於 parent 的最大 importance,多個 parent 的情況下,選擇一個最大值 importance = 0.0; for (RelSubset parent : subset.getParentSubsets(planner)) { //note: 計算這個 RelSubset 相對於 parent 的 importance final double childImportance = computeImportanceOfChild(mq, subset, parent); //note: 選擇最大的 importance importance = Math.max(importance, childImportance); } } LOGGER.trace("Importance of [{}] is {}", subset, importance); return importance; } //note:根據 cost 計算 child 相對於 parent 的 importance(這是個相對值) private double computeImportanceOfChild(RelMetadataQuery mq, RelSubset child, RelSubset parent) { //note: 獲取 parent 的 importance final double parentImportance = getImportance(parent); //note: 獲取對應的 cost 信息 final double childCost = toDouble(planner.getCost(child, mq)); final double parentCost = toDouble(planner.getCost(parent, mq)); double alpha = childCost / parentCost; if (alpha >= 1.0) { // child is always less important than parent alpha = 0.99; } //note: 根據 cost 比列計算其 importance final double importance = parentImportance * alpha; LOGGER.trace("Importance of [{}] to its parent [{}] is {} (parent importance={}, child cost={}," + " parent cost={})", child, parent, importance, parentImportance, childCost, parentCost); return importance; }
在 computeImportanceOfChild()
中計算 RelSubset 相對於 parent RelSubset 的 importance 時,一個比較重要的地方就是如何計算 cost,關於 cost 的計算見:
//org.apache.calcite.plan.volcano.VolcanoPlanner //note: Computes the cost of a RelNode. public RelOptCost getCost(RelNode rel, RelMetadataQuery mq) { assert rel != null : "pre-condition: rel != null"; if (rel instanceof RelSubset) { //note: 如果是 RelSubset,證明是已經計算 cost 的 subset return ((RelSubset) rel).bestCost; } if (rel.getTraitSet().getTrait(ConventionTraitDef.INSTANCE) == Convention.NONE) { return costFactory.makeInfiniteCost(); //note: 這種情況下也會返回 infinite Cost } //note: 計算其 cost RelOptCost cost = mq.getNonCumulativeCost(rel); if (!zeroCost.isLt(cost)) { //note: cost 比0還小的情況 // cost must be positive, so nudge it cost = costFactory.makeTinyCost(); } //note: RelNode 的 cost 會把其 input 全部加上 for (RelNode input : rel.getInputs()) { cost = cost.plus(getCost(input, mq)); } return cost; }
上面就是 RelSubset importance 計算的代碼實現,從實現中可以發現這個特點:
- 越靠近 root 的 RelSubset,其 importance 越大,這個帶來的好處就是在優化時,會盡量先優化靠近 root 的 RelNode,這樣帶來的收益也會最大。
RuleMatch 的 importance
RuleMatch 的 importance 定義為以下兩個中比較大的一個(如果對應的 RelSubset 有 importance 的情況下):
- 這個 RuleMatch 對應 RelSubset(這個 rule match 的 RelSubset)的 importance;
- 輸出的 RelSubset(taget RelSubset)的 importance(如果這個 RelSubset 在 VolcanoPlanner 的緩存中存在的話)。
//org.apache.calcite.plan.volcano.VolcanoRuleMatch /** * Computes the importance of this rule match. * note:計算 rule match 的 importance * * @return importance of this rule match */ double computeImportance() { assert rels[0] != null; //note: rels[0] 這個 Rule Match 對應的 RelSubset RelSubset subset = volcanoPlanner.getSubset(rels[0]); double importance = 0; if (subset != null) { //note: 獲取 RelSubset 的 importance importance = volcanoPlanner.ruleQueue.getImportance(subset); } //note: Returns a guess as to which subset the result of this rule will belong to. final RelSubset targetSubset = guessSubset(); if ((targetSubset != null) && (targetSubset != subset)) { // If this rule will generate a member of an equivalence class // which is more important, use that importance. //note: 獲取 targetSubset 的 importance final double targetImportance = volcanoPlanner.ruleQueue.getImportance(targetSubset); if (targetImportance > importance) { importance = targetImportance; // If the equivalence class is cheaper than the target, bump up // the importance of the rule. A converter is an easy way to // make the plan cheaper, so we'd hate to miss this opportunity. // // REVIEW: jhyde, 2007/12/21: This rule seems to make sense, but // is disabled until it has been proven. // // CHECKSTYLE: IGNORE 3 if ((subset != null) && subset.bestCost.isLt(targetSubset.bestCost) && false) { //note: 肯定不會進入 importance *= targetSubset.bestCost.divideBy(subset.bestCost); importance = Math.min(importance, 0.99); } } } return importance; }
RuleMatch 的 importance 主要是決定了在選擇 RuleMatch 時,應該先處理哪一個?它本質上還是直接用的 RelSubset 的 importance。
VolcanoPlanner 處理流程
還是以前面的示例,只不過這里把優化器換成 VolcanoPlanner 來實現,通過這個示例來詳細看下 VolcanoPlanner 內部的實現邏輯。
//1. 初始化 VolcanoPlanner 對象,並添加相應的 Rule VolcanoPlanner planner = new VolcanoPlanner(); planner.addRelTraitDef(ConventionTraitDef.INSTANCE); planner.addRelTraitDef(RelDistributionTraitDef.INSTANCE); // 添加相應的 rule planner.addRule(FilterJoinRule.FilterIntoJoinRule.FILTER_ON_JOIN); planner.addRule(ReduceExpressionsRule.PROJECT_INSTANCE); planner.addRule(PruneEmptyRules.PROJECT_INSTANCE); // 添加相應的 ConverterRule planner.addRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE); planner.addRule(EnumerableRules.ENUMERABLE_SORT_RULE); planner.addRule(EnumerableRules.ENUMERABLE_VALUES_RULE); planner.addRule(EnumerableRules.ENUMERABLE_PROJECT_RULE); planner.addRule(EnumerableRules.ENUMERABLE_FILTER_RULE); //2. Changes a relational expression to an equivalent one with a different set of traits. RelTraitSet desiredTraits = relNode.getCluster().traitSet().replace(EnumerableConvention.INSTANCE); relNode = planner.changeTraits(relNode, desiredTraits); //3. 通過 VolcanoPlanner 的 setRoot 方法注冊相應的 RelNode,並進行相應的初始化操作 planner.setRoot(relNode); //4. 通過動態規划算法找到 cost 最小的 plan relNode = planner.findBestExp();
優化后的結果為:
EnumerableSort(sort0=[$0], dir0=[ASC]) EnumerableProject(USER_ID=[$0], USER_NAME=[$1], USER_COMPANY=[$5], USER_AGE=[$2]) EnumerableMergeJoin(condition=[=($0, $3)], joinType=[inner]) EnumerableFilter(condition=[>($2, 30)]) EnumerableTableScan(table=[[USERS]]) EnumerableFilter(condition=[>($0, 10)]) EnumerableTableScan(table=[[JOBS]])
在應用 VolcanoPlanner 時,整體分為以下四步:
- 初始化 VolcanoPlanner,並添加相應的 Rule(包括 ConverterRule);
- 對 RelNode 做等價轉換,這里只是改變其物理屬性(
Convention
); - 通過 VolcanoPlanner 的
setRoot()
方法注冊相應的 RelNode,並進行相應的初始化操作; - 通過動態規划算法找到 cost 最小的 plan;
下面來分享一下上面的詳細流程。
1. VolcanoPlanner 初始化
在這里總共有三步,分別是 VolcanoPlanner 初始化,addRelTraitDef()
添加 RelTraitDef,addRule()
添加 rule,先看下 VolcanoPlanner 的初始化:
//org.apache.calcite.plan.volcano.VolcanoPlanner /** * Creates a uninitialized <code>VolcanoPlanner</code>. To fully initialize it, the caller must register the desired set of relations, rules, and calling conventions. * note: 創建一個沒有初始化的 VolcanoPlanner,如果要進行初始化,調用者必須注冊 set of relations、rules、calling conventions. */ public VolcanoPlanner() { this(null, null); } /** * Creates a {@code VolcanoPlanner} with a given cost factory. * note: 創建 VolcanoPlanner 實例,並制定 costFactory(默認為 VolcanoCost.FACTORY) */ public VolcanoPlanner(RelOptCostFactory costFactory, // Context externalContext) { super(costFactory == null ? VolcanoCost.FACTORY : costFactory, // externalContext); this.zeroCost = this.costFactory.makeZeroCost(); }
這里其實並沒有做什么,只是做了一些簡單的初始化,如果要想設置相應 RelTraitDef 的話,需要調用 addRelTraitDef()
進行添加,其實現如下:
//org.apache.calcite.plan.volcano.VolcanoPlanner //note: 添加 RelTraitDef @Override public boolean addRelTraitDef(RelTraitDef relTraitDef) { return !traitDefs.contains(relTraitDef) && traitDefs.add(relTraitDef); }
如果要給 VolcanoPlanner 添加 Rule 的話,需要調用 addRule()
進行添加,在這個方法里重點做的一步是將具體的 RelNode 與 RelOptRuleOperand 之間的關系記錄下來,記錄到 classOperands
中,相當於在優化時,哪個 RelNode 可以應用哪些 Rule 都是記錄在這個緩存里的。其實現如下:
//org.apache.calcite.plan.volcano.VolcanoPlanner //note: 添加 rule public boolean addRule(RelOptRule rule) { if (locked) { return false; } if (ruleSet.contains(rule)) { // Rule already exists. return false; } final boolean added = ruleSet.add(rule); assert added; final String ruleName = rule.toString(); //note: 這里的 ruleNames 允許重復的 key 值,但是這里還是要求 rule description 保持唯一的,與 rule 一一對應 if (ruleNames.put(ruleName, rule.getClass())) { Set<Class> x = ruleNames.get(ruleName); if (x.size() > 1) { throw new RuntimeException("Rule description '" + ruleName + "' is not unique; classes: " + x); } } //note: 注冊一個 rule 的 description(保存在 mapDescToRule 中) mapRuleDescription(rule); // Each of this rule's operands is an 'entry point' for a rule call. Register each operand against all concrete sub-classes that could match it. //note: 記錄每個 sub-classes 與 operand 的關系(如果能 match 的話,就記錄一次)。一個 RelOptRuleOperand 只會有一個 class 與之對應,這里找的是 subclass for (RelOptRuleOperand operand : rule.getOperands()) { for (Class<? extends RelNode> subClass : subClasses(operand.getMatchedClass())) { classOperands.put(subClass, operand); } } // If this is a converter rule, check that it operates on one of the // kinds of trait we are interested in, and if so, register the rule // with the trait. //note: 對於 ConverterRule 的操作,如果其 ruleTraitDef 類型包含在我們初始化的 traitDefs 中, //note: 就注冊這個 converterRule 到 ruleTraitDef 中 //note: 如果不包含 ruleTraitDef,這個 ConverterRule 在本次優化的過程中是用不到的 if (rule instanceof ConverterRule) { ConverterRule converterRule = (ConverterRule) rule; final RelTrait ruleTrait = converterRule.getInTrait(); final RelTraitDef ruleTraitDef = ruleTrait.getTraitDef(); if (traitDefs.contains(ruleTraitDef)) { //note: 這里注冊好像也沒有用到 ruleTraitDef.registerConverterRule(this, converterRule); } } return true; }
2. RelNode changeTraits
這里分為兩步:
- 通過 RelTraitSet 的
replace()
方法,將 RelTraitSet 中對應的 RelTraitDef 做對應的更新,其他的 RelTrait 不變; - 這一步簡單來說就是:Changes a relational expression to an equivalent one with a different set of traits,對相應的 RelNode 做 converter 操作,這里實際上也會做很多的內容,這部分會放在第三步講解,主要是
registerImpl()
方法的實現。
3. VolcanoPlanner setRoot
VolcanoPlanner 會調用 setRoot()
方法注冊相應的 Root RelNode,並進行一系列 Volcano 必須的初始化操作,很多的操作都是在這里實現的,這里來詳細看下其實現。
//org.apache.calcite.plan.volcano.VolcanoPlanner public void setRoot(RelNode rel) { // We're registered all the rules, and therefore RelNode classes, // we're interested in, and have not yet started calling metadata providers. // So now is a good time to tell the metadata layer what to expect. registerMetadataRels(); //note: 注冊相應的 RelNode,會做一系列的初始化操作, RelNode 會有對應的 RelSubset this.root = registerImpl(rel, null); if (this.originalRoot == null) { this.originalRoot = rel; } // Making a node the root changes its importance. //note: 重新計算 root subset 的 importance this.ruleQueue.recompute(this.root); //Ensures that the subset that is the root relational expression contains converters to all other subsets in its equivalence set. ensureRootConverters(); }
對於 setRoot()
方法來說,核心的處理流程是在 registerImpl()
方法中,在這個方法會進行相應的初始化操作(包括 RelNode 到 RelSubset 的轉換、計算 RelSubset 的 importance 等),其他的方法在上面有相應的備注,這里我們看下 registerImpl()
具體做了哪些事情:
//org.apache.calcite.plan.volcano.VolcanoPlanner /** * Registers a new expression <code>exp</code> and queues up rule matches. * If <code>set</code> is not null, makes the expression part of that * equivalence set. If an identical expression is already registered, we * don't need to register this one and nor should we queue up rule matches. * * note:注冊一個新的 expression;對 rule match 進行排隊; * note:如果 set 不為 null,那么就使 expression 成為等價集合(RelSet)的一部分 * note:rel:必須是 RelSubset 或者未注冊的 RelNode * @param rel relational expression to register. Must be either a * {@link RelSubset}, or an unregistered {@link RelNode} * @param set set that rel belongs to, or <code>null</code> * @return the equivalence-set */ private RelSubset registerImpl( RelNode rel, RelSet set) { if (rel instanceof RelSubset) { //note: 如果是 RelSubset 類型,已經注冊過了 return registerSubset(set, (RelSubset) rel); //note: 做相應的 merge } assert !isRegistered(rel) : "already been registered: " + rel; if (rel.getCluster().getPlanner() != this) { //note: cluster 中 planner 與這里不同 throw new AssertionError("Relational expression " + rel + " belongs to a different planner than is currently being used."); } // Now is a good time to ensure that the relational expression // implements the interface required by its calling convention. //note: 確保 relational expression 可以實施其 calling convention 所需的接口 //note: 獲取 RelNode 的 RelTraitSet final RelTraitSet traits = rel.getTraitSet(); //note: 獲取其 ConventionTraitDef final Convention convention = traits.getTrait(ConventionTraitDef.INSTANCE); assert convention != null; if (!convention.getInterface().isInstance(rel) && !(rel instanceof Converter)) { throw new AssertionError("Relational expression " + rel + " has calling-convention " + convention + " but does not implement the required interface '" + convention.getInterface() + "' of that convention"); } if (traits.size() != traitDefs.size()) { throw new AssertionError("Relational expression " + rel + " does not have the correct number of traits: " + traits.size() + " != " + traitDefs.size()); } // Ensure that its sub-expressions are registered. //note: 其實現在 AbstractRelNode 對應的方法中,實際上調用的還是 ensureRegistered 方法進行注冊 //note: 將 RelNode 的所有 inputs 注冊到 planner 中 //note: 這里會遞歸調用 registerImpl 注冊 relNode 與 RelSet,直到其 inputs 全部注冊 //note: 返回的是一個 RelSubset 類型 rel = rel.onRegister(this); // Record its provenance. (Rule call may be null.) //note: 記錄 RelNode 的來源 if (ruleCallStack.isEmpty()) { //note: 不知道來源時 provenanceMap.put(rel, Provenance.EMPTY); } else { //note: 來自 rule 觸發的情況 final VolcanoRuleCall ruleCall = ruleCallStack.peek(); provenanceMap.put( rel, new RuleProvenance( ruleCall.rule, ImmutableList.copyOf(ruleCall.rels), ruleCall.id)); } // If it is equivalent to an existing expression, return the set that // the equivalent expression belongs to. //note: 根據 RelNode 的 digest(摘要,全局唯一)判斷其是否已經有對應的 RelSubset,有的話直接放回 String key = rel.getDigest(); RelNode equivExp = mapDigestToRel.get(key); if (equivExp == null) { //note: 還沒注冊的情況 // do nothing } else if (equivExp == rel) {//note: 已經有其緩存信息 return getSubset(rel); } else { assert RelOptUtil.equal( "left", equivExp.getRowType(), "right", rel.getRowType(), Litmus.THROW); RelSet equivSet = getSet(equivExp); //note: 有 RelSubset 但對應的 RelNode 不同時,這里對其 RelSet 做下 merge if (equivSet != null) { LOGGER.trace( "Register: rel#{} is equivalent to {}", rel.getId(), equivExp.getDescription()); return registerSubset(set, getSubset(equivExp)); } } //note: Converters are in the same set as their children. if (rel instanceof Converter) { final RelNode input = ((Converter) rel).getInput(); final RelSet childSet = getSet(input); if ((set != null) && (set != childSet) && (set.equivalentSet == null)) { LOGGER.trace( "Register #{} {} (and merge sets, because it is a conversion)", rel.getId(), rel.getDigest()); merge(set, childSet); registerCount++; // During the mergers, the child set may have changed, and since // we're not registered yet, we won't have been informed. So // check whether we are now equivalent to an existing // expression. if (fixUpInputs(rel)) { rel.recomputeDigest(); key = rel.getDigest(); RelNode equivRel = mapDigestToRel.get(key); if ((equivRel != rel) && (equivRel != null)) { assert RelOptUtil.equal( "rel rowtype", rel.getRowType(), "equivRel rowtype", equivRel.getRowType(), Litmus.THROW); // make sure this bad rel didn't get into the // set in any way (fixupInputs will do this but it // doesn't know if it should so it does it anyway) set.obliterateRelNode(rel); // There is already an equivalent expression. Use that // one, and forget about this one. return getSubset(equivRel); } } } else { set = childSet; } } // Place the expression in the appropriate equivalence set. //note: 把 expression 放到合適的 等價集 中 //note: 如果 RelSet 不存在,這里會初始化一個 RelSet if (set == null) { set = new RelSet( nextSetId++, Util.minus( RelOptUtil.getVariablesSet(rel), rel.getVariablesSet()), RelOptUtil.getVariablesUsed(rel)); this.allSets.add(set); } // Chain to find 'live' equivalent set, just in case several sets are // merging at the same time. //note: 遞歸查詢,一直找到最開始的 語義相等的集合,防止不同集合同時被 merge while (set.equivalentSet != null) { set = set.equivalentSet; } // Allow each rel to register its own rules. registerClass(rel); registerCount++; //note: 初始時是 0 final int subsetBeforeCount = set.subsets.size(); //note: 向等價集中添加相應的 RelNode,並更新其 best 信息 RelSubset subset = addRelToSet(rel, set); //note: 緩存相關信息,返回的 key 之前對應的 value final RelNode xx = mapDigestToRel.put(key, rel); assert xx == null || xx == rel : rel.getDigest(); LOGGER.trace("Register {} in {}", rel.getDescription(), subset.getDescription()); // This relational expression may have been registered while we // recursively registered its children. If this is the case, we're done. if (xx != null) { return subset; } // Create back-links from its children, which makes children more // important. //note: 如果是 root,初始化其 importance 為 1.0 if (rel == this.root) { ruleQueue.subsetImportances.put( subset, 1.0); // todo: remove } //note: 將 Rel 的 input 對應的 RelSubset 的 parents 設置為當前的 Rel //note: 也就是說,一個 RelNode 的 input 為其對應 RelSubset 的 children 節點 for (RelNode input : rel.getInputs()) { RelSubset childSubset = (RelSubset) input; childSubset.set.parents.add(rel); // Child subset is more important now a new parent uses it. //note: 重新計算 RelSubset 的 importance ruleQueue.recompute(childSubset); } if (rel == this.root) {// TODO: 2019-03-11 這里為什么要刪除呢? ruleQueue.subsetImportances.remove(subset); } // Remember abstract converters until they're satisfied //note: 如果是 AbstractConverter 示例,添加到 abstractConverters 集合中 if (rel instanceof AbstractConverter) { set.abstractConverters.add((AbstractConverter) rel); } // If this set has any unsatisfied converters, try to satisfy them. //note: check set.abstractConverters checkForSatisfiedConverters(set, rel); // Make sure this rel's subset importance is updated //note: 強制更新(重新計算) subset 的 importance ruleQueue.recompute(subset, true); //note: 觸發所有匹配的 rule,這里是添加到對應的 RuleQueue 中 // Queue up all rules triggered by this relexp's creation. fireRules(rel, true); // It's a new subset. //note: 如果是一個 new subset,再做一次觸發 if (set.subsets.size() > subsetBeforeCount) { fireRules(subset, true); } return subset; }
registerImpl()
處理流程比較復雜,其方法實現,可以簡單總結為以下幾步:
- 在經過最上面的一些驗證之后,會通過
rel.onRegister(this)
這步操作,遞歸地調用 VolcanoPlanner 的ensureRegistered()
方法對其inputs
RelNode 進行注冊,最后還是調用registerImpl()
方法先注冊葉子節點,然后再父節點,最后到根節點; - 根據 RelNode 的 digest 信息(一般這個對於 RelNode 來說是全局唯一的),判斷其是否已經存在
mapDigestToRel
緩存中,如果存在的話,那么判斷會 RelNode 是否相同,如果相同的話,證明之前已經注冊過,直接通過getSubset()
返回其對應的 RelSubset 信息,否則就對其 RelSubset 做下 merge; - 如果 RelNode 對應的 RelSet 為 null,這里會新建一個 RelSet,並通過
addRelToSet()
將 RelNode 添加到 RelSet 中,並且更新 VolcanoPlanner 的mapRel2Subset
緩存記錄(RelNode 與 RelSubset 的對應關系),在addRelToSet()
的最后還會更新 RelSubset 的 best plan 和 best cost(每當往一個 RelSubset 添加相應的 RelNode 時,都會判斷這個 RelNode 是否代表了 best plan,如果是的話,就更新); - 將這個 RelNode 的 inputs 設置為其對應 RelSubset 的 children 節點(實際的操作時,是在 RelSet 的
parents
中記錄其父節點); - 強制重新計算當前 RelNode 對應 RelSubset 的 importance;
- 如果這個 RelSubset 是新建的,會再觸發一次
fireRules()
方法(會先對 RelNode 觸發一次),遍歷找到所有可以 match 的 Rule,對每個 Rule 都會創建一個 VolcanoRuleMatch 對象(會記錄 RelNode、RelOptRuleOperand 等信息,RelOptRuleOperand 中又會記錄 Rule 的信息),並將這個 VolcanoRuleMatch 添加到對應的 RuleQueue 中(就是前面圖中的那個 RuleQueue)。
這里,來看下 fireRules()
方法的實現,它的目的是把配置的 RuleMatch 添加到 RuleQueue 中,其實現如下:
//org.apache.calcite.plan.volcano.VolcanoPlanner /** * Fires all rules matched by a relational expression. * note: 觸發滿足這個 relational expression 的所有 rules * * @param rel Relational expression which has just been created (or maybe * from the queue) * @param deferred If true, each time a rule matches, just add an entry to * the queue. */ void fireRules( RelNode rel, boolean deferred) { for (RelOptRuleOperand operand : classOperands.get(rel.getClass())) { if (operand.matches(rel)) { //note: rule 匹配的情況 final VolcanoRuleCall ruleCall; if (deferred) { //note: 這里默認都是 true,會把 RuleMatch 添加到 queue 中 ruleCall = new DeferringRuleCall(this, operand); } else { ruleCall = new VolcanoRuleCall(this, operand); } ruleCall.match(rel); } } } /** * A rule call which defers its actions. Whereas {@link RelOptRuleCall} * invokes the rule when it finds a match, a <code>DeferringRuleCall</code> * creates a {@link VolcanoRuleMatch} which can be invoked later. */ private static class DeferringRuleCall extends VolcanoRuleCall { DeferringRuleCall( VolcanoPlanner planner, RelOptRuleOperand operand) { super(planner, operand); } /** * Rather than invoking the rule (as the base method does), creates a * {@link VolcanoRuleMatch} which can be invoked later. * note:不是直接觸發 rule,而是創建一個后續可以被觸發的 VolcanoRuleMatch */ protected void onMatch() { final VolcanoRuleMatch match = new VolcanoRuleMatch( volcanoPlanner, getOperand0(), //note: 其實就是 operand rels, nodeInputs); volcanoPlanner.ruleQueue.addMatch(match); } }
在上面的方法中,對於匹配的 Rule,將會創建一個 VolcanoRuleMatch 對象,之后再把這個 VolcanoRuleMatch 對象添加到對應的 RuleQueue 中。
//org.apache.calcite.plan.volcano.RuleQueue /** * Adds a rule match. The rule-matches are automatically added to all * existing {@link PhaseMatchList per-phase rule-match lists} which allow * the rule referenced by the match. * note:添加一個 rule match(添加到所有現存的 match phase 中) */ void addMatch(VolcanoRuleMatch match) { final String matchName = match.toString(); for (PhaseMatchList matchList : matchListMap.values()) { if (!matchList.names.add(matchName)) { // Identical match has already been added. continue; } String ruleClassName = match.getRule().getClass().getSimpleName(); Set<String> phaseRuleSet = phaseRuleMapping.get(matchList.phase); //note: 如果 phaseRuleSet 不為 ALL_RULES,並且 phaseRuleSet 不包含這個 ruleClassName 時,就跳過(其他三個階段都屬於這個情況) //note: 在添加 rule match 時,phaseRuleSet 可以控制哪些 match 可以添加、哪些不能添加 //note: 這里的話,默認只有處在 OPTIMIZE 階段的 PhaseMatchList 可以添加相應的 rule match if (phaseRuleSet != ALL_RULES) { if (!phaseRuleSet.contains(ruleClassName)) { continue; } } LOGGER.trace("{} Rule-match queued: {}", matchList.phase.toString(), matchName); matchList.list.add(match); matchList.matchMap.put( planner.getSubset(match.rels[0]), match); } }
到這里 VolcanoPlanner 需要初始化的內容都初始化完成了,下面就到了具體的優化部分。
4. VolcanoPlanner findBestExp
VolcanoPlanner 的 findBestExp()
是具體進行優化的地方,先介紹一下這里的優化策略(每進行一次迭代,cumulativeTicks
加1,它記錄了總的迭代次數):
- 第一次找到可執行計划的迭代次數記為
firstFiniteTick
,其對應的 Cost 暫時記為 BestCost; - 制定下一次優化要達到的目標為
BestCost*0.9
,再根據firstFiniteTick
及當前的迭代次數計算giveUpTick
,這個值代表的意思是:如果迭代次數超過這個值還沒有達到優化目標,那么將會放棄迭代,認為當前的 plan 就是 best plan; - 如果 RuleQueue 中 RuleMatch 為空,那么也會退出迭代,認為當前的 plan 就是 best plan;
- 在每次迭代時都會從 RuleQueue 中選擇一個 RuleMatch,策略是選擇一個最高 importance 的 RuleMatch,可以保證在每次規則優化時都是選擇當前優化效果最好的 Rule 去優化;
- 最后根據 best plan,構建其對應的 RelNode。
上面就是 findBestExp()
主要設計理念,這里來看其具體的實現:
//org.apache.calcite.plan.volcano.VolcanoPlanner /** * Finds the most efficient expression to implement the query given via * {@link org.apache.calcite.plan.RelOptPlanner#setRoot(org.apache.calcite.rel.RelNode)}. * * note:找到最有效率的 relational expression,這個算法包含一系列階段,每個階段被觸發的 rules 可能不同 * <p>The algorithm executes repeatedly in a series of phases. In each phase * the exact rules that may be fired varies. The mapping of phases to rule * sets is maintained in the {@link #ruleQueue}. * * note:在每個階段,planner 都會初始化這個 RelSubset 的 importance,planner 會遍歷 rule queue 中 rules 直到: * note:1. rule queue 變為空; * note:2. 對於 ambitious planner,最近 cost 不再提高時(具體來說,第一次找到一個可執行計划時,需要達到需要迭代總數的10%或更大); * note:3. 對於 non-ambitious planner,當找到一個可執行的計划就行; * <p>In each phase, the planner sets the initial importance of the existing * RelSubSets ({@link #setInitialImportance()}). The planner then iterates * over the rule matches presented by the rule queue until: * * <ol> * <li>The rule queue becomes empty.</li> * <li>For ambitious planners: No improvements to the plan have been made * recently (specifically within a number of iterations that is 10% of the * number of iterations necessary to first reach an implementable plan or 25 * iterations whichever is larger).</li> * <li>For non-ambitious planners: When an implementable plan is found.</li> * </ol> * * note:此外,如果每10次迭代之后,沒有一個可實現的計划,包含 logical RelNode 的 RelSubSets 將會通過 injectImportanceBoost 給一個 importance; * <p>Furthermore, after every 10 iterations without an implementable plan, * RelSubSets that contain only logical RelNodes are given an importance * boost via {@link #injectImportanceBoost()}. Once an implementable plan is * found, the artificially raised importance values are cleared (see * {@link #clearImportanceBoost()}). * * @return the most efficient RelNode tree found for implementing the given * query */ public RelNode findBestExp() { //note: 確保 root relational expression 的 subset(RelSubset)在它的等價集(RelSet)中包含所有 RelSubset 的 converter //note: 來保證 planner 從其他的 subsets 找到的實現方案可以轉換為 root,否則可能因為 convention 不同,無法實施 ensureRootConverters(); //note: materialized views 相關,這里可以先忽略~ registerMaterializations(); int cumulativeTicks = 0; //note: 四個階段通用的變量 //note: 不同的階段,總共四個階段,實際上只有 OPTIMIZE 這個階段有效,因為其他階段不會有 RuleMatch for (VolcanoPlannerPhase phase : VolcanoPlannerPhase.values()) { //note: 在不同的階段,初始化 RelSubSets 相應的 importance //note: root 節點往下子節點的 importance 都會被初始化 setInitialImportance(); //note: 默認是 VolcanoCost RelOptCost targetCost = costFactory.makeHugeCost(); int tick = 0; int firstFiniteTick = -1; int splitCount = 0; int giveUpTick = Integer.MAX_VALUE; while (true) { ++tick; ++cumulativeTicks; //note: 第一次運行是 false,兩個不是一個對象,一個是 costFactory.makeHugeCost, 一個是 costFactory.makeInfiniteCost //note: 如果低於目標 cost,這里再重新設置一個新目標、新的 giveUpTick if (root.bestCost.isLe(targetCost)) { //note: 本階段第一次運行,目的是為了調用 clearImportanceBoost 方法,清除相應的 importance 信息 if (firstFiniteTick < 0) { firstFiniteTick = cumulativeTicks; //note: 對於那些手動提高 importance 的 RelSubset 進行重新計算 clearImportanceBoost(); } if (ambitious) { // Choose a slightly more ambitious target cost, and // try again. If it took us 1000 iterations to find our // first finite plan, give ourselves another 100 // iterations to reduce the cost by 10%. //note: 設置 target 為當前 best cost 的 0.9,調整相應的目標,再進行優化 targetCost = root.bestCost.multiplyBy(0.9); ++splitCount; if (impatient) { if (firstFiniteTick < 10) { // It's possible pre-processing can create // an implementable plan -- give us some time // to actually optimize it. //note: 有可能在 pre-processing 階段就實現一個 implementable plan,所以先設置一個值,后面再去優化 giveUpTick = cumulativeTicks + 25; } else { giveUpTick = cumulativeTicks + Math.max(firstFiniteTick / 10, 25); } } } else { break; } //note: 最近沒有任何進步(超過 giveUpTick 限制,還沒達到目標值),直接采用當前的 best plan } else if (cumulativeTicks > giveUpTick) { // We haven't made progress recently. Take the current best. break; } else if (root.bestCost.isInfinite() && ((tick % 10) == 0)) { injectImportanceBoost(); } LOGGER.debug("PLANNER = {}; TICK = {}/{}; PHASE = {}; COST = {}", this, cumulativeTicks, tick, phase.toString(), root.bestCost); VolcanoRuleMatch match = ruleQueue.popMatch(phase); //note: 如果沒有規則,會直接退出當前的階段 if (match == null) { break; } assert match.getRule().matches(match); //note: 做相應的規則匹配 match.onMatch(); // The root may have been merged with another // subset. Find the new root subset. root = canonize(root); } //note: 當期階段完成,移除 ruleQueue 中記錄的 rule-match list ruleQueue.phaseCompleted(phase); } if (LOGGER.isTraceEnabled()) { StringWriter sw = new StringWriter(); final PrintWriter pw = new PrintWriter(sw); dump(pw); pw.flush(); LOGGER.trace(sw.toString()); } //note: 根據 plan 構建其 RelNode 樹 RelNode cheapest = root.buildCheapestPlan(this); if (LOGGER.isDebugEnabled()) { LOGGER.debug( "Cheapest plan:\n{}", RelOptUtil.toString(cheapest, SqlExplainLevel.ALL_ATTRIBUTES)); LOGGER.debug("Provenance:\n{}", provenance(cheapest)); } return cheapest; }
整體的流程正如前面所述,這里來看下 RuleQueue 中 popMatch()
方法的實現,它的目的是選擇 the highest importance 的 RuleMatch,這個方法的實現如下:
/org.apache.calcite.plan.volcano.RuleQueue /** * Removes the rule match with the highest importance, and returns it. * * note:返回最高 importance 的 rule,並從 Rule Match 中移除(處理過后的就移除) * note:如果集合為空,就返回 null * <p>Returns {@code null} if there are no more matches.</p> * * <p>Note that the VolcanoPlanner may still decide to reject rule matches * which have become invalid, say if one of their operands belongs to an * obsolete set or has importance=0. * * @throws java.lang.AssertionError if this method is called with a phase * previously marked as completed via * {@link #phaseCompleted(VolcanoPlannerPhase)}. */ VolcanoRuleMatch popMatch(VolcanoPlannerPhase phase) { dump(); //note: 選擇當前階段對應的 PhaseMatchList PhaseMatchList phaseMatchList = matchListMap.get(phase); if (phaseMatchList == null) { throw new AssertionError("Used match list for phase " + phase + " after phase complete"); } final List<VolcanoRuleMatch> matchList = phaseMatchList.list; VolcanoRuleMatch match; for (;;) { //note: 按照前面的邏輯只有在 OPTIMIZE 階段,PhaseMatchList 才不為空,其他階段都是空 // 參考 addMatch 方法 if (matchList.isEmpty()) { return null; } if (LOGGER.isTraceEnabled()) { matchList.sort(MATCH_COMPARATOR); match = matchList.remove(0); StringBuilder b = new StringBuilder(); b.append("Sorted rule queue:"); for (VolcanoRuleMatch match2 : matchList) { final double importance = match2.computeImportance(); b.append("\n"); b.append(match2); b.append(" importance "); b.append(importance); } LOGGER.trace(b.toString()); } else { //note: 直接遍歷找到 importance 最大的 match(上面先做排序,是為了輸出日志) // If we're not tracing, it's not worth the effort of sorting the // list to find the minimum. match = null; int bestPos = -1; int i = -1; for (VolcanoRuleMatch match2 : matchList) { ++i; if (match == null || MATCH_COMPARATOR.compare(match2, match) < 0) { bestPos = i; match = match2; } } match = matchList.remove(bestPos); } if (skipMatch(match)) { LOGGER.debug("Skip match: {}", match); } else { break; } } // A rule match's digest is composed of the operand RelNodes' digests, // which may have changed if sets have merged since the rule match was // enqueued. //note: 重新計算一下這個 RuleMatch 的 digest match.recomputeDigest(); //note: 從 phaseMatchList 移除這個 RuleMatch phaseMatchList.matchMap.remove( planner.getSubset(match.rels[0]), match); LOGGER.debug("Pop match: {}", match); return match; }
到這里,我們就把 VolcanoPlanner 的優化講述完了,當然並沒有面面俱到所有的細節,VolcanoPlanner 的整體處理圖如下:
一些思考
1. 初始化 RuleQueue 時,添加的 one useless rule name 有什么用?
在初始化 RuleQueue 時,會給 VolcanoPlanner 的四個階段 PRE_PROCESS_MDR, PRE_PROCESS, OPTIMIZE, CLEANUP
都初始化一個 PhaseMatchList 對象(記錄這個階段對應的 RuleMatch),這時候會給其中的三個階段添加一個 useless rule,如下所示:
protected VolcanoPlannerPhaseRuleMappingInitializer getPhaseRuleMappingInitializer() { return phaseRuleMap -> { // Disable all phases except OPTIMIZE by adding one useless rule name. //note: 通過添加一個無用的 rule name 來 disable 優化器的其他三個階段 phaseRuleMap.get(VolcanoPlannerPhase.PRE_PROCESS_MDR).add("xxx"); phaseRuleMap.get(VolcanoPlannerPhase.PRE_PROCESS).add("xxx"); phaseRuleMap.get(VolcanoPlannerPhase.CLEANUP).add("xxx"); }; }
開始時還困惑這個什么用?后來看到下面的代碼基本就明白了
for (VolcanoPlannerPhase phase : VolcanoPlannerPhase.values()) { // empty phases get converted to "all rules" //note: 如果階段對應的 rule set 為空,那么就給這個階段對應的 rule set 添加一個 【ALL_RULES】 //也就是只有 OPTIMIZE 這個階段對應的會添加 ALL_RULES if (phaseRuleMapping.get(phase).isEmpty()) { phaseRuleMapping.put(phase, ALL_RULES); } }
后面在調用 RuleQueue 的 addMatch()
方法會做相應的判斷,如果 phaseRuleSet 不為 ALL_RULES,並且 phaseRuleSet 不包含這個 ruleClassName 時,那么就跳過這個 RuleMatch,也就是說實際上只有 OPTIMIZE 這個階段是發揮作用的,其他階段沒有添加任何 RuleMatch。
2. 四個 phase 實際上只用了 1個階段,為什么要設置4個階段?
VolcanoPlanner 的四個階段 PRE_PROCESS_MDR, PRE_PROCESS, OPTIMIZE, CLEANUP
,實際只有 OPTIMIZE
進行真正的優化操作,其他階段並沒有,這里自己是有一些困惑的:
- 為什么要分為4個階段,在添加 RuleMatch 時,是向四個階段同時添加,這個設計有什么好處?為什么要優化四次?
- 設計了4個階段,為什么默認只用了1個?
這兩個問題,暫時也沒有頭緒,有想法的,歡迎交流。
- HepPlanner源碼分析——Calcite;
- SQL 查詢優化原理與 Volcano Optimizer 介紹;
- 高級數據庫十六:查詢優化器(二);
- 【SQL】SQL優化器原理——查詢優化器綜述;
- SparkSQL – 從0到1認識Catalyst;
- BigData-‘基於代價優化’究竟是怎么一回事?;
- Cost-based Query Optimization in Apache Phoenix using Apache Calcite;
- The Volcano Optimizer Generator: Extensibility and Efficient Search:Volcano 模型的經典論文;
- The Cascades Framework for Query Optimization:Cascades 模型的經典論文。