1. 目標
從70年代就有很多關于查詢優化器的技術,很難用一篇短文就把這個領域的深度和廣度表達出來,因此這里會列出基礎技術和這個領域重要工作的采樣
2. 概述
關系查詢語言提供的高抽象聲明的接口來訪問存儲的數據,慢慢的SQL成了關系查詢語言的標準,對于SQL數據庫系統,最重要的兩個組件是查詢優化器和查詢執行引擎。
查詢執行引擎是執行一組物理算子,算子有一個或者多個輸入,一個輸出。典型的物理算子有
(外部)排序-(external) sort
順序掃描-sequential scan
索引掃描-index scan
嵌套連接- nested-loop join
排序連接-sort-merge join
對于一種執行,一個抽象表達是如圖1物理算子樹,圖1中的邊代表數據在物理算子間的數據流轉。物理算子樹和執行計劃(簡稱計劃)用兩個術語分別表示。
執行引擎負責計劃的執行和產出結果。所以查詢執行器的能力取決于它支持的算子。查詢執行技術可以看另外一篇論文《Query Evaluation Techniques for Large Databases》
查詢優化器負責產出供執行引擎使用的計劃,優化器很重要,因為對于一個查詢,有可能會有很多算子計劃樹:
對于給定的查詢有可能有很多等價的計劃,比如
Join(Join(A,B),C)= Join(Join(B,C),A)
對于給定的關系代數,會有很多物理實現,比如join操作就有幾種實現算法。
另外,對于這些計劃的吞吐量和執行耗時會有很大不同,因此對于優化器選擇一個優等的計劃很重要。因此,查詢優化可以看作是一個艱難的搜索問題,為了解決這個這些問題,我們需要提供
計劃的搜索空間。
價估計技術,這些代價用于各個搜索空間的代價計算。
對于執行空間進行搜索的枚舉算法。
一個好的優化器應該是
a. 搜索空間包含了代價低的計劃,b. 代價評估是準確的,c. 搜索枚舉算法是有效的,對于每一個任務都是重要的,這樣就是為什么一個好的優化器是一個重大的挑戰。
3. 舉例:System-R 優化器
System-R的項目,顯著提高了關系查詢優化器的狀態。《Access Path Selection in a Relational Database System》這篇論文里的設計已經被很多商業數據庫使用。這里會展示Select-Project-Join(SPJ)查詢的一些技術設計。
在System-R中對于SPJ查詢,只支持線性Join算子,比如圖2里面的a
因為結合律和交換律,以上這些join都是邏輯相等的。對于join可以使用nested loop 或者 sort-merge的實現。對于scan既可以使用索引掃描(聚簇或者非聚簇),也可以使用順序掃描,最后斷言是要盡可能早執行。
代價模型會對搜索空間的部分或者全部計劃進行代價估計,也會估計算子產出的記錄數,代價模型依賴于以下3點:
關系和索引的統計信息,比如關系的數據頁數,索引的數據頁數,基數等。
評估 predicates 選擇率和 project 輸出數據大小的計算公式。
評估執行計劃算子的 CPU 和 I/O代價的計算公式,這些公式需要考慮輸入數據的統計信息,輸入數據的物理實現方法,輸入數據流的可用排序順序
代價模型使用以上的能力來自底向上的風格來計算計劃,來獲取以下信息:
算子數據輸出流的記錄數
數據輸出流的新創建的排序或者傳遞的已有排序
評估執行算子的代價(包含累計代價)
System-R的迭代算法有兩個重要的設計,動態規劃和interesting orders
動態規劃的本質上是,最優子結構。就是為了得到包含k個join的SPJ查詢,只需要考慮k-1個join的的最優計劃是哪些,之后拓展一個join,就能獲得k個join的SPJ查詢的最優,而不用考慮k-1個join的的最優計劃的細節。動態規劃是很有效的算法,從O(n!)時間復雜度減少到( 2 的n-1次方 )
第二個System-R優化器重要設計是interesting orders,考慮{R1, R2, R3}三個關系的join,join的條件是R1.a, R2.a, R3.a。對于{R1, R2}的join,采用nested loop的代價是x,采用sort-merge的代價是y。x<y。對于{R1, R2, R3}就不會考慮{R1, R2}使用sort-merge的計劃。
假設{R1, R2}使用sort-merge join,那么join結果是根據字段a有序。這樣的有序會大大減少和R3 join的代價。因此提前把{R1, R2}的sort-merge join裁剪掉會導致次優計劃的產生。這個問題產生的本質是{R1, R2} sort-merge join會產生出排序,這個對于后續是有利的,但是nested loop沒有這樣的順序。因此System-R會記錄對于計劃有意義的排序,也叫interesting orders。這個interesting orders在《The Volcano Optimizer Generator: Extensibility and Efficient Search》被稱作是物理屬性。物理屬性不被所有的邏輯計劃共有,但是可以影響子樹的代價。
盡管System-R設計優雅,這個框架不太容易拓展轉換規則來增大搜索空間。所以就有了更利于拓展的架構設計,代價優化,動態規劃,interesting orders深深地影響了數據庫優化器的發展。
4. 搜索空間
就像第二部提到的,搜索空間的大小取決于
保留相同語義的關系代數轉換數量
優化器支持的物理實現算子
關系代數轉換并不一定會減少代價,因此也要經過基于代價的處理器獲得最優計劃。
優化器在優化查詢的不同階段會使用不同的方式去表達查詢。最開始會是一個查詢的抽象語法樹,最后是一個算子計劃樹,中間使用邏輯的算子樹(也叫做查詢樹)來表達關系代數。
有些系統使用面向代數的方式來表現查詢的結構,對于SPJ查詢,這樣的結構就是一個查詢圖,節點代表關系,邊代表join predicates,如圖3。
4.1 算子間交換(Commuting Between Operators)
有很多重要的轉換類探索操作符間的可交換性
4.4.1 歸納join順序
在很多系統中,join算子順序的會被限制來減少搜索空間。比如在System R中,只支持線性join連接順序,笛卡爾的操作要延遲到join之后進行。
因為join滿足交換律和結合律,所以join不一定是線性 join順序,也可以如圖2b的濃密樹的方式,濃密樹的方式需要產生中間物化關系。濃密樹有可能會降低代價,但是會增加搜索空間。
延遲操作笛卡爾積到join之后,同樣會產生性能問題,比如在星型查詢中,在維表之間的笛卡爾積可以大幅減少代價。
在一個可拓展的系統中,join優化迭代可以是自適應的。根據查詢來限制是否使用濃密樹優化,或者是否禁用笛卡爾積。
4.1.2 外部連接和連接
單側的outer join是不對稱的操作符,會保留單邊關系的所有元組。對稱的outer join會保留關系中的左右元組。
(R LOJ S)會保留R關系中所有元組,LOJ就是left outer join。不像自然連接,對于一系列的 outer joins 和自然連接是不能自由交換的。但是當 (R, S)根據斷言自然連接。(S, T)根據斷言外部連接,可以進行下面的結合:
Join(R, S LOJ T) = Join (R,S) LOJ T
如果如上的結合律可以重復執行,我們就可以在LOJ之前獲自然連接的Join (R,S),對于Join (R,S)就可以自由進行交換律。
4.1.3 Group-By 和 Join
SPJ查詢在傳統的執行中,一般會在Group-By之前,有一種優化技術是將Group-By下推到Join之前,這樣可以大幅減少join的元組數量,因為Group-By的每個分區只會產出一行元組。對于SELECT DISTINCT語句也是有效的,因為SELECT DISTINCT是一種特殊的Group-By。下推后的Group-By如果有索引,就可以通過索引低代價執行。
4.2 合并多 Block 查詢到單 Block
4.2.1 合并視圖
設想鏈接查詢,查詢中使用了一個或者多個view,對于view可以進行展開來得到單Block。比如查詢
query Q = Join(R,V) view V = Join(S,T),可以展開為
Join(R, Join(S,T)
展開后可以自由進行交換律。
對于views更復雜時,就不能進行簡單的展開了,比如views包含SELECT DISTINCT,需要將DISTINCT消除或者上拉,這個步驟需要注意保持數據的重復度(duplicates)。當展開后可以自由交換join 或者 Group-By進行優化。就是圖4b 到 圖4a。
4.2.2 合并嵌套子查詢
考慮如下的子查詢
如果按照元組迭代語義來響應這個查詢,對于Dept關系中的每個元組,子查詢都需要執行一遍。如果子查詢沒有引用外部的變量,即非相關子查詢,那么子查詢只需要執行一次就好了。如果子查詢使用了外部的變量,就是相關子查詢,對于上面的查詢,Emp.Emp#就是這個相關變量。對于這種子查詢可以解嵌套和打平(flatten)成一個查詢,如下
如果子查詢有quantifiers(比如 ALL, EXISTS),聚合,或者其他,解嵌套會更復雜一些。最簡單的情況就是上面的情況,相同語義的轉換是
Somijoin(Emp,Dept,Emp.Dept# = Dept. Dept#) =
Project(Join(Emp,Dept), Emp.*)
其中 Join (Emp, Dept) 的鏈接條件是 Emp.Dept# =Dopt . Dept# ,Emp.*表明要保留所有的列。
當子查詢中有聚合的時候會解嵌套更復雜一些,提升聚合時要保留相同的語義,如下查詢
可以轉換成如下
4.3 使用類似Semijoin技術來優化Multi-Block查詢
示例如下查詢
對于上面的查詢,對于E和D之間的連接,因為有條件
E.age <30
AND D.budget > lOOk
所以可以先進行連接,減少E.did的數量,這樣group by的時候就會減少計算量,可以進行如下改造,比如首先創建部分結果partialresult
之后使用上面的 **partialresult **創建 Filter
之后根據 did Filter 和Emp連接,計算平均sal 進行限制
最后再進行查詢
上面用到的技術,可以用在multi-block的包含view的查詢中,主要是想避免仕途或者嵌套子查詢中重復多余的計算。當然也是進行權衡,就是計算view的代價和這個view能減少的代價,哪個收益更高。
上面的轉換優化可以使用Semi-Join,見論文《Cost Based Optimization for Magic: Algebra and Implementatio》,當然也有更簡單的做法,就是將斷言盡量下推,見論文《Query Optimization by Prcdicatc Move-Aroun》
5. 統計信息和代價估計
對于一個查詢,有很多邏輯等價的關系代數表達式,遍歷可能使用的關系代數空間和確定消耗最少資源的計劃是復雜的問題。資源包括 CPU 時間,I/O代價,內存,通信帶寬,也有可能是這些的組合。因此給定查詢的算子樹,能夠準確和高效的評估它的代價是很重要的基礎。代價估計應該是準確的,因為這決定了優化器是否準確。代價估計應該是高效的,因為再優化過程中會循環多次調用。
System-R 基礎代價估計框架如下:
- 收集已存數據的統計信息摘要
- 給定算子和算子輸入流的統計信息摘要,可以計算如下信息
a. 算子輸出流的統計信息摘要
b. 算子執行的代價估計
第2步會迭代的探索算子樹的任意深度來推導每個算子的代價,當每個算子的代價都估計出來后,就能得到整個算子樹的代價。
統計信息屬性和計劃的代價是不一樣的。統計信息屬性是邏輯屬性,就是對于同一個查詢的不同計劃是一樣的。而代價是物理屬性,對于同一個查詢的不同計劃,代價有可能是不同的。
5.1 數據的統計信息摘要
5.1.1 基礎數據的統計信息**
對于每個表,必要的統計信息包含如下幾部分:
- 數據流中的元組數量(據此可以估計scan,join等代價和它們的內存消耗)
- 表使用的物理頁數
- 列的統計信息(估計此列斷言的選擇率)
基于列的數據分布信息是通過直方圖提供的。直方圖有等寬和等高不同的類別,對于等高直方圖,會把所有的元組(n個元組)分成k個桶,每個桶的元組數相同,那么每個桶就有n/k個元組。
在論文《Improved Histograms for Selectivity Estimation of Range Predicate》中論述了這種直方圖對于數據高度或者低度傾斜是有效的。
除了直方圖,min-max值統計信息也很有用,實踐中,次最大和次最小的值經常會用到,因為最大值最小值偏差太多。
每列的基數也是很有用的統計信息。
雖然直方圖提供了單列的統計信息,但是并沒有統計列之間的相關性。為了計算相關性,需要列的聯合分布信息。這個分布空間會很大。因此一些系統只是提供了disctinct的值對的信息.
5.1.2 基礎數據的統計估計
企業級數據庫有很多庫和大量的數據,準確和高效的統計信息很重要,所以就需要進行采樣,挑戰就是如何減少采樣帶來的估計錯誤問題。
5.1.3 統計信息的傳播計算
在基礎數據上進行統計信息是不夠的,因為查詢會包含很多算子,因此,基于統計信息在算子間進行估計很重要。
最簡單的統計估計是selection算子。如果查詢一張表上的A列,同時A列上有直方圖統計信息,可以用直方圖進行統計信息估計。在這個過程中有2點會導致誤差。
- 查詢A列的值是單個桶的子集,這樣會有一定誤差
- 列之間是有相關性的,如果對多個列進行斷言過濾,就假定列之間是獨立的,有的系統會選擇多個列中的最大選擇率的列統計信息
如果沒有統計信息的話,一般按照《Access Path Selection in a Relational Database System. In Readings in Database System》論文進行估算
5.2 代價計算
代價估計階段會計算算子代價。代價模型估計CPU,I/O,在分布式系統中還會估計通信代價。在大多的系統中會組合使用來比較等價計劃。選擇一組合適的指標來計算代價需要仔細斟酌。
早期的研究中除了上面這些屬性來計算代價,還包含使用緩存的命中率,比如對于nested loop join的算子,如果是index scan的內存緩存可以大幅減少代價。代價估算再查詢優化器中一直是很難的部分。
6. 枚舉架構
對于給定的查詢,枚舉算法需要遍歷搜索空間來選擇一個廉價的執行計劃。System-R的設計是只考慮線性join順序的優化。
對于工程師來說,需要設計一種枚舉計劃的架構,可以優雅的添加一種轉換規則或者物理實現來拓寬搜索空間。也可以優雅地調整代價模型。最近的優化器架構是可拓展的設計,拓展性的同時需要權衡高效。
介紹兩種不同的設計類型的優化器,Starburst和Volcano/Cascades,盡管他們有不同的設計,他們的相同點是
- 算子會使用計算代價函數和物理屬性
- 使用規則引擎來改變查詢表達式或者算子
- 預留拓展點,可以改變枚舉框架的行為
6.1 Starburst
使用QGM(Query Graph Model)來表示關系代數
- 查詢改寫階段,使用規則把QGM轉換成另外等價的QGM,在這個階段沒有代價信息
- 計劃優化階段,使用grammar-like的語言,join枚舉器類似于*System-R *的自下向上的枚舉框架
6.2 Volcano/Cascades
Volcano 和 Cascades拓展的架構是Exodus的衍生體。在這些系統里,規則被用來拓展搜索空間。
有兩種類型的規則,轉換規則和實現規則。
為了高效,Volcano 和 Cascades使用自上而下的動態規劃(這個過程也叫memoization)。當執行優化任務的時候會通過邏輯和物理屬性進行校驗任務是否執行過。如果沒執行過,有可能執行轉換規則,物理實現規則,或者使用enforcer來改變數據流的屬性。在優化的每一步使用promise來決定下一步要進行什么操作。
Volcano 和 Cascades框架和Starburst不同的是:
a. 這些系統沒有使用兩種不同的優化階段,因為所有的轉換都是關系代數和基于代價的
b. 從關系代數到物理的映射發生在單獨的步驟
c. 和Starburst的通過鏈式執行規則不同,Volcano 和 Cascades是代價減少為目標的規則使用
7. 基礎拓展
以上是優化器基礎的組件。這部分描述一些高級議題。
7.1 分布式和并行數據庫
分布式數據庫需要引進了節點通信代價,可以通過移動數據和為中間算子選擇節點來拓展搜索空間。早期的工作專注于減少通信代價。隨著時間推移,分布式數據庫架構衍生為副本數據庫或者并行數據庫來擴容。在副本數據庫中保持副本間的數據一致性是一個很重要的議題。
不像分布式數據庫,并行數據庫的行為就像一個單獨的系統,通過并行執行來降低查詢響應時間。
并行處理有幾個好處,比如數據的物理分布有可能根據分區或者備份至幾個節點,允許獨立的處理數據。并行處理可以并行處理獨立的算子或者流水線(通過把生產者和消費者分布在不同節點上)。壞處就是并行需要在節點之間通信來交換數據
高效地對物理算子進行調度給優化器帶來新的挑戰。
7.2 用戶自定義函數
存儲過程(也叫用戶定義函數)在關系型數據庫廣泛使用。提供了一種機制,可以減少客戶端和服務器的通信。
隨之也帶來了問題,比如用戶定義函數的代價評估。見《Optimization of Queries with Userdefined Predicate》
解決了用戶定義函數的優化問題只是第一步,還有ADTs對于查詢怎么進行優化。
7.3 物化視圖
物化視圖是視圖的結果進行物化存儲。之后被優化器使用。對于優化器的挑戰是給定一部分物化視圖,怎么用物化視圖來改寫查詢和查詢使用無話改寫的高效性。
7.4 其他優化項
排序優化《Fundamental Techniques for Order Optimization》等
8. 結論
優化不僅僅是轉換和查詢等價,構建優秀的SQL轉換,代價估計,枚舉框架都很有挑戰。核心挑戰問題依然在,了解目前現有的優化技術對于未來對優化做貢獻也是必要的。