calcite物化檢視詳解

zzzzMing發表於2022-03-20

概述

物化檢視和檢視類似,反映的是某個查詢的結果,但是和檢視僅儲存SQL定義不同,物化檢視本身會儲存資料,因此是物化了的檢視。

當使用者查詢的時候,原先建立的物化檢視會註冊到優化器中,使用者的查詢命中物化檢視後,會直接去物化檢視拿資料(快取),提高執行速度,是典型的空間換時間。

image-20220302084200055

本篇文章會先介紹《Optimizing Queries Using Materialized Views: A Practical, Scalable Solution》如果改寫物化檢視,接下來會說明 calcite 的物化檢視改寫邏輯。

物化檢視有三個需要解決的問題:

  • View design: determining what views to materialize, including how to store and index them.

第一個問題,是要選擇哪些資料需要進行物化,這個通常是由使用者自己決定的,我們能做的就是收集使用者的統計資訊,展示高頻的表資訊,查詢謂詞或者子查詢,輔助使用者判斷哪些資料需要物化。
另外 calcite 也有一個 Lattices 的功能,可以自動收集統計星型模型和雪花模型的表,自動構建部分 cube 的物化檢視。

  • View maintenance: efficiently updating materialized views when base tables are updated.

第二個問題,當原始表更新後,如果更新物化檢視表。
當原始表增加資料或更新資料後,是直接增量更新到物化檢視,還是全量更新到物化檢視,實時觸發還是延遲觸發抑或是定時觸發,這些都是需要考慮的點。

  • View exploitation: making efficient use of materialized views to speed up query processing.

如果利用物化檢視進行加速,主要是對使用者查詢進行改寫,使查詢命中物化檢視然後進行改寫。
主要有三種改寫方法,基於語法的改寫,基於規則的改寫 ,基於結構的改寫。

這裡只介紹論文中的基於結構的改寫演算法,對其他幾種有興趣的同學可以看看阿里的這篇:一文詳解物化檢視改寫

物化檢視重寫演算法

《Optimizing Queries Using Materialized Views: A Practical, Scalable Solution》 一文介紹了一種物化檢視重寫演算法,Calcite 有一種 SPJG 重寫演算法,正式基於這篇 paper 實現的。

這篇論文,主要介紹了一種 SPJG檢視(即 Join-Select-Project-GroupBy 檢視) rewrite 的方法,即前面提到的基於結構的改寫演算法。

基於結構的改寫演算法,會提取查詢和物化檢視的各項結構資訊,包括,表,列資訊,謂詞資訊,表示式等等,然後用一個演算法這些結構資訊進行驗證,物化檢視補償等等操作,最終完成改寫。

join-select-project (SPJ) views and queries 改寫

物化檢視能夠被使用者查詢改寫的先決條件:

  1. equivalence classes (等價類,即 SQL 相等的列,是後續做進一步判斷的基礎)
  2. The view contains all rows needed by the query expression.( 物化檢視 view 包含的行,需要覆蓋使用者 query 查詢所需要的行。)
  3. All required rows can be selected from the view.( 使用者 query 需要的資料,都能夠在 view 中查詢得到。)
  4. Can output expressions be computed (query sql 中的表示式都能過被 view 計算出來)
  5. Do rows occur with correct duplication factor(有相同的重複語義,比如 distinct)

接下來,將說明前面的幾種先決條件判斷方法。

等價類(equivalence classes)

等價類(equivalence classes),即一組等價的列的集合 ,是基於等價謂詞求出來的。如果在一條 SQL 中,兩個列包含在一個等價類中,那麼就說明這兩個列在這一條 SQL 中是完全等價的。

另外等價類可以基於傳遞性,判斷兩個列等價,例如 A=date_format(now(), '%Y%m%D') 和 B=date_format(now(), '%Y%m%D'),因為函式是確定性的,可以得到 A=B,如果我們還有 inner join 的條件 B=C,可以進一步得到 A=B=C。

根據 calcite 的程式碼,equivalence classes 的計算方式,是通過計算所有的等價謂詞資訊 ,將所有相等的 columns 按照 HashMap<column ,Set> 的方式儲存起來。

根據等價類的特性,物化檢視 view 中一些缺失的列可以通過等價類獲得等價的列作為替代。

條件1 : The view contains all rows needed by the query expression

為什麼

為什麼需要判斷物化檢視 view 包含的行,覆蓋使用者 query 查詢所需要的行,這裡舉個簡單的例子:

//物化檢視
CREATE MATERIALIZED VIEW mv1 AS
SELECT column1,column2,column3,column4
FROM test1
WHERE column1=column2
  AND column3=column4;

//使用者查詢
SELECT column1,column2,column3,column4
FROM test1
WHERE column1=column2;

這裡物化檢視的條件為 column1=column2 & column3=column4,而使用者 query 的條件為 column1=column2。物化檢視多了一個謂詞條件column3=column4,那麼物化檢視的 SQL 查詢結果資料大概率是比使用者 query 的資料少的,所以使用者查詢無法將自身改寫成這個物化檢視。

如果驗證條件

令Wq表示查詢表示式的謂詞,而Wv表示檢視表示式的謂詞。我們只要確定, (select * from T1,T2,…,Tm where Wq) 和(select * from T1,T2, …,Tm where Wv) 的情況下,Wq是 Wv的子集。即我們需要確定Wq⇒Wv是否成立。

然後我們對 Wq 和 Wv 進行拆解,將謂詞重寫為Wq = Pq1 ∧ Pq2 ∧...∧Pqm ,和Wv = Pv1∧Pv2 ∧...∧ Pvn。

paper 設計一種演算法來對 sql 進行匹配,將所有的謂詞分為三種,等價謂詞 PE,範圍謂詞 PN,剩餘謂詞PU。

mv1

公式 (A⇒C) ⇒ (AB⇒C) 對任意謂詞 A、B、C 成立。換句話說,如果我們能推匯出 A 本身包含 C 那麼肯定 A 和 B 一起就包含 C 。為了確定查詢所需的所有行是否存在於檢視中,我們可以應用以下三個測試:

(PEq ⇒ PEv ) (Equijoin subsumption test)

(PEq∧ PRq ⇒ PRv) (Range subsumption test)

(PEq ∧PUq ⇒ PUv ) (Residual subsumption test)

Equijoin subsumption test(等價包含測試)

Equijoin subsumption test 要求檢視中所有相等的列在查詢中也必須相等(反之則不然,因為使用者 query 可以補償到物化檢視中)。通過為使用者查詢 query 和物化檢視計算列等價類來實現此測試,假設檢視包含(A=B 和 B=C),查詢包含(A=C 和 C=B)。

  • 使用者 query 的等價類為:<A , B , C>
  • 物化檢視等價類為:<A , C , B>

即使實際的謂詞不匹配,它們在邏輯上也是等價的,因為它們都暗示 A=B=C。這一點將等價類摘出來就可以很清楚得觀測到。 即通過使用等價類可以正確捕獲傳遞性的影響。

另外如果使用者 query 包含更多的條件,比如多一個 C=D ,這個條件在最後的改寫過程中,也可以加到物化檢視中,這個謂詞就稱為補償謂詞(即物化檢視沒有,補償給物化檢視)。

Range subsumption test(範圍謂詞包含測試)

範圍包含測試有一個簡單的演算法。 將查詢中的每個等價類與一個範圍相關聯,該範圍指定了等價類中列的下限和上限, 兩個邊界最初都未初始化。

然後我們一一考慮範圍謂詞,找到包含引用列的等價類,並根據需要設定或調整其範圍。 如果謂詞是 (Ti.Cp <= c) 型別,我們將上限設定為其當前值和 c 的最小值。 如果是 (Ti.Cp >= c) 型別,我們將上限設定為其當前值和 c 的最大值。 (Ti.Cp < c) 形式的謂詞被視為 (Ti.Cp <= c-Δ),其中 c-Δ 表示列 Ti.Cp 的域中 c 之前的最小值。 (Ti.Cp > c) 形式的謂詞被視為 (Ti.Cp <= c+Δ)。 最後,形如 (Ti.Cp = c) 的謂詞被視為 (Ti.Cp >= c)∧ (Ti.Cp <= c)。 對檢視重複相同的過程。

舉個簡單的例子:

//物化檢視
CREATE MATERIALIZED VIEW mv1 AS
SELECT column1,column2,column3,column4,column5
FROM test1
WHERE column3 > 10;

//查詢
SELECT column1,column2,column3,column4,column5
FROM test1
WHERE column3 > 50;
  • 物化檢視範圍謂詞:{column3 } ∈ (10, +∞)
  • 使用者查詢謂詞:{column3 } ∈ (50, +∞)

query 中的範圍謂詞在物化檢視的範圍之內,在最終改寫中,需要將 column3 > 50 這個條件作為補償謂詞加到物化檢視中。

Residual subsumption test(剩餘謂詞包含測試)

剩餘謂詞就是出去上面兩種謂詞後,剩下的謂詞(比如 a like "str"),只能通過精確匹配,判斷使用者 query 和物化檢視是否屬於同一種型別的謂詞 & 有相同的值,確定它們是否包含。

條件2 : All required rows can be selected from the view

經過調解1 的驗證,就可以確定使用者查詢的謂詞,包含物化檢視的謂詞,接下來要確保補償謂詞能夠在物化檢視執行。

三種補償謂詞為:

  1. 比較物化檢視和使用者 query 等價類時獲得的列等價謂詞。 比如謂詞:(o_orderdate = l_shipdate)。
  2. 根據物化檢視範圍謂詞,檢查使用者 query 範圍謂詞時獲得的範圍謂詞。 比如謂詞: ({p_partkey, l_partkey} <= 160) 。
  3. 查詢中不匹配的剩餘謂詞。 比如謂詞:(l_quantity*l_extendedprice > 100)。

要使條件2 驗證通過:

  1. 將檢視等價類與查詢等價類進行比較時構造補償列等價謂詞。 嘗試將每個列引用對映到物化檢視輸出列(通過檢視等價類),若對映失敗,則改寫失敗。
  2. 通過比較列範圍來構造補償範圍謂詞。 嘗試將每個列引用對映到物化檢視的輸出列(使用查詢等價類)。 若對映失敗,則改寫失敗。
  3. 查詢物化檢視中缺少的 query 的剩餘謂詞。 嘗試將每個列引用對映到物化檢視輸出列(使用查詢等價類)。 若對映失敗,則改寫失敗。

條件3 : Can output expressions be computed

檢查是否可以從檢視中計算查詢的所有輸出表示式,即檢查附加謂詞是否可以正確計算。

如果輸出表示式是常量,則只需將常量複製到輸出中即可。 如果輸出表示式是簡單的列引用,需要檢查是否可以將其對映(使用查詢等效類)到檢視的輸出列。

對於其他表示式,我們首先檢查檢視輸出是否包含完全相同的表示式(考慮到列的等效性)。如果是這樣,則僅將輸出表示式替換為對匹配檢視輸出列的引用。 如果不是,我們將檢查表示式的源列是否可以全部對映到檢視輸出列,即是否可以從(簡單)輸出列中計算出完整的表示式。

條件 4 :Do rows occur with correct duplication factor?

這個點比較直觀,不細說。

rewrite

如果完成了前面提到的物化檢視 rewrite 判斷,以及謂詞補償,那進行重寫其實就比較簡單了,直接生成新的 logical plan 節點然後替換就可以了。

這部分通常是做到 CBO 優化器中,一條 sql 可以被多個物化檢視改寫,甚至可以被一個物化檢視改寫多次,在 CBO 優化器中可以利用其特性,根據 COST 模型找出最佳 plan。

故重點是如果判斷物化檢視 view 的邏輯計劃能否進行 rewrite ,是否進行補償。

舉例

例1

建立檢視:
Create view V2 with schemabinding as
Select l_orderkey, o_custkey, l_partkey,
l_shipdate, o_orderdate,
l_quantity*l_extendedprice as gross_revenue
From dbo.lineitem, dbo.orders, dbo.part
Where l_orderkey = o_orderkey
And l_partkey = p_partkey
And p_partkey >= 150
And o_custkey >= 50 and o_custkey <= 500
And p_name like ‘%abc%’

使用者 Query:
Select l_orderkey, o_custkey, l_partkey,
l_quantity*l_extendedprice
From lineitem, orders, part
Where l_orderkey = o_orderkey
And l_partkey = p_partkey
And l_partkey >= 150 and l_partkey <= 160
And o_custkey = 123
And o_orderdate = l_shipdate
And p_name like ‘%abc%’
And l_quantity*l_extendedprice > 100

Step 1: 計算等價類

  • View equivalence classes: {l_orderkey, o_orderkey}, {l_partkey, p_partkey}, {o_orderdate}, {l_shipdate}
  • Query equivalence classes: {l_orderkey, o_orderkey}, {l_partkey, p_partkey}, {o_orderdate, l_shipdate}

Step 2: 檢查 View 等價類

這裡 view 相比 query ,少了 {o_orderdate, l_shipdate},所以後續需要加上這個等價謂詞。

Step 3 : 計算範圍謂詞

  • View ranges: {l_partkey, p_partkey} ∈ (150, +∞), {o_custkey} ∈ (50, 500)
  • Query ranges: {l_partkey, p_partkey} ∈ (150, 160), {o_custkey} ∈ (123, 123)

Step 4 : 計算 Query 範圍謂詞

Query 中 {l_partkey, p_partkey} 的範圍 (150, 160) 在相應的 View 相關謂詞的範圍內,但上限不匹配,因此我們必須在 View 中新增補償謂詞 ({l_partkey, p_partkey} <= 160)。 {o_custkey} 上的範圍 (123, 123) 在 View 也在相應的檢視謂詞的範圍內,但邊界不匹配,因此我們必須新增補償謂詞 (o_custkey >= 123) 和 (o_custkey <= 123),可以簡化為 (o_custkey = 123)。

Step 5 : 計算 View 剩餘謂詞

  • View residual predicate: p_name like ‘%abc%’
  • Query residual predicate: p_name like ‘%abc%’, l_quantity*l_extendedprice > 100

該 View只有一個剩餘謂詞 p_name like ‘%abc%’,它也存在於 Query 中。 必須新增補償額外的剩餘謂詞 l_quantity*l_extendedprice > 100。

該檢視通過了所有測試,因此我們得出結論,它包含所有必需的行。 必須新增的補償謂詞是 (o_orderdate = l_shipdate)、({p_partkey, l_partkey} <= 160)、(o_custkey = 123) 和 (l_quantity*l_extendedprice > 100.00)。

Calcite 物化檢視實現

前面提到物化檢視的三個問題:

  1. 哪些資料需要被物化
  2. 如何保持原始表與物化表的同步關係
  3. 如何進行改寫

這裡主要是看 calcite 如果進行改寫。那麼衍生出兩個問題:

  1. 物化檢視如何被定義 & 註冊
  2. 物化檢視改寫流程

兩種改寫演算法

Calcite 有兩種物化檢視 rewrite 的實現,一種是 SubstitutionVisitor 及其擴充套件 MaterializedViewSubstitutionVisitor(使用的 rule 是 unifyrule系列規則),另一種則是MaterializedViewRule

  • SubstitutionVisitor: based on pattern, and bottom-up visit.
  • MaterializedViewRule: analyze semantics with relnode, such as SPJA

SubstitutionVisitor 的優勢和缺陷:

  1. 輕鬆擴充套件:可以方便新建一個自定義模式來匹配並使用 mv 重寫查詢的計劃。
  2. 支援out join
  3. 不止 'SPJA' :SubstitutionVisitor支援更多模式,如:Sort。

缺陷:

  1. 不支援 JOIN 補償,這在MaterializedViewRule中實現
  2. 受join順序影響,eg:query(a join b), mv(b join a)

第二種MaterializedViewRule,實現並擴充自 [GL01] 所述演算法,其實現方式提取 query 的relnode結構(謂詞資訊,列資訊等),然後進行驗證,構建補償謂詞並完成重寫,這是一種更加先進的方式,但目前其適用範圍比 SubstitutionVisitor更窄,比如join 型別必須為 inner-join。

第一種SubstitutionVisitor 重寫,在 calcite 基本快要被廢棄了,這裡主要介紹 MaterializedViewRule的主要實現方式,即前面介紹的演算法。

calcite 物化檢視原理

MV 是怎麼註冊的

在 model json 檔案中,可以配置 Materize View SQL,然後 Calcite 會將 MV 的 SQL 解析成 RelNode 而後儲存到 VolcanoPlanner#materializations 欄位中

mv2

每次執行的時候,並不會直接迴圈所有快取中的物化檢視,而是會通過VolcanoPlanner#registerMaterializations() 找出被命中的物化檢視,然後進一步判斷是否該寫。

  protected void registerMaterializations() {
    // Avoid using materializations while populating materializations!
    final CalciteConnectionConfig config =
        context.unwrap(CalciteConnectionConfig.class);
    if (config == null || !config.materializationsEnabled()) {
      return;
    }

    // Register rels using materialized views.
    //通過 `SubstitutionVisitor` ,獲取可能進行重寫的物化檢視
    final List<Pair<RelNode, List<RelOptMaterialization>>> materializationUses =
        RelOptMaterializations.useMaterializedViews(originalRoot, materializations);
    for (Pair<RelNode, List<RelOptMaterialization>> use : materializationUses) {
      RelNode rel = use.left;
      Hook.SUB.run(rel);
      registerImpl(rel, root.set);
    }

		//通過 table 引用構建的圖演算法,計算剩餘可能被重寫的物化檢視
    // Register table rels of materialized views that cannot find a substitution
    // in root rel transformation but can potentially be useful.
    final Set<RelOptMaterialization> applicableMaterializations =
        new HashSet<>(
            RelOptMaterializations.getApplicableMaterializations(
                originalRoot, materializations));
    for (Pair<RelNode, List<RelOptMaterialization>> use : materializationUses) {
      applicableMaterializations.removeAll(use.right);
    }
    for (RelOptMaterialization materialization : applicableMaterializations) {
      RelSubset subset = registerImpl(materialization.queryRel, null);
      RelNode tableRel2 =
          RelOptUtil.createCastRel(
              materialization.tableRel,
              materialization.queryRel.getRowType(),
              true);
      registerImpl(tableRel2, subset.set);
    }

    // Register rels using lattices.
    //通過 lattices 計算物化檢視
    final List<Pair<RelNode, RelOptLattice>> latticeUses =
        RelOptMaterializations.useLattices(
            originalRoot, ImmutableList.copyOf(latticeByName.values()));
    if (!latticeUses.isEmpty()) {
      RelNode rel = latticeUses.get(0).left;
      Hook.SUB.run(rel);
      registerImpl(rel, root.set);
    }
  }

計算物化檢視和使用者 query 表引用計算

這裡的條件基本就一個:物化檢視至少包含一個 Query 的表引用。

表引用情況有三種:

  • 全匹配
  • 物化檢視的表引用是使用者 query 表引用的子集
  • 使用者 query 表引用是物化檢視的表引用的子集

這三種情況都有可能進行改寫,但物化檢視至少包含一個使用者 query 的表引用。

通過 lattices 註冊

第三個是使用 lattices 進行註冊,lattices 是 Calcite 針對星型模型和雪花模型推出的一種物化檢視框架,主要可以物化星型模型中部分 cube ,能夠智慧收集資訊並智慧決定物化哪些維度等。有點類似 kylin 的思路。

CBO 註冊邏輯(registerImpl)

上面註冊邏輯中可以看到,計算出 List<Pair<RelNode, List>> 後, 會呼叫 registerImpl註冊。這裡涉及到 calcite 中 rule 註冊的邏輯,Calcite 用 VolcanoPlanner 模型來進行 CBO 優化,這裡不詳細說明流程,具體情況可以看這裡:Apache Calcite 優化器詳解(二)

簡單說就是每個新的 Relnode 都會生成一個 Relset 和 RelSubset。每次新建一個 RelSubset ,都會遍歷所有可以 match 該 RelSubset 的 Rule(物化檢視改寫也是一種 rule),建立一個 VolcanoRuleMatch 物件(會記錄 RelNode、RelOptRuleOperand 等資訊,RelOptRuleOperand 中又會記錄 Rule 的資訊)。記錄 importance 資訊並將這個 VolcanoRuleMatch 新增到對應的 RuleQueue 中。

實際進行優化的findBestExp()方法中,主要就是遍歷 RuleQueue ,實現 DP 優化演算法。

第二個問題,MV 改寫是怎樣的

VolcanoPlanner CBO 優化的最後一個階段,就是通過 findBestExp() 方法找到最佳的 plan,這裡會先通過 registerMaterializations() 註冊 物化檢視相關 rule,這也就是上面說到的內容。

而實際註冊的 rule ,基本都是 AbstractMaterializedViewRule 的子類 ,這個 Rule 有多個衍生的 rule,MaterializedViewProjectFilterRuleMaterializedViewProjectJoinRule 等適配不同的 query。在上一步中,會尋找 equel 的 relnode 和 match,然後呼叫這些 rule 迭代 rel 資訊,替換 MV 後生成新的 RelNode,完成物化檢視的 sql rewrite 過程。

觸發入口,是在每個 rule 的 onMatch() 方法中,但實際執行,是在 AbstractMaterializedViewRule#perform() 中。

  /**
   * Rewriting logic is based on "Optimizing Queries Using Materialized Views:
   * A Practical, Scalable Solution" by Goldstein and Larson.
   *
   * <p>On the query side, rules matches a Project-node chain or node, where node
   * is either an Aggregate or a Join. Subplan rooted at the node operator must
   * be composed of one or more of the following operators: TableScan, Project,
   * Filter, and Join.
   *
   * <p>For each join MV, we need to check the following:
   * <ol>
   * <li> The plan rooted at the Join operator in the view produces all rows
   * needed by the plan rooted at the Join operator in the query.</li>
   * <li> All columns required by compensating predicates, i.e., predicates that
   * need to be enforced over the view, are available at the view output.</li>
   * <li> All output expressions can be computed from the output of the view.</li>
   * <li> All output rows occur with the correct duplication factor. We might
   * rely on existing Unique-Key - Foreign-Key relationships to extract that
   * information.</li>
   * </ol>
   *
   * <p>In turn, for each aggregate MV, we need to check the following:
   * <ol>
   * <li> The plan rooted at the Aggregate operator in the view produces all rows
   * needed by the plan rooted at the Aggregate operator in the query.</li>
   * <li> All columns required by compensating predicates, i.e., predicates that
   * need to be enforced over the view, are available at the view output.</li>
   * <li> The grouping columns in the query are a subset of the grouping columns
   * in the view.</li>
   * <li> All columns required to perform further grouping are available in the
   * view output.</li>
   * <li> All columns required to compute output expressions are available in the
   * view output.</li>
   * </ol>
   *
   * <p>The rule contains multiple extensions compared to the original paper. One of
   * them is the possibility of creating rewritings using Union operators, e.g., if
   * the result of a query is partially contained in the materialized view.
   */
  protected void perform(RelOptRuleCall call, Project topProject, RelNode node) {
    final RexBuilder rexBuilder = node.getCluster().getRexBuilder();
    final RelMetadataQuery mq = call.getMetadataQuery();
    final RelOptPlanner planner = call.getPlanner();
    final RexExecutor executor =
        Util.first(planner.getExecutor(), RexUtil.EXECUTOR);
    final RelOptPredicateList predicates = RelOptPredicateList.EMPTY;
    final RexSimplify simplify =
        new RexSimplify(rexBuilder, predicates, executor);

    final List<RelOptMaterialization> materializations =
        planner.getMaterializations();
    //呼叫 `isValidPlan(topProject, node, mq)`,驗證 RelNode 是否滿足 rewrite 先覺條件,這個每個 rule 都有不同的實現,比如 join 需要通過 `RelMetadataQuery` 獲取 RelNode 的 NodeType。
    if (!materializations.isEmpty()) {
      // 1. Explore query plan to recognize whether preconditions to
      // try to generate a rewriting are met
      if (!isValidPlan(topProject, node, mq)) {
        return;
      }

      // 2. Initialize all query related auxiliary data structures
      // that will be used throughout query rewriting process
      // Generate query table references
      //獲取所有 table 引用,以便後續各種操作,這一步基本都是通過RelMetadataQuery獲取各種資訊
      final Set<RelTableRef> queryTableRefs = mq.getTableReferences(node);
      if (queryTableRefs == null) {
        // Bail out
        return;
      }

      // Extract query predicates
      //提取 查詢的所有謂詞,等價謂詞和剩餘謂詞
      final RelOptPredicateList queryPredicateList =
          mq.getAllPredicates(node);
      if (queryPredicateList == null) {
        // Bail out
        return;
      
      final RexNode pred =
          simplify.simplifyUnknownAsFalse(
              RexUtil.composeConjunction(rexBuilder,
                  queryPredicateList.pulledUpPredicates));
      //將查詢兩種謂詞包裝成 RexNode,等價謂詞(左)和剩餘謂詞(右)
      final Pair<RexNode, RexNode> queryPreds = splitPredicates(rexBuilder, pred);

      // Extract query equivalence classes. An equivalence class is a set
      // of columns in the query output that are known to be equal.
      //提取 query 的等價類(equivalence class),equivalence class 即 query output 中,已知等價的 columns 的集合
      final EquivalenceClasses qEC = new EquivalenceClasses();
      //遍歷等價謂詞,並將等價謂詞的列都存起來
      for (RexNode conj : RelOptUtil.conjunctions(queryPreds.left)) {
        assert conj.isA(SqlKind.EQUALS);
        RexCall equiCond = (RexCall) conj;
        qEC.addEquivalenceClass(
            (RexTableInputRef) equiCond.getOperands().get(0),
            (RexTableInputRef) equiCond.getOperands().get(1));
      }

      // 3. We iterate through all applicable materializations trying to
      // rewrite the given query
      //遍歷所有給定物化檢視,並嘗試重寫,一個 relnode 可以對應多個 materialization view
      for (RelOptMaterialization materialization : materializations) {
        //獲取 view 的 RelNode,並提取各種基礎資訊,比如所有的 RelTableRef
        RelNode view = materialization.tableRel;
        Project topViewProject;
        RelNode viewNode;
        //materialization.queryRel 表示註冊的 MV sql
        //materialization.tableRel 表示註冊的 MV 的 table name relnode
        //這裡找到 topViewProject
        if (materialization.queryRel instanceof Project) {
          topViewProject = (Project) materialization.queryRel;
          viewNode = topViewProject.getInput();
        } else {
          topViewProject = null;
          viewNode = materialization.queryRel;
        }

        // Extract view table references
        final Set<RelTableRef> viewTableRefs = mq.getTableReferences(viewNode);
        if (viewTableRefs == null) {
          // Skip it
          continue;
        }

        // Filter relevant materializations. Currently, we only check whether
        // the materialization contains any table that is used by the query
        //第一個過濾條件,這裡只檢查 MV 是否包含使用者 query 所要的表
        // TODO: Filtering of relevant materializations can be improved to be more fine-grained.
        boolean applicable = false;
        for (RelTableRef tableRef : viewTableRefs) {
          if (queryTableRefs.contains(tableRef)) {
            applicable = true;
            break;
          }
        }
        if (!applicable) {
          // Skip it
          continue;
        }

        //跟步驟 1 一樣,不過這裡對 MV query 進行校驗(Valid)
        // 3.1. View checks before proceeding
        if (!isValidPlan(topViewProject, viewNode, mq)) {
          // Skip it
          continue;
        }

        // 3.2. Initialize all query related auxiliary data structures
        // that will be used throughout query rewriting process
        // Extract view predicates
        //跟步驟 2 類似,不過這裡獲取的是 MV 的各種謂詞資訊,表示式
        final RelOptPredicateList viewPredicateList =
            mq.getAllPredicates(viewNode);
        if (viewPredicateList == null) {
          // Skip it
          continue;
        }
        final RexNode viewPred = simplify.simplifyUnknownAsFalse(
            RexUtil.composeConjunction(rexBuilder,
                viewPredicateList.pulledUpPredicates));
        //獲取 MV 的兩種謂詞,等價謂詞(左)和剩餘謂詞(右)
        final Pair<RexNode, RexNode> viewPreds = splitPredicates(rexBuilder, viewPred);

        // Extract view tables
        //用 view 和 query 的所有表進行匹配,有三種匹配結果
        //MatchModality.COMPLETE:所有 MV view 的 tables 和 query 的 tables 都一致
        //MatchModality.QUERY_PARTIAL:使用者查詢是 MV 檢視的子集 的情況,檢查 MV 和 query 是否保持相同的基數,主要是獲取所有等價謂詞,tableref 等資訊,然後 compensatePartial 進行判斷 
        //MatchModality.VIEW_PARTIAL:MV 檢視是使用者 query 的子集,直接呼叫compensateViewPartial(不同 rule 有不同實現)進行重寫新增缺失的 view,重寫成功則更新 view 相關資訊,後面再對 view 進行補償
        MatchModality matchModality;
        Multimap<RexTableInputRef, RexTableInputRef> compensationEquiColumns =
            ArrayListMultimap.create();
        if (!queryTableRefs.equals(viewTableRefs)) {
          //進行補償
          // We try to compensate, e.g., for join queries it might be
          // possible to join missing tables with view to compute result.
          // Two supported cases: query tables are subset of view tables (we need to
          // check whether they are cardinality-preserving joins), or view tables are
          // subset of query tables (add additional tables through joins if possible)
          if (viewTableRefs.containsAll(queryTableRefs)) {
            //通過這個來控制補償機制
            matchModality = MatchModality.QUERY_PARTIAL;
            //對於使用者查詢是 MV 檢視的子集 的情況,主要是獲取所有等價謂詞,tableref 等資訊,然後 compensatePartial 進行判斷
            final EquivalenceClasses vEC = new EquivalenceClasses();
            for (RexNode conj : RelOptUtil.conjunctions(viewPreds.left)) {
              assert conj.isA(SqlKind.EQUALS);
              RexCall equiCond = (RexCall) conj;
              vEC.addEquivalenceClass(
                  (RexTableInputRef) equiCond.getOperands().get(0),
                  (RexTableInputRef) equiCond.getOperands().get(1));
            }
            //確認 query 是否能夠使用 MV view 進行 rewrite
            if (!compensatePartial(viewTableRefs, vEC, queryTableRefs,
                    compensationEquiColumns)) {
              // Cannot rewrite, skip it
              continue;
            }
          } else if (queryTableRefs.containsAll(viewTableRefs)) {
            //若MV 檢視是使用者 query 的子集,直接呼叫compensateViewPartial(不同 rule 有不同實現)進行重寫
            // 重寫目標是將 QUery 有但 view 沒有的表新增到 view 中,重寫成功則更新 view 相關資訊,後續會再使用
            matchModality = MatchModality.VIEW_PARTIAL;
            ViewPartialRewriting partialRewritingResult = compensateViewPartial(
                call.builder(), rexBuilder, mq, view,
                topProject, node, queryTableRefs, qEC,
                topViewProject, viewNode, viewTableRefs);
            if (partialRewritingResult == null) {
              // Cannot rewrite, skip it
              continue;
            }
            // Rewrite succeeded
            view = partialRewritingResult.newView;
            topViewProject = partialRewritingResult.newTopViewProject;
            viewNode = partialRewritingResult.newViewNode;
          } else {
            // Skip it
            continue;
          }
        } else {
          matchModality = MatchModality.COMPLETE;
        }

        // 4. We map every table in the query to a table with the same qualified
        // name (all query tables are contained in the view, thus this is equivalent
        // to mapping every table in the query to a view table).
        //獲取 query 中所有具有相等 qualified name 的 RelTableRef,並儲存起來

        final Multimap<RelTableRef, RelTableRef> multiMapTables = ArrayListMultimap.create();
        for (RelTableRef queryTableRef1 : queryTableRefs) {
          for (RelTableRef queryTableRef2 : queryTableRefs) {
            if (queryTableRef1.getQualifiedName().equals(
                queryTableRef2.getQualifiedName())) {
              multiMapTables.put(queryTableRef1, queryTableRef2);
            }
          }
        }

        // If a table is used multiple times, we will create multiple mappings,
        // and we will try to rewrite the query using each of the mappings.
        // Then, we will try to map every source table (query) to a target
        // table (view), and if we are successful, we will try to create
        // compensation predicates to filter the view results further
        // (if needed).
        //
        //如果一張表被使用多次,將對該表建立多重對映,並將重寫使用者 query 使用這些對映。
        //然後嘗試將 QUERY 的 table 對映到 MV view 的 table。
        //如果可以進行對映,那麼下一步將填充剩餘謂詞
        final List<BiMap<RelTableRef, RelTableRef>> flatListMappings =
            generateTableMappings(multiMapTables);
        //遍歷 query table -> view table 的對映集合
        for (BiMap<RelTableRef, RelTableRef> queryToViewTableMapping : flatListMappings) {
          // TableMapping : mapping query tables -> view tables
          // 4.0. If compensation equivalence classes exist, we need to add
          // the mapping to the query mapping
          //如果存在compensation equivalence(補償等價類),我們需要將這些加到 query mapping 中
          final EquivalenceClasses currQEC = EquivalenceClasses.copy(qEC);
          if (matchModality == MatchModality.QUERY_PARTIAL) {
            for (Entry<RexTableInputRef, RexTableInputRef> e
                : compensationEquiColumns.entries()) {
              // Copy origin
              RelTableRef queryTableRef = queryToViewTableMapping.inverse().get(
                  e.getKey().getTableRef());
              RexTableInputRef queryColumnRef = RexTableInputRef.of(queryTableRef,
                  e.getKey().getIndex(), e.getKey().getType());
              // Add to query equivalence classes and table mapping
              currQEC.addEquivalenceClass(queryColumnRef, e.getValue());
              queryToViewTableMapping.put(e.getValue().getTableRef(),
                  e.getValue().getTableRef()); // identity
            }
          }

          // 4.1. Compute compensation predicates, i.e., predicates that need to be
          // enforced over the view to retain query semantics. The resulting predicates
          // are expressed using {@link RexTableInputRef} over the query.
          // First, to establish relationship, we swap column references of the view
          // predicates to point to query tables and compute equivalence classes.
          //計算補償謂詞,生成的謂詞在查詢中使用 {@link RexTableInputRef} 表示。
          final RexNode viewColumnsEquiPred = RexUtil.swapTableReferences(
              rexBuilder, viewPreds.left, queryToViewTableMapping.inverse());
          final EquivalenceClasses queryBasedVEC = new EquivalenceClasses();
          for (RexNode conj : RelOptUtil.conjunctions(viewColumnsEquiPred)) {
            assert conj.isA(SqlKind.EQUALS);
            RexCall equiCond = (RexCall) conj;
            queryBasedVEC.addEquivalenceClass(
                (RexTableInputRef) equiCond.getOperands().get(0),
                (RexTableInputRef) equiCond.getOperands().get(1));
          }
          //計算得到補償的等價謂詞和剩餘謂詞
          // TODO : computeCompensationPredicates 是如果提取補償謂詞的
          Pair<RexNode, RexNode> compensationPreds =
              computeCompensationPredicates(rexBuilder, simplify,
                  currQEC, queryPreds, queryBasedVEC, viewPreds,
                  queryToViewTableMapping);
          //若補償謂詞為空,並且允許 union rewrite,那麼進行 union 的改寫
          if (compensationPreds == null && generateUnionRewriting) {
            // Attempt partial rewriting using union operator. This rewriting
            // will read some data from the view and the rest of the data from
            // the query computation. The resulting predicates are expressed
            // using {@link RexTableInputRef} over the view.
            //嘗試加上 union operator 進行 sql rewrite
            compensationPreds = computeCompensationPredicates(rexBuilder, simplify,
                queryBasedVEC, viewPreds, currQEC, queryPreds,
                queryToViewTableMapping.inverse());
            if (compensationPreds == null) {
              // This was our last chance to use the view, skip it
              continue;
            }
            RexNode compensationColumnsEquiPred = compensationPreds.left;
            RexNode otherCompensationPred = compensationPreds.right;
            assert !compensationColumnsEquiPred.isAlwaysTrue()
                || !otherCompensationPred.isAlwaysTrue();

            // b. Generate union branch (query).
            //進行改寫,生成 union branch
            final RelNode unionInputQuery = rewriteQuery(call.builder(), rexBuilder,
                simplify, mq, compensationColumnsEquiPred, otherCompensationPred,
                topProject, node, queryToViewTableMapping, queryBasedVEC, currQEC);
            if (unionInputQuery == null) {
              // Skip it
              continue;
            }

            // c. Generate union branch (view).
            // We trigger the unifying method. This method will either create a Project
            // or an Aggregate operator on top of the view. It will also compute the
            // output expressions for the query.
            final RelNode unionInputView = rewriteView(call.builder(), rexBuilder, simplify, mq,
                matchModality, true, view, topProject, node, topViewProject, viewNode,
                queryToViewTableMapping, currQEC);
            if (unionInputView == null) {
              // Skip it
              continue;
            }

            // d. Generate final rewriting (union).
            //分別對 query 和 view 進行 rewrite,最後再處理
            final RelNode result = createUnion(call.builder(), rexBuilder,
                topProject, unionInputQuery, unionInputView);
            if (result == null) {
              // Skip it
              continue;
            }
            call.transformTo(result);
          } else if (compensationPreds != null) {
            RexNode compensationColumnsEquiPred = compensationPreds.left;
            RexNode otherCompensationPred = compensationPreds.right;

            // a. Compute final compensation predicate.
            //判斷等價補償謂詞和剩餘補償謂詞,是否都返回 true
            if (!compensationColumnsEquiPred.isAlwaysTrue()
                || !otherCompensationPred.isAlwaysTrue()) {
              // All columns required by compensating predicates must be contained
              // in the view output (condition 2).
              // 條件 2 判斷,補償謂詞所需的列都在 view 中
              // 使用對映的方式,將表引用及對應列引用對映到 View 中,若返回 null 則失敗,即改寫失敗
              List<RexNode> viewExprs = topViewProject == null
                  ? extractReferences(rexBuilder, view)
                  : topViewProject.getChildExps();
              // For compensationColumnsEquiPred, we use the view equivalence classes,
              // since we want to enforce the rest
              //如果等價謂詞不總為 TRUE,使用檢視等價類,因為要強制執行剩餘部分
              if (!compensationColumnsEquiPred.isAlwaysTrue()) {
                //這裡 rewrite 的作用,基本上就是把之前 column 的對映再轉回來
                compensationColumnsEquiPred = rewriteExpression(rexBuilder, mq,
                    view, viewNode, viewExprs, queryToViewTableMapping.inverse(), queryBasedVEC,
                    false, compensationColumnsEquiPred);
                if (compensationColumnsEquiPred == null) {
                  // Skip it
                  continue;
                }
              }
              // For the rest, we use the query equivalence classes
              //對於剩餘謂詞,使用 query 等價類
              if (!otherCompensationPred.isAlwaysTrue()) {
                otherCompensationPred = rewriteExpression(rexBuilder, mq,
                    view, viewNode, viewExprs, queryToViewTableMapping.inverse(), currQEC,
                    true, otherCompensationPred);
                if (otherCompensationPred == null) {
                  // Skip it
                  continue;
                }
              }
            }
            // 合併補償的等價謂詞和剩餘謂詞,獲得最終需要在 view 上面執行的剩餘謂詞
            final RexNode viewCompensationPred =
                RexUtil.composeConjunction(rexBuilder,
                    ImmutableList.of(compensationColumnsEquiPred,
                        otherCompensationPred));

            // b. Generate final rewriting if possible.
            // First, we add the compensation predicate (if any) on top of the view.
            // Then, we trigger the unifying method. This method will either create a
            // Project or an Aggregate operator on top of the view. It will also compute
            // the output expressions for the query.
            //生成最終重寫,
            //1. 新增補償謂詞到 view
            //2. 呼叫統一方法,生成一個 project 或一個 Aggregate operator on top of view。
            RelBuilder builder = call.builder().transform(c -> c.withPruneInputOfAggregate(false));
            RelNode viewWithFilter;
            // 如果最終的補償謂詞並非總是 true,生成 viewWithFilter 的時候需要加上 FIlter
            if (!viewCompensationPred.isAlwaysTrue()) {
              RexNode newPred =
                  simplify.simplifyUnknownAsFalse(viewCompensationPred);
              viewWithFilter = builder.push(view).filter(newPred).build();
              // No need to do anything if it's a leaf node.
              if (viewWithFilter.getInputs().isEmpty()) {
                call.transformTo(viewWithFilter);
                return;
              }
              // We add (and push) the filter to the view plan before triggering the rewriting.
              // This is useful in case some of the columns can be folded to same value after
              // filter is added.
              // 觸發重寫前,新增 filter 並 PUSH 到 view plan。
              Pair<RelNode, RelNode> pushedNodes =
                  pushFilterToOriginalViewPlan(builder, topViewProject, viewNode, newPred);
              topViewProject = (Project) pushedNodes.left;
              viewNode = pushedNodes.right;
            } else {
              //如果 filter 總是 TRUE ,那麼只需要生成 view
              viewWithFilter = builder.push(view).build();
            }
            //進行重寫,生成最終的 view
            //這裡 Agg 和 Join rule 都有自己的實現,主要看 join
            final RelNode result = rewriteView(builder, rexBuilder, simplify, mq, matchModality,
                false, viewWithFilter, topProject, node, topViewProject, viewNode,
                queryToViewTableMapping, currQEC);
            if (result == null) {
              // Skip it
              continue;
            }
            call.transformTo(result);
          } // end else
        }
      }
    }
  }

這種方式有以下限制:

  • Aggregate 或 Join 型別的 sql ,其子查詢節點的 root ,必須為以下幾種:TableScan, Project, Filter, and Join,且僅支援 inner join 型別的 sql。

Join 型別進行改寫,條件如下:

  1. MV 的 logical plan 中 join operator ,需包含所有 query 中,join operator 需要的所有資料。
  2. 擁有補償謂詞所需的列
  3. 所有的輸出表示式都可以從檢視的輸出中計算出來。
  4. 所有輸出行都以正確的重複因子出現。

而對於 aggregate MV,需要滿足以下:

  1. MV 的 logical plan 中 Aggregate operator ,需包含所有 query Aggregate operator 需要的所有資料。
  2. 擁有補償謂詞所需的列
  3. 查詢中的分組列是檢視中分組列的子集。
  4. 檢視輸出中提供了進一步分組所需的所有列。
  5. 檢視輸出中提供了計算輸出表示式所需的所有列。

基本都是最上面提到的條件。

說明:

等價類:EquivalenceClasses:a set of equivalent columns

具體流程如下:

首先,對使用者 query 進行預處理,包括校驗,提取資訊,構建等價類等。

  1. 呼叫 isValidPlan(topProject, node, mq),驗證 RelNode 是否滿足 rewrite 先決定條件,這個join 和 Aggregate rule 都有不同的實現,比如 join 僅支援 relnode 節點型別:TableScan - Project - Filter - Inner Join。
  2. 通過 RelMetadataQuery,生成 sql rewrite 過程中,所需要的一些輔助資料(所有 table 表引用等)。提取使用者 query 的謂詞條件,分為兩部分:等價謂詞,剩餘謂詞。表引用,表示式(常量表示式,函式等)等等。
  3. 構建等價類EquivalenceClasses,等價類是從等價謂詞中提取的 columns 集合

然後,遍歷 relnode 的所有物化檢視,嘗試重寫並註冊新 relnode

  1. 首先,和 query 一樣,獲取物化檢視的基本資訊,校驗,構建等價類。

  2. 嘗試計算補償謂詞,若不能則跳過檢視,根據 query 和 MV 物化檢視的 table 表引用數量,分三種情況。

    1. MatchModality.COMPLETE:所有 MV view 的 tables 和 query 的 tables 都一致,什麼都不做。
    2. MatchModality.QUERY_PARTIAL:使用者查詢 表引用是 MV 檢視的子集 的情況,檢查 MV 和 query 是否保持相同的基數(join 的情況),主要是獲取所有等價謂詞,tableref 等資訊,然後 compensatePartial 進行判斷是否物化檢視能進行 review。即最上面介紹的特殊 case,需要根據唯一健,外健等約束進行判斷。
    3. MatchModality.VIEW_PARTIAL:MV 檢視表引用是使用者 query 的子集,會呼叫compensateViewPartial(不同 rule 有不同實現)檢測業務 query 能否使用 view 進行重寫,可以的話會將 Query 中多出來的表(若存在謂詞那便新增這些補償謂詞)新增到 view 中並新生成一個物化檢視(初步改寫)。
  3. 經過上述步驟,可以認定 Query 和 View 具有相同的表引用關係(或 view 有多的表引用但不影響)。

  4. 開始計算補償謂詞,需要在檢視 view 上執行補償謂詞以保證 query 語義。

    1. 建立 query 表引用 -> view 表引用的對映集合List<BiMap<RelTableRef, RelTableRef>>
    2. 物化檢視的等價謂詞,交換 view column 指向的 TABLE 為 QUERY 的 table ,然後再計算等價類。呼叫 computeCompensationPredicates 計算補償謂詞,包括等價補償謂詞和剩餘補償謂詞。交換 view 的表引用,是為了方便判斷:檢視 view 的查詢條件都為 query 條件的子集。如果 view 條件不為 query 的子集,補償謂詞為空。
    3. 若計算得到的補償謂詞為空並且允許 union 重寫,則進行 union 重寫(最開始 union 型別是不支援改寫的,這裡是 calcite 的擴充套件。這部分先忽略。)
    4. 先進行優化,如果補償謂詞條件用 AND 相連後,結果總為 true ,類似 where 1=1,那麼省略部分操作,生成新的 view。
    5. 否則進行判斷,All columns required by compensating predicates must be contained in the view output(條件二) 。具體做法是將補償謂詞的列進行對映。
    6. 生成 view ,並將補償謂詞資訊加到這個 view 中,呼叫統一方法,生成一個 project 或一個 Aggregate operator on top of view。最後再呼叫 rewriteView (不同 join 有不同實現)生成最終重寫後的 relnode,再將新的 relnode 註冊。等待後續 CBO 進一步優化。

小結

本篇文章主要介紹了物化檢視的功能,作用,以及實現過程中需要解決的三個問題。主要介紹物化檢視如何進行改寫,需要實現哪些條件等等。

然後主要說明 Calcite 如何註冊物化檢視和實現 SPJG 改寫演算法。

以上~

Optimizing Queries Using Materialized Views: A Practical, Scalable Solution

一文詳解物化檢視改寫

Materialized Views

Apache Calcite 優化器詳解(二)

相關文章