當前位置: 妍妍網 > 資訊

StarRocks消除聚合操作規則(EliminateAggRule)解析

2024-05-24資訊

引言

在資料庫查詢最佳化中,消除不必要的聚合操作是提高查詢效能的重要手段。本文將詳細介紹StarRocks最佳化器中的EliminateAggRule類,該類透過一系列規則檢查和轉換,最佳化聚合查詢。我們將深入解析其實作細節,並解釋每個步驟的目的和原理。

PR連結:https://github.com/StarRocks/starrocks/pull/45432

背景介紹

當使用唯一鍵(Unique Key,UK)執行聚合查詢時,在滿足某些條件下可以將聚合操作消除。例如,查詢「SELECT MAX(t1) FROM demo GROUP BY uk」可以覆寫為「SELECT t1 FROM demo」。這種覆寫的原因在於唯一鍵的特性,UK保證了表中的每一行數據都是唯一的,因此每個UK值都不會重復。
更詳細的原因如下:

  1. 主鍵的唯一性和非空約束:

  • 唯一鍵(Unique Key,UK)的定義要求每個值都是唯一的,並且唯一鍵不允許為空。這意味著表中的每一行都可以透過UK進行唯一標識。

  • 2. 分組和聚合操作的邏輯:

  • 使用「GROUP BY」子句時,資料庫會按照一個或多個列進行分組。當使用「GROUP BY uk」時,由於UK是唯一鍵,每個UK值在表中都是唯一的,因此每個分組實際上都包含該唯一鍵的行。

  • 當對一個數據組使用「MAX(t1)」或其他聚合函式時,如果分組中只有一條記錄(因為每個UK值唯一,故每組只有一行),那麽聚合函式的結果就是那條記錄中相應的值。因此,「MAX(t1)」的結果就是該行的「t1」欄位值。

  • 3. 查詢覆寫的合理性:

  • 原查詢「SELECT MAX(t1) FROM demo GROUP BY uk」實際上對於每個唯一鍵都返回「t1」的最大值。但由於每個唯一鍵對應只有一條記錄,「t1」列的最大值就是「t1」列的實際值。因此,這個查詢可以簡化為「SELECT t1 FROM demo」,直接返回每行的「t1」欄位值。這樣做不僅更簡單,而且效率更高,避免了不必要的分組和聚合計算過程。

  • 透過以上分析可以看出,當UK為表的唯一鍵時,使用「GROUP BY uk」對數據進行分組實際是多余的,因為每個唯一鍵都唯一標識一條記錄。在這種情況下,任何對單個記錄的聚合操作都可以透過選擇該記錄的相應欄位來實作,從而簡化查詢並提高查詢效能。

    實作步驟

    在StarRocsk中添加新的最佳化規則一般涉及到查詢最佳化器的修改,一些明顯正收益的最佳化規則一般添加在FE層的logicalRuleRewrite方法中。logicalRuleRewrite的主要作用是透過一系列邏輯規則(Logical Rules)來最佳化查詢計劃。這個步驟在查詢最佳化過程中起到關鍵作用,透過套用預定義的最佳化規則來覆寫查詢計劃,使其更加高效。
    logicalRuleRewrite的具體作用包括:

    1. 簡化查詢計劃:透過套用最佳化規則來簡化查詢計劃,消除冗余操作。例如,去掉不必要的投影、過濾等操作

    2. 重寫查詢結構:根據最佳化規則重寫查詢的邏輯結構,使得查詢更加高效。例如,將巢狀查詢轉換為連線操作,或者將復雜的運算式簡化為等價的更高效的運算式。

    3. 最佳化操作順序:調整查詢計劃中操作的順序,使得數據處理的效率更高。例如,將選擇操作(過濾條件)盡可能提前到數據處理的早期階段,以減少後續處理的數據量。

    4. 套用特定的最佳化策略:根據查詢的特征和資料庫的內部實作,套用特定的最佳化策略。例如,使用索引、分區等技術來最佳化查詢效能。

    5. 準備物理執行計劃:logicalRuleRewrite是生成最終物理計劃的重要一步。邏輯最佳化後的查詢計劃將作為物理計劃生成的基礎,在物理計劃生成階段進一步最佳化和具體化。

    具體實作上,logicalRuleRewrite會透過遍歷查詢樹,套用一系列定義的邏輯規則對查詢計劃進行覆寫和最佳化。每個邏輯規則通常會針對特定的查詢模式進行最佳化,例如子查詢消除、連線條件下推、常量折疊等。
    logicalRuleRewrite中的一條新的最佳化規則通常由以下部份組成:

    1. 規則型別和模式匹配:每條規則都需要定義其型別和匹配模式。規則型別通常是一個列舉值,用於標識規則的類別。模式匹配適用於指定規則的查詢計劃模式。

    2. 規則的實作類:定義一個繼承TransformationRule的實作類,裏麵包含規則的具體實作邏輯,透過重寫check和transform來實作具體的最佳化規則。

    在StarRocks中的logicalRuleRewrite執行過程中,規則的套用順序是最佳化器設計的一個重要方面。確定規則的套用順序通常依據以下幾個邏輯和原則:

    1. 規則的優先性:不同規則可能有不同的優先級。通常,高優先級的規則先套用,因為它們對查詢計劃的最佳化效果更明顯。例如,消除冗余操作或簡化查詢結構的規則通常優先套用。

    2. 規則的依賴關系:有些規則可能依賴於其他規則的結果。例如,某些規則在執行前需要其他規則先行最佳化查詢計劃。因此,必須確保依賴的規則先套用。

    3. 規則的作用和範圍:一些規則的作用範圍較廣,可能在更大範圍內進行最佳化,而其他規則可能只針對特定型別的查詢操作。一般來說,範圍較廣的規則優先套用,以便為後續更具體的最佳化規則創造條件。

    4. 規則的變換性質:規則的變換性質也影響套用順序。某些變化規則可能引入新的最佳化機會,這些機會通常需要透過後續規則進一步最佳化。例如,將一個子查詢覆寫為連線操作後,可能會有新的連線下推的機會。

    5. 規則的代價和收益:最佳化器可能會根據規則的套用代價和潛在收益來確定順序。高收益且低代價的規則會優先套用,以盡可能減少查詢最佳化的總體開銷。

    在StarRocks中添加新最佳化規則具體實踐中,確定規則的放置位置有兩種具體方法:

    1. 檢視規則的註釋,有些規則的註釋會明確指出其特定的放置要求。例如,某些規則在執行後不允許生成新的project操作,那麽生成新project的規則就需要放在這些規則之前。你還可以搜尋類似的最佳化關鍵字,盡量將同類最佳化規則靠近放置。

    2. 與StarRocks的研發團隊同學溝通。StarRocks的研發團隊擁有豐富的經驗,他們可以快速判斷出新最佳化規則的最佳放置位置。與他們溝通可以確保你的規則在整個最佳化流程中發揮最大效用。

    下面將介紹如何實作具體的消除規則實作過程

    1. 模式規則的定義

    在StarRocks的logicalRuleRewrite添加新的規則時,通常的做法是用新的節點替換匹配到的節點。這種轉換規則的目的是透過查詢計劃中的某些節點進行替換和重構,以實作查詢最佳化。
    定義一個新的模式匹配規則涉及到建立一個繼承TransformationRule的類,並在其建構函式中指定規則型別和匹配模式。具體的實作步驟如下

    1. 建立新的規則類:首先,在package com.starrocks.sql.optimizer.rule.transformation下建立一個新的EliminateAggRule類

    2. 定義規則型別和匹配模式:在類的建構函式中,定義規則型別和匹配模式。

    super(RuleType.TF_ELIMINATE_AGG, Pattern.create(OperatorType.LOGICAL_AGGR)
    .addChildren(Pattern.create(OperatorType.PATTERN_LEAF)));

    下面對實作程式碼進行詳細的解釋說明:
    super(RuleType.TF_ELIMINATE_AGG, ...)
    這個部份呼叫了父類的建構函式,傳遞了兩個參數:規則型別和匹配模式。RuleType.TF_ELIMINATE_AGG:這是一個列舉值,表示規則的型別。TF代表某種型別轉換或傳遞函式。ELIMINATE_AGG表示這是一個用於消除聚合操作的規則。

    Pattern.create(OperatorType.LOGICAL_AGGR)
    這個部份定義了匹配模式的根節點,即這個模式規則的查詢計劃節點型別。
    Pattern.create(...):建立一個新的匹配模式。
    OperatorType.LOGICAL_AGGR:指定匹配的節點型別為邏輯聚合操作(LOGICAL_AGGR)。這意味著這個規則適用於查詢計劃中包含邏輯聚合操作的節點。
    addChildren(Pattern.create(OperatorType.PATTERN_LEAF))
    這個部份定義了根節點的子節點模式
    .addChildren(...):添加子節點模式,表示根節點必須滿足的子節點結構。
    Pattern.create(OperatorType.PATTERN_LEAF):指定子節點的模式為單節點(PATTERN_LEAF)。StarRocks中節點型別包括單節點(PATTERN_LEAF)、多節點(PATTERN_MULTI_LEAF)、掃描節點(PATTERN_SCAN)等。

    2. 規則檢查

    TransformationRule中,check方法用於驗證輸入的查詢計劃節點是否符合規則的套用條件。該方法的主要作用是確保只有特定條件的節點才會套用該轉換規則。
    check方法主要有2個作用:

    1. 驗證規則的套用條件:check方法透過檢查輸入的查詢節點的內容、結構和其他條件,決定是否可以套用該規則,例如:對於EliminateAggrule,需要檢查聚合操作中的函式能否允許被消除,分組鍵是否唯一等。

    2. 提高最佳化效率:透過預先檢查條件,可以避免不必要的轉換操作,提高查詢最佳化的效率。只有滿足條件的節點才會被傳遞給transform方法進行進一步處理。

    TransformationRule的具體邏輯和步驟詳細說明如下:

    1. 檢查聚合函式的合法性

    遍歷聚合操作中的每一個聚合函式,檢查其是否合法。如果聚合函式是Distinct的或者不是指定的聚合函式之一,則返回「false」

    for(Map.Entry<ColumnRefOperator, CallOperator> entry : aggOp.getAggregations().entrySet()){
    if(entry.getValue().isDistinct()){
    returnfalse;
    }
    String fnName = entry.getValue().getFnName();
    if(!(fnName.equals(FunctionSet.SUM)|| fnName.equals(FunctionSet.COUNT)||
    fnName.equals(FunctionSet.AVG)||
    fnName.equals(FunctionSet.FIRST_VALUE)||
    fnName.equals(FunctionSet.MAX)|| fnName.equals(FunctionSet.MIN)||
    fnName.equals(FunctionSet.GROUP_CONCAT))){
    returnfalse;
    }
    }

    Q1: 為什麽要過濾掉Distinct函式

    對於Distinct聚合函式,消除聚合操作可能會導致查詢結果不正確,因為Distinct意味著需要對結果集進行去重處理,直接消除會忽略這一要求。此外,從單一職責原則來看,一條最佳化規則應該是清晰和簡單的,因此不應包含復雜的Distinct邏輯。

    Q2:為什麽聚合消除只處理sum、count等函式

    主要原因如下:

  • 常用性和適用性:這些函式是最常見和廣泛使用的聚合函式,處理這些函式可以覆蓋大部份實際套用中的場景。這些函式具有明確的數學和邏輯定義,容易進行最佳化和處理。

  • 計算邏輯計算:這些函式的計算邏輯相對簡單,聚合消除操作在這些函式上更容易實作。例如,SUM、COUNT、AVG、MAX、MIN、FIRST_VALUE都是基於單一列進行簡單的數學運算。

  • 結果確定性:這些函式的結果是確定的,不依賴於數據的排列或其他復雜因素。因此,聚合消除操作不會影響查詢的正確性。例如,sum和count只需要累加數據或計數,agv可以透過sum和count計算得出,max和min只需要比較值,first_value是第一個出現的值,而group_concat只需要將字串連線起來。

  • 復雜函式的處理難度:其他聚合函式(如復雜的自訂聚合函式)可能具有復雜的邏輯和計算過程,聚合消除操作難以確保正確性和效率。處理這些復雜函式需要更多的邏輯判斷和條件處理,增加了最佳化規則的復雜性,不利於規則的簡單和清晰。

  • 輸入參數簡單:這些函式的輸入參數通常只有一個,這使得消除的邏輯更加簡單和清晰。處理單一輸入參數的聚合函式,能夠更容易地套用最佳化規則。例如,sum(t1)、max(t1)等函式值需要關註t1列,而不需要處理復雜的多參數輸入。

  • 因此,聚合消除只處理SUM、COUNT、AVG、MAX、MIN、FIRST_VALUE、GROUP_CONCAT等函式,以確保最佳化規則的有效性、簡單性和高效性。

    2. 驗證唯一鍵的聚合分組鍵是否一致

    這個過程主要完成這幾項工作:

  • 收集子節點的唯一鍵約束

  • UKFKConstraintsCollector collector =new UKFKConstraintsCollector();
    input.getOp().accept(collector, input,null);
    OptExpression childOptExpression = input.inputAt(0);
    Map<Integer, UKFKConstraints.UniqueConstraintWrapper> uniqueKeys =
    childOptExpression.getConstraints().getUniqueKeys();
    Set<Integer> uniqueColumnRefIds =
    uniqueKeys.keySet().stream().collect(Collectors.toSet());

    收集子節點的唯一鍵約束在check方法中非常重要,因為這些唯一鍵約束是決定能否進行聚合消除的關鍵。聚合消除的前提是保證每個分組(由唯一鍵確定的分組)中只有一條記錄,因此連線子節點的唯一鍵約束是確保查詢正確正確性的基礎。
    收集子節點通常透過樹遍歷和約束傳播演算法來實作,實作這一過程的步驟如下:

    1. 樹遍歷:查詢計劃被表示為一棵樹,每個節點代表一個操作符。為了收集子節點的唯一鍵約束,需要遍歷這棵樹。自下而上的遍歷(bottom-up traversal)是一種常用的方法,從葉子節點開始,逐層向上收集和傳播約束資訊。

    2. 約束傳播:每個節點根據自身的操作型別和子節點的約束資訊來計算和更新自己的唯一鍵約束。例如,一個投影(projection)會根據子節點的唯一約束和投影列來更新自己的約束。

  • 驗證唯一鍵的存在 :如果沒有唯一鍵約束,分組後的每個分組可能包含多條記錄,這樣聚合函式就不能被安全消除。

  • 對比唯一鍵的數量和聚合分組鍵的列是否數量和ID是否一致 :唯一鍵(Unique Key)是在資料庫表中由一列或多列組成的,這些列上的值在每一行都是唯一的,即沒有重復值。如果分組鍵與唯一鍵的列完全一致(包括數量和列ID),那麽按照分組鍵進行分組時,每個分組必定只有一條記錄,因為唯一鍵確保了這些列的組合是唯一的。相反,如果分組鍵和唯一鍵的列數量和列ID不一致,則在分組聚合時無法保證每個分組只有一條記錄。

  • 3. 執行節點轉換

    在transform方法中,最重要的原則是 保持輸入和輸出列的一致性 。保證輸出列完全一樣的原因主要與查詢計劃的正確性和一致性有關。以下是一些關鍵點:

  • 保持查詢結果的一致性:當最佳化套用一個轉換規則來改變查詢計劃的一部份時,重要的是確保這種轉換不會改變查詢的預期輸出。輸出列必須保持不變,以確保查詢前後的結果集是一致的。這意味著無論最佳化規則如何轉換或重組查詢的某些部份,最終提供給使用者的數據應該與未最佳化的查詢相同。

  • 避免對上層操作產生影響:查詢計劃通常由多層操作組成,如選擇、投影、連線等。一個層級輸出往往作為另一個層級的輸入。如果一個轉換規則改變了其輸出列,那麽依賴於這些列的上層操作可能無法正確執行,因為它們可能期待特定的列存在。

  • 計劃最佳化過程:維持輸出列的不變可以簡化最佳化邏輯。最佳化器不需要擔心後續操作可能因為缺少預期的列而失敗。這使得編寫和測試最佳化規則更加直接和可預測。

  • 允許獨立的最佳化規則套用:如果每個規則都保證了輸出列的一致性,那麽這些規則可以獨立套用,而無需擔心其順序或組合的復雜性。這種靈活性允許最佳化器更加自由地探索不同的最佳化策略組合,以找到最有效的執行計劃。

  • 確保邏輯的正確性:查詢計劃的邏輯正確性是資料庫管理核心的一部份。確保每次最佳化轉換後的輸出列不變,是維護整個查詢處理過程中數據完整性和正確性的重要措施。

  • 因此,在StarRocks中,維護在套用一條新的TransformationRule規則後輸出列的一致性是確保查詢最佳化不會引起意外結果和錯誤的關鍵。這種方法確保了最佳化過程中的穩定性和可靠性,同時減少了最佳化器設計的復雜性。
    例如,在轉換過程中生成新的投影(Project)操作,要確保輸入列的一致性。在Project操作中,輸出列的對映儲存於 Map<ColumnRefOperator, ScalarOperator> 結構中,其中 ColumnRefOperator 用於表示列參照,而 ScalarOperator 用於描述相應的列運算式。ColumnRefOperator 通常包含列的唯一標識(如ID)和型別資訊,這些都需要保持不變。如果 ColumnRefOperator 相同,則意味著其參照的列在資料庫表中的位置和型別都未發生變化。確保每個 ColumnRefOperator 在原始和轉換後的查詢計劃中保持相同,是保證輸出列一致性的關鍵。
    Map<ColumnRefOperator, ScalarOperator>結構中。ColumnRefOperator和ScalarOperator分別有著特定的角色和含義,它們共同構成了在處理資料庫查詢時所使用的一個關鍵的數據對映結構。這種對映主要用於定義和管理查詢計劃中列的對映和轉換邏輯。

    ColumnRefOperator: 是一個用於表示資料庫查詢中列參照的操作符。它封裝了特定列的參照資訊,比如列識別元,數據型別,列名,可空性。
    ScalarOperator :是一個更廣泛的概念,它表示任何可能產生純量值(單個值)的操作或運算式。這可以包括簡單的列參照,也可以包括復雜的運算式或函式呼叫。例如:SELECT MAX(t1) AS tt1 FROM demo GROUP BY uk;MAX函式的ScalarOperator通常是一個CallOperator。CallOperator是一個表示函式呼叫的操作符,用於處理聚合函式、純量函式等。而透過聚合消除之後,CallOperator轉換為適當的ScalarOperator。轉換後的ScalarOperator需要保證返回型別一致。

    Map<ColumnRefOperator, ScalarOperator>結構中,每個ColumnRefOperator作為鍵,表示輸出列的一個標識,而與之關聯的ScalarOperator作為值,定義了如何計算或衍生這個列的值。這個對映允許復雜的數據轉換和計算邏輯被明確地表示在查詢計劃中的某個階段,例如在邏輯投影(LogicalProject)操作中。

    這種對映的設計不僅清晰地分離了列的標識(哪一列)和列的計算邏輯(如何處理這一列),還提供了足夠的靈活性來支持廣泛的查詢最佳化和執行策略,這使得資料庫系統能夠有效地處理和最佳化復雜的查詢請求。

    綜上所述,在轉換過程中輸出的 ColumnRefOperator 通常保持不變,可以直接沿用輸入時的列參照。而ScalarOperator 通常會發生變化,因為最佳化過程涉及調整相關列的計算邏輯。

    聚合最佳化規則中的transform方法的目的是將符合特定條件的聚合操作轉換為有效的投影操作,以最佳化查詢計劃。下面是transform方法的實作邏輯。

    1. 處理聚合函式

    for(Map.Entry<ColumnRefOperator, CallOperator> entry : aggOp.getAggregations().entrySet()){
    ColumnRefOperator aggColumnRef = entry.getKey();
    CallOperator callOperator = entry.getValue();
    ScalarOperator newOperator = handleAggregationFunction(callOperator.getFnName(), callOperator);
    newProjectMap.put(aggColumnRef, newOperator);
    }

    遍歷聚合操作符中的每個聚合函式,將其轉換為新的操作符,然後將這些新的操作符放入一個新的投影對映 (newProjectMap)中。

    根據聚合函式的名稱呼叫不同的處理常式

    private ScalarOperator handleAggregationFunction(String fnName, CallOperator callOperator){
    if(fnName.equals(FunctionSet.COUNT)){
    return rewriteCountFunction(callOperator);
    }elseif(fnName.equals(FunctionSet.SUM)|| fnName.equals(FunctionSet.AVG)||
    fnName.equals(FunctionSet.FIRST_VALUE)|| fnName.equals(FunctionSet.MAX)||
    fnName.equals(FunctionSet.MIN)|| fnName.equals(FunctionSet.GROUP_CONCAT)){
    return rewriteCastFunction(callOperator);
    }
    return callOperator;
    }

    這裏為什麽只有count單獨處理,而其他函式直接處理。原因如下:

  • count函式的特殊性:參數為空的時候,count(*)計算的是非空的數量,與具體的列值無關。參數不為空的時候,需要檢查列值是否為Null。

  • 其他聚合函式的通用性:SUM、AVG、FIRST_VALUE、MAX、MIN、GROUP_CONCAT等函式的計算邏輯相對簡單,可以透過型別轉換保持一致。這些函式直接作用於列值,不需要額外的邏輯判斷。另外,這些函式只需確保返回型別與參數型別一致,透過型別轉換透過型別轉換(CastOperator)可以輕松實作這一點。

  • private ScalarOperator rewriteCountFunction(CallOperator callOperator){
    if(callOperator.getArguments().isEmpty()){
    return ConstantOperator.createInt(1);
    }
    IsNullPredicateOperator isNullPredicateOperator =
    new IsNullPredicateOperator(callOperator.getArguments().get(0));
    ArrayList<ScalarOperator> ifArgs = Lists.newArrayList();
    ScalarOperator thenExpr = ConstantOperator.createInt(0);
    ScalarOperator elseExpr = ConstantOperator.createInt(1);
    ifArgs.add(isNullPredicateOperator);
    ifArgs.add(thenExpr);
    ifArgs.add(elseExpr);
    Type[] argumentTypes = ifArgs.stream().map(ScalarOperator::getType).toArray(Type[]::new);
    Function fn =
    Expr.getBuiltinFunction(FunctionSet.IF, argumentTypes, Function.CompareMode.IS_NONSTRICT_SUPERTYPE_OF);
    returnnew CallOperator(FunctionSet.IF, ScalarType.createType(PrimitiveType.TINYINT), ifArgs, fn);
    }

    rewriteCountFunction函式的目的是處理count聚合函式的重寫邏輯,使其能夠在不需要聚合的情況下正確執行。實作判斷是否有參數比如(count(*)),如果沒有參數則直接返回常量1。如果有參數則建立IsNullPredicateOperator用於判斷參數是否為Null。
    第一個參數:isNullPredicateOperator,判斷列值是否為NULL。
    第二個參數:thenExpr,如果列值為NULL,返回0。
    第三個參數:elseExpr,如果列值不為NULL,返回1。
    最終使用IF函式建立CallOperator,模擬Count(column)的行為。

    private ScalarOperator rewriteCastFunction(CallOperator callOperator){
    ScalarOperator argument = callOperator.getArguments().get(0);
    if(callOperator.getType().equals(argument.getType())){
    return argument;
    }
    ScalarOperator scalarOperator =new CastOperator(callOperator.getType(), argument);
    return scalarOperator;
    }

    rewriteCastFunction函式的是處理(如SUM、AVG、FIRST_VALUE、MAX、MIN、GROUP_CONCAT)的重寫邏輯,確保它們在聚合操作消除後,參數型別與聚合函式的返回型別一致,從而保證查詢結果的正確性。這些函式可能涉及型別轉換,因此需要生成適當的型別轉換操作符(CastOperator)。程式碼的整體邏輯:

  • 從聚合操作符中獲取第一個參數。檢查聚合函式返回型別與參數型別是否一致,如果一致,直接返回參數。

  • 如果不一致,生成一個型別轉換符,將參數轉換為聚合函式的返回型別。

  • 2. 保留分組列

    aggOp.getGroupingKeys()
    .forEach(columnRefOperator -> newProjectMap.put(columnRefOperator, columnRefOperator))

    這段程式碼的目的是將聚合操作符中的分組列直接添加到新的投影中,以確保在聚合消除後,這些列仍然保留在查詢計劃中。比如執行SELECT uk, MAX(t1) FROM demo GROUP BY uk,uk就是分組列,我們在轉換時同樣需要保留。這個邏輯的具體步驟如下:

  • 獲取分組列:aggOp.getGroupingKeys()。返回一個包含聚合操作符中所有分組鍵(grouping keys)的列表。

  • 遍歷分組列:.forEach(columnRefOperator -> ...)。遍歷分組鍵列表中的每個 ColumnRefOperator 物件。

  • 添加到新的投影對映:newProjectMap.put(columnRefOperator, columnRefOperator)。 將每個分組鍵列參照操作符 columnRefOperator 作為鍵和值放入 newProjectMap 中。

  • 透過這個操作,可以保證在消除聚合操作符並轉換為投影操作符後,這些分組列依然存在於查詢計劃中,並且可以正確地參與後續的查詢處理和最佳化。

    3. 建立新的投影對映

    LogicalProjectOperator newProjectOp =
    LogicalProjectOperator.builder().setColumnRefMap(newProjectMap).build();

    LogicalProjectOperator.builder().setColumnRefMap(newProjectMap).build(),使用構建器模式建立一個新的LogicalProjectOperator例項,設定列參照對映為newProjectMap,這確保了新的投影操作符包含了所有需要的列和它們對應的操作。

    4. 返回新的查詢計劃節點

    return List.of(OptExpression.create(newProjectOp, input.inputAt(0)));

    使用OptExpression.create方法建立一個新的查詢計劃節點,該節點包含新的投影操作符,並將原始查詢計劃Agg節點的子節點作為輸入。返回一個包含該新查詢計劃節點的不可變單元素列表。新的計劃節點可以直接使用 input.inputAt(0) 子節點的輸入,原因如下:
    保持查詢計劃結構的連貫性:

  • 節點替換原則:在查詢計劃轉換過程中,通常是將某個節點替換為另一個節點,但該節點的子節點結構不變。這樣可以確保在轉換後,新的節點仍然與原有的子節點相連線,從而保持查詢計劃結構的連貫性。

  • 保持數據流的正確性:原始的子節點輸入包含了查詢計劃中已經存在的數據流和操作步驟。這些子節點在轉換後的新節點中仍然需要被保留,以確保數據處理的正確順序和邏輯不變。

  • 保持輸入輸出一致:列參照的一致性,子節點輸入的列參照在轉換前後需要保持一致。透過直接使用原來的子節點輸入,可以確保列參照的一致性,避免列參照遺失或不匹配。查詢邏輯的一致性,轉換後的查詢邏輯應該與轉換前的邏輯一致。直接使用原有子節點輸入,可以確保查詢邏輯的連貫性和正確性。

  • 單元測試

    在StarRcosk這樣的系統只能夠,單元測試的重要性不容忽略。它不僅保證了系統的穩定性和可靠性,還提高了程式碼品質,支持快速叠代和持續整合,防止回歸問題,並提供了對重構和新功能開發的支持,透過編寫全面的單元測試,開發者可以確保系統在各種情況下都能穩定執行,從而為使用者提供高品質的資料庫服務。

    而如何保證資料庫進行完整的測試覆蓋,主要有三種方法:

    1. 從資料庫本身支持的語法檔、函式、型別進行正交

    2. 嘗試各種隨機性測試

    3. 人工正交梳理,持續記錄相關場景的測試case。

    不斷積累的單元測試case對於資料庫而言是一筆寶貴的資產。

    在進行測試之前,我們需要構建數據表,插入測試數據集和生成覆蓋全部場景的測試SQL。

    1. 建立測試數據表

    ---------unqiue key測試表-----------------
    CREATE TABLE `test_agg_group_single_unique_key` (
    id INTNOTNULL,
    big_value BIGINT,
    double_value DOUBLE,
    decimal_value DECIMAL(10, 5),
    varchar_value VARCHAR(255)
    ) ENGINE=OLAP
    UNIQUEKEY(id)
    DISTRIBUTED BYHASH(id) BUCKETS 10
    PROPERTIES (
    "replication_num"="1"
    );
    ---------unqiue key測試表-----------------
    CREATE TABLE `test_agg_group_multi_unique_key` (
    id INTNOTNULL,
    big_value BIGINT,
    double_value DOUBLE,
    decimal_value DECIMAL(10, 5),
    varchar_value VARCHAR(255)
    ) ENGINE=OLAP
    UNIQUEKEY(id,big_value)
    DISTRIBUTED BYHASH(id) BUCKETS 10
    PROPERTIES (
    "replication_num"="1"
    );

    SUM、AVG、FIRST_VALUE、MAX、MIN、GROUP_CONCAT的輸入參數一般為整形與字元型,所以在建立測試表的時候也主要使用這些欄位型別。

    2. 建立測試數據集

    -- 插入測試數據
    INSERTINTO`test_agg_group_single_unique_key` (id, big_value, double_value, decimal_value, varchar_value) VALUES
    (1, 100000, 1.23, 123.45678, 'Test1'),
    (2, 200000, 2.34, 234.56789, 'Test2'),
    (3, 300000, 3.45, 345.67890, 'Test3'),
    (4, 400000, 4.56, 456.78901, 'Test4'),
    (5, 500000, 5.67, 567.89012, NULL),
    (6, 600000, 6.78, 678.90123, 'Test6'),
    (7, 700000, 7.89, 789.01234, NULL),
    (8, 800000, 8.90, 890.12345, 'Test8'),
    (9, 900000, 9.01, 901.23456, NULL),
    (10, 1000000, 10.12, 1012.34567, 'Test10'),
    (11, 1100000, 11.23, 1123.45678, 'Test11'),
    (12, 1200000, 12.34, 1234.56789, 'Test12'),
    (13, 1300000, 13.45, 1345.67890, NULL),
    (14, 1400000, 14.56, 1456.78901, 'Test14'),
    (15, 1500000, 15.67, 1567.89012, 'Test15'),
    (16, 1600000, 16.78, 1678.90123, NULL),
    (17, 1700000, 17.89, 1789.01234, 'Test17'),
    (18, 1800000, 18.90, 1890.12345, 'Test18'),
    (19, 1900000, 19.01, 1901.23456, NULL),
    (20, 2000000, 20.12, 2012.34567, 'Test20');
    -- 插入測試數據
    INSERTINTO`test_agg_group_multi_unique_key` (id, big_value, double_value, decimal_value, varchar_value) VALUES
    (1, 100000, 1.23, 123.45678, 'Test1'),
    (2, 200000, 2.34, 234.56789, 'Test2'),
    (3, 300000, 3.45, 345.67890, 'Test3'),
    (4, 400000, 4.56, 456.78901, 'Test4'),
    (5, 500000, 5.67, 567.89012, NULL),
    (6, 600000, 6.78, 678.90123, 'Test6'),
    (7, 700000, 7.89, 789.01234, NULL),
    (8, 800000, 8.90, 890.12345, 'Test8'),
    (9, 900000, 9.01, 901.23456, NULL),
    (10, 1000000, 10.12, 1012.34567, 'Test10'),
    (11, 1100000, 11.23, 1123.45678, 'Test11'),
    (12, 1200000, 12.34, 1234.56789, 'Test12'),
    (13, 1300000, 13.45, 1345.67890, NULL),
    (14, 1400000, 14.56, 1456.78901, 'Test14'),
    (15, 1500000, 15.67, 1567.89012, 'Test15'),
    (16, 1600000, 16.78, 1678.90123, NULL),
    (17, 1700000, 17.89, 1789.01234, 'Test17'),
    (18, 1800000, 18.90, 1890.12345, 'Test18'),
    (19, 1900000, 19.01, 1901.23456, NULL),
    (20, 2000000, 20.12, 2012.34567, 'Test20');

    3. 生成測試SQL

    生成單個Unique Key測試SQL

    -- SUM 聚合函式測試
    SELECT
    id,
    SUM(big_value) AS sum_big_value
    FROM
    test_agg_group_single_unique_key
    GROUPBY
    id
    ORDERBY
    id;
    -- SUM 聚合函式測試 (double_value 列)
    SELECT
    id,
    SUM(double_value) AS sum_double_value
    FROM
    test_agg_group_single_unique_key
    GROUPBY
    id
    ORDERBY
    id;
    -- AVG 聚合函式測試
    SELECT
    id,
    AVG(big_value) AS avg_big_value
    FROM
    test_agg_group_single_unique_key
    GROUPBY
    id
    ORDERBY
    id;
    -- AVG 聚合函式測試 (double_value 列)
    SELECT
    id,
    AVG(double_value) AS avg_double_value
    FROM
    test_agg_group_single_unique_key
    GROUPBY
    id
    ORDERBY
    id;
    -- COUNT 聚合函式測試
    SELECT
    id,
    COUNT(big_value) AS count_big_value
    FROM
    test_agg_group_single_unique_key
    GROUPBY
    id
    ORDERBY
    id;
    -- COUNT 聚合函式測試 (varchar_value 列)
    SELECT
    id,
    COUNT(varchar_value) AS count_varchar_value
    FROM
    test_agg_group_single_unique_key
    GROUPBY
    id
    ORDERBY
    id;
    -- MAX 聚合函式測試
    SELECT
    id,
    MAX(decimal_value) AS max_decimal_value
    FROM
    test_agg_group_single_unique_key
    GROUPBY
    id
    ORDERBY
    id;
    -- MAX 聚合函式測試 (double_value 列)
    SELECT
    id,
    MAX(double_value) AS max_double_value
    FROM
    test_agg_group_single_unique_key
    GROUPBY
    id
    ORDERBY
    id;
    -- MIN 聚合函式測試
    SELECT
    id,
    MIN(double_value) AS min_double_value
    FROM
    test_agg_group_single_unique_key
    GROUPBY
    id
    ORDERBY
    id;
    -- MIN 聚合函式測試 (big_value 列)
    SELECT
    id,
    MIN(big_value) AS min_big_value
    FROM
    test_agg_group_single_unique_key
    GROUPBY
    id
    ORDERBY
    id;
    -- GROUP_CONCAT 聚合函式測試
    SELECT
    id,
    GROUP_CONCAT(varchar_value ORDERBY varchar_value) AS group_concat_varchar_value
    FROM
    test_agg_group_single_unique_key
    GROUPBY
    id
    ORDERBY
    id;
    -- GROUP_CONCAT 聚合函式測試 (double_value 列)
    SELECT
    id,
    GROUP_CONCAT(double_value ORDERBY double_value) AS group_concat_double_value
    FROM
    test_agg_group_single_unique_key
    GROUPBY
    id
    ORDERBY
    id;










    生成多個Unique Key測試SQL

    -- SUM 聚合函式測試 (double_value 列)
    SELECT
    id,
    big_value,
    SUM(double_value) AS sum_double_value
    FROM
    test_agg_group_multi_unique_key
    GROUPBY
    id, big_value
    ORDERBY
    id;
    -- AVG 聚合函式測試 (decimal_value 列)
    SELECT
    id,
    big_value,
    AVG(decimal_value) AS avg_decimal_value
    FROM
    test_agg_group_multi_unique_key
    GROUPBY
    id, big_value
    ORDERBY
    id;
    -- COUNT 聚合函式測試 (varchar_value 列)
    SELECT
    id,
    big_value,
    COUNT(varchar_value) AS count_varchar_value
    FROM
    test_agg_group_multi_unique_key
    GROUPBY
    id, big_value
    ORDERBY
    id;
    -- MAX 聚合函式測試 (double_value 列)
    SELECT
    id,
    big_value,
    MAX(double_value) AS max_double_value
    FROM
    test_agg_group_multi_unique_key
    GROUPBY
    id, big_value
    ORDERBY
    id;
    -- MIN 聚合函式測試 (big_value 列)
    SELECT
    id,
    big_value,
    MIN(big_value) AS min_big_value
    FROM
    test_agg_group_multi_unique_key
    GROUPBY
    id, big_value
    ORDERBY
    id;
    -- GROUP_CONCAT 聚合函式測試 (varchar_value 列)
    SELECT
    id,
    big_value,
    GROUP_CONCAT(varchar_value ORDERBY varchar_value) AS group_concat_varchar_value
    FROM
    test_agg_group_multi_unique_key
    GROUPBY
    id, big_value
    ORDERBY
    id;




    具體到聚合消除單測設計中,套用二個原則來進行測試:

    1. 聚合規則套用前後,確保查詢輸出結果的一致性。

    2. 聚合規則套用後,確保生成的執行計劃符合預期

    第一部份,聚合規則套用查詢結果前後一致性驗證,這裏使用了Starrocks內建的SQL-tester。

    SQL-tester 是一個用於 StarRocks 的 SQL 測試框架,旨在降低測試用例構建的成本。它使用 SQL 語言編寫測試用例,並具有驗證測試結果的能力。

    SQL-tester 中有兩種型別的測試檔:

  • R 檔:儲存測試用例的 SQL 語句和需要驗證的執行結果。

  • T 檔:儲存測試用例的 SQL 語句。

  • 本次測試中,建立了二個新的測試檔
    starrocks/test/sql/test_agg/R/test_eliminate_agg (實際執行SQL,可以理解為EXE可執行)
    starrocks/test/sql/test_agg/T/test_eliminate_agg(記錄測試SQL,可以理解為TXT可讀)

    這2個檔因為查詢SQL和測試結果內容多,手工輸入成本非常高。所以使用了自動化Python程式來生成R/test_eliminate_agg和T/test_eliminate_agg檔。

    在自動化生成test_eliminate_agg內容時,有一點特別需要註意:執行自動化程式生成test_eliminate_agg測試檔之前,需要註釋關閉此次最佳化。正式測試時再取消註釋開啟此次最佳化,這樣就可以驗證最佳化規則前後的輸出是否一致。

    執行測試命令,完成聚合規則套用前後查詢結果一致性驗證。

    python3 run.py -d sql/test_agg/R -c 1 -v --file_filter=test_eliminate_agg

    第二部份,聚合規則套用後,驗證生成的執行計劃是否符合預期

    執行計劃的測試驗證程式碼放置在 fe/fe-core/src/test/java/com/starrocks/sql/plan 目錄下

    1. 建立執行計劃測試數據表

    fe/fe-core/src/test/java/com/starrocks/sql/plan/PlanTestBase.java
    starRocksAssert.withTable("CREATE TABLE `test_agg_group_single_unique_key` (\n"+
    " `id` int(11) NOT NULL COMMENT \"\",\n"+
    " `big_value` bigint(20) NULL COMMENT \"\",\n"+
    " `double_value` double NULL COMMENT \"\",\n"+
    " `decimal_value` decimal(10, 5) NULL COMMENT \"\",\n"+
    " `varchar_value` varchar(255) NULL COMMENT \"\"\n"+
    ") ENGINE=OLAP\n"+
    "UNIQUE KEY(`id`)\n"+
    "DISTRIBUTED BY HASH(`id`) BUCKETS 10\n"+
    "PROPERTIES (\n"+
    "\"compression\" = \"LZ4\",\n"+
    "\"fast_schema_evolution\" = \"true\",\n"+
    "\"replicated_storage\" = \"true\",\n"+
    "\"replication_num\" = \"1\"\n"+
    ");");
    starRocksAssert.withTable("CREATE TABLE `test_agg_group_multi_unique_key` (\n"+
    " `id` int(11) NOT NULL COMMENT \"\",\n"+
    " `big_value` bigint(20) NULL COMMENT \"\",\n"+
    " `double_value` double NULL COMMENT \"\",\n"+
    " `decimal_value` decimal(10, 5) NULL COMMENT \"\",\n"+
    " `varchar_value` varchar(255) NULL COMMENT \"\"\n"+
    ") ENGINE=OLAP\n"+
    "UNIQUE KEY(`id`, `big_value`)\n"+
    "DISTRIBUTED BY HASH(`id`) BUCKETS 10\n"+
    "PROPERTIES (\n"+
    "\"compression\" = \"LZ4\",\n"+
    "\"fast_schema_evolution\" = \"true\",\n"+
    "\"replicated_storage\" = \"true\",\n"+
    "\"replication_num\" = \"1\"\n"+
    ");");

    2. 驗證生成的執行計劃是否符合預期

    fe/fe-core/src/test/java/com/starrocks/sql/plan/SetTest.java
    @Test
    publicvoidtestEliminateAgg()throws Exception {
    String sql ="SELECT \n"+
    " id, \n"+
    " SUM(big_value) AS sum_big_value\n"+
    "FROM \n"+
    " test_agg_group_single_unique_key\n"+
    "GROUP BY \n"+
    " id\n"+
    "ORDER BY \n"+
    " id;";
    String plan = getVerboseExplain(sql);
    assertContains(plan," 1:Project\n"+
    " | output columns:\n"+
    " | 1 <-> [1: id, INT, false]\n"+
    " | 6 <-> [2: big_value, BIGINT, true]\n"+
    " | cardinality: 1\n"+
    " | \n"+
    " 0:OlapScanNode\n"+
    " table: test_agg_group_single_unique_key, rollup: test_agg_group_single_unique_key\n"+
    " preAggregation: off. Reason: None aggregate function\n");
    sql ="SELECT\n"+
    " id,\n"+
    " big_value,\n"+
    " COUNT(varchar_value) AS count_varchar_value\n"+
    "FROM\n"+
    " test_agg_group_multi_unique_key\n"+
    "GROUP BY\n"+
    " id, big_value\n"+
    "ORDER BY\n"+
    " id;";
    plan = getVerboseExplain(sql);
    assertContains(plan," 1:Project\n"+
    " | output columns:\n"+
    " | 1 <-> [1: id, INT, false]\n"+
    " | 2 <-> [2: big_value, BIGINT, true]\n"+
    " | 6 <-> if[(5: varchar_value IS NULL, 0, 1); args: BOOLEAN,INT,INT; "+
    "result: TINYINT; args nullable: false; result nullable: true]\n"+
    " | cardinality: 1\n"+
    " | \n"+
    " 0:OlapScanNode\n"+
    " table: test_agg_group_multi_unique_key, rollup: test_agg_group_multi_unique_key\n"+
    " preAggregation: off. Reason: None aggregate function\n");
    sql ="SELECT\n"+
    " id,\n"+
    " big_value,\n"+
    " AVG(decimal_value) AS avg_decimal_value\n"+
    "FROM\n"+
    " test_agg_group_multi_unique_key\n"+
    "GROUP BY\n"+
    " id, big_value\n"+
    "ORDER BY\n"+
    " id;";
    plan = getVerboseExplain(sql);
    assertContains(plan," 1:Project\n"+
    " | output columns:\n"+
    " | 1 <-> [1: id, INT, false]\n"+
    " | 2 <-> [2: big_value, BIGINT, true]\n"+
    " | 6 <-> cast([4: decimal_value, DECIMAL64(10,5), true] as DECIMAL128(38,11))\n"+
    " | cardinality: 1\n"+
    " | \n"+
    " 0:OlapScanNode\n"+
    " table: test_agg_group_multi_unique_key, rollup: test_agg_group_multi_unique_key\n"+
    " preAggregation: off. Reason: None aggregate function\n");
    sql ="SELECT\n"+
    " id,\n"+
    " big_value,\n"+
    " COUNT(varchar_value) AS count_varchar_value\n"+
    "FROM\n"+
    " test_agg_group_multi_unique_key\n"+
    "GROUP BY\n"+
    " id, big_value\n"+
    "ORDER BY\n"+
    " id;";
    plan = getVerboseExplain(sql);
    assertContains(plan," 1:Project\n"+
    " | output columns:\n"+
    " | 1 <-> [1: id, INT, false]\n"+
    " | 2 <-> [2: big_value, BIGINT, true]\n"+
    " | 6 <-> if[(5: varchar_value IS NULL, 0, 1); args: BOOLEAN,INT,INT;"+
    " result: TINYINT; args nullable: false; result nullable: true]\n"+
    " | cardinality: 1\n"+
    " | \n"+
    " 0:OlapScanNode\n"+
    " table: test_agg_group_multi_unique_key, rollup: test_agg_group_multi_unique_key\n"+
    " preAggregation: off. Reason: None aggregate function\n");
    }



    透過設計完善、清晰的單元測試,不僅驗證了EliminateAggRule這個規則在不同輸入情況下的行為,還確保了其與整個系統的相容性。這些單元測試為系統的維護和最佳化提供了堅實的基礎。

    總結

    EliminateAggRule類為新的最佳化規則提供了一個規範樣版,從規則定義、驗證到轉換和測試,涵蓋了完整的實作過程,確保最佳化規則的有效性和可靠性。