簡介
PostgreSQL 的開發源自上世紀80年代,它最初是 Michael Stonebraker 等人在美國國防部支持下建立的POSTGRE項目。上世紀末,Andrew Yu 等人在它上面搭建了第一個SQL Parser,這個版本稱爲Postgre95,也是加州大學伯克利分校版本的PostgreSQL的基石[1]。html
咱們今天看到的 PostgreSQL 的優化器代碼主要是 Tom Lane 在過去的20年間貢獻的,使人驚訝的是這20年的改動都是持續一以貫之的,Tom Lane 本人也無愧於「開源軟件十大傑出貢獻者」的稱號。git
可是從今天的視角,PostgreSQL 優化器不是一個好的實現,它用C語言實現,因此擴展性很差;它不是 Volcano 優化模型的[2],因此靈活性很差;它的不少優化複雜度很高(例如Join重排是System R[3]風格的動態規劃算法),因此性能很差。github
不管如何,PostgreSQL 是優化器的優秀實現和創新源頭(想象 Greenplum 和 ORCA 等一系列項目),它的一些優化手段和代碼結構在今天仍然是值得借鑑的,包括:算法
參數化路徑,做用於indexed lookup join
分區裁剪和並行優化
強一致的cardinality estimation保證
本文嘗試快速地瀏覽一遍 PostgreSQL 優化器的代碼,和現代優化器比較優缺點。大部分的 PostgreSQL 優化器代碼來自於 https://github.com/postgres/p... 。 咱們提到現代優化器主要指的是 Apache Calcite 和 ORCA。sql
術語解釋
Datum
Qual
Path
關鍵數據結構
查詢
__Query__: Parse Tree,優化器的輸入
__RangeTblEntry__: Parse Tree的一個節點,它描述了一個數據集的視圖,這個數據集可能來源於某個子查詢、Join、Values或任何一個簡單關係代數表達式。Join實現須要把它的輸入都表達爲 RangeTblEntry (如下簡稱RTE)。
執行計劃
__PlannedStmt__: 執行計劃的頂層節點
__PlannerInfo__: 優化器的上下文信息。它是一個樹形結構,用parent_root變量指向父節點。一個Query包含一個或多個PlannerInfo,每次Join切分一次樹節點。它包含RelOptInfo的指針。
__RelOptInfo__: 優化器的核心數據結構,包含一個子查詢的Path集合等信息。這個概念對應於ORCA的Group或Calcite中的Set。
__Path__: 區別於Parser稱Relational Expression爲Node,Optimizer稱優化時的關係代數爲Path。Path是物理計劃,它包含優化器對於單個關係代數的理解,包括並行度、PathKey和cost。
__PathKey__: 排序屬性。這個概念至關於Volcano中的Physical Property或Calcite中的Trait。由於 PostgreSQL 是單機數據庫,僅用排序屬性就能夠表達全部算法的需求和實現特性。對於分佈式數據庫,一般還須要分佈屬性。
主流程
子查詢上拉數據庫
由於優化的單元(RelOptInfo)是子查詢,合併子查詢能夠簡化優化流程。關聯的過程包括:express
__pull_up_sublinks__: 將可轉換的 ANY/EXISTS 子句轉換爲 (anti-)semi-join 。一些優化器稱這個過程爲de-correlation。
__pull_up_subqueries__: 將可上拉的子查詢上拉到當前查詢,刪除原來的子查詢。若是子查詢是一個 Join ,這個操做至關於打平 binary join 到 multi join。
EquivalenceClass解析編程
Equivalence Class(EC)是 qual 的術語,它指代的是 expression 的等價性。例如,expression數據結構
a = b AND b = c
則稱 {a, b, c} 是一個EC。特別地,在 PostgreSQL 中,expressionless
a = b AND b = 5
只生成簡化的EC:{a = 5} {b = 5}
EC是很關鍵的數據結構,它的應用場景包括:
在 Join 時,EC用來決定 Join Key,它決定了 Outer Join 簡化和PathKey設定
在 Join 時決定 qual 穿越
決定參數化路徑的參數列表
匹配主-外鍵約束,以便優化(Join的)cardinality estimation
EC是一個樹形結構,每一個節點是一個EC,並連接到它合併的父節點上。考慮a = b AND b = c的例子,最後的EC tree表達爲
{a, b, c}
|- {a, b}
|- {b, c}
其中,每一個EC內部的expression稱爲EquivalenceMember(EM)。
生成 EC 的入口是 generate_base_implied_equalities ,它從 query_planner 調入。也就是說,EC是在規劃Join的前一刻生成的,這個階段解析EC的代價最小,可是也決定了EC只能應用於Join優化。
Join重排
(有關Join重排的背景知識能夠參考我以前的文章 SQL優化器原理 - Join重排)
make_rel_from_joinlist是Join重排的入口,當前版本的 PostgreSQL 有三種算法:
你能夠插入一個自定義的Join重排算法
GEQO: Genetic Optimization (基因算法,或遺傳算法[4]),是一種非窮舉的最優化算法實現
Standard:一個略微剪枝的動態規劃算法。
默認在12路及以上的複雜Join中會打開GEQO。能夠在postgresql.conf中修改參數
geqo = on
geqo_threshold = 12
控制GEQO設定。
如今讓咱們檢查 Standard 算法。它的主入口在 join_search_one_level ,每次在已生成的局部計劃的基礎上:
按EC檢查未加入的Join input,加入到生成的局部計劃,這個操做僅產生 Left-deep-tree
從未加入局部計劃的Join input裏找到有EC的兩個input,生成額外的局部計劃,用於生成Bushy-tree
若是當前層找不到任何EC關聯,生成笛卡爾積。
上述描述已經足夠複雜,讓咱們總結一下 Standard 算法:
Standard 算法仍然是一個窮舉的動態規劃算法
它對 a-b/b-a 鏡像去重,同時當EC存在時不考慮笛卡爾積,這些工程上的降級有效下降了搜索複雜度
路徑生成和動態規劃
如上所述,優化過程集中在對子查詢(RelOptInfo)的重建過程,這能夠理解爲邏輯優化過程,這一般是跨關係代數操做符的、比較複雜的優化。事實上 PostgreSQL 也同步在作物理優化。
物理優化就是將 Path 加入 RelOptInfo。考慮Join,物理優化的入口在 populate_joinrel_with_paths。對每一個JoinRel(Join RelOptInfo),考慮:
sort_inner_and_outer:兩邊排序的MergeJoin路徑
match_unsorted_outer:Null-generating side不排序路徑,包括 MergeJoin 和 NestedLoopJoin 。
hash_inner_and_outer:兩邊哈希的HashJoin路徑。
有趣的點是HashJoin路徑(hash_inner_and_outer),顧名思義,它要求Join兩邊都計算哈希值。在生成Path過程當中,須要計算兩邊的參數信息。例如A join B on A.x = B.y,對於A來講,x是參數,對於B是y。若是選定A做爲Probe side,一旦B上有y的索引,每次x的probe將以參數的形式傳遞給y的索引。經過調用 get_joinrel_parampathinfo 來產生參數信息。
路徑生成的入口是add_path,每次生成路徑,須要更新RelOptInfo的最佳路徑和最小代價以便後續動態規劃選擇全局最優。
流程圖
planner
|- subquery_planner 迭代的子查詢優化
|- pull_up_sublinks de-correlation |- pull_up_subqueries 子查詢上拉 |- preprocess_expression Query/PlannerInfo 結構解析,常量摺疊 |- remove_useless_groupby_columns |- reduce_outer_joins Outer Join退化 |- grouping_planner |- plan_set_operations SetOp優化 |- query_planner 子查詢優化主入口 |- generate_base_implied_equalities 生成/合併EC |- make_one_rel Join優化入口 |- set_base_rel_pathlists 生成Join RelOptInfo列表 |- make_rel_from_joinlist Join重排和規劃 |- standard_join_search 標準Join重排算法 |- join_search_one_level |- make_join_rel 生成JoinRel和對應的Path |- create_XXX_paths Grouping、window等其餘expression優化
討論
擴展性和靈活性
首先,PostgreSQL 的優化器代碼能夠說很是複雜,這已經極大限制了它的擴展性和靈活性。若是看一眼這部分代碼的更新日誌,會發現裏面的做者已經只有少數幾我的。
一部分擴展性限制是由編程語言帶來的,由於C語言自己不容易擴展,這意味着大部分時候想要添加一個新的Node或Path變得很不容易,你須要定義一系列的數據結構、Cardinality Estimation邏輯、並行邏輯和Path解釋邏輯。並無相似interface這樣的抽象指導你該怎麼作。雖然,PostgreSQL 的代碼已經寫得很是工整,並且也有不少的文章告訴你該怎麼作(好比 Introduction to Hacking PostgreSQL 和 The Internals of PostgreSQL)。
另外一部分擴展性限制是優化器自己的結構帶來的。現代的優化器基本都是Volcano Model[2]的(例如SQL Server和Oracle,就像他們聲稱的那樣),而 PostgreSQL 沒有實現爲 Volcano Model 這種 Generic purpose,pluggable 的形式。影響包括:
沒法作邏輯和物理優化的互操做。例如前文說到的,一個Join產生的EC必須和它緊跟的 RTE 結合才能產生 IndexedLookupJoin,而不像其餘優化器能夠把這個 EC (它在某種意義上已是物理計劃)下推到合適的邏輯計劃上,指導它作物理計劃轉換。
不容易定製優化規則。
開發者關注的切片太大,開發一個優化規則除了關注優化自己,不得不學習其餘優化規則的數據結構、動態規劃更新、RelOptInfo新建和清理,甚至內存分配自己。
PostgreSQL 仍然提供了部分手寫的 Plugin Point,包括:
可定製的Join重排算法
可定製的PathKey生成算法
定製的Join Path生成算法
等等。
性能
雖然沒有實驗,可是 PostgreSQL 在優化上的性能能夠想像是比較好的,這很大程度是用靈活性交換來的。
首先,不像 Volcano Optimizer ,PostgreSQL 優化器不須要不斷生成中間節點,它的 RelOptInfo 的數量是相對穩定的(約等於Join的數量)。它的最優計劃搜索以 RelOptInfo 爲單位,若是 Join 重排不產生大量 RelOptInfo ,搜索寬度很低。
其次,RelOptInfo 簡化了大量跨 Relational Expression 優化的細節,比起 Calcite 這種按 Relational Expression 來組織等價路徑集合的方案, 它的搜索寬度進一步下降了。從等價集合的數量看, PostgreSQL 的搜索寬度大概比 Calcite 要低一個數量級,固然,如上所述,這是用更多優化可能性做爲交換的。
最後,PostgreSQL 在優化階段糅合了不少業務邏輯,在提升代碼閱讀的難度同時,也相應加快的優化效率。在優化過程當中,PostgreSQL會不間斷地作常量摺疊、PathKey去重、Union打平、子查詢打平……這些操做不會應用在memo裏。
對比 Calcite/Orca ,PostgreSQL 的優化更快,更適合事務性場景。不過我沒法判斷 Calcite/Orca 在作了適當的剪枝和優化規則糅合後,是否也能支持事務場景。
註釋
[1] Brief History of PostgreSQL, https://www.postgresql.org/do...
[2] Graefe, G., & McKenna, W. J. (1993). The Volcano Optimizer Generator: Extensibility and Efficient Search. Proceedings of the Ninth International Conference on Data Engineering, (April), 209–218. https://doi.org/10.1109/ICDE....
[3] Selinger, P. Griffiths, et al. "Access path selection in a relational database management system." Proceedings of the 1979 ACM SIGMOD international conference on Management of data. ACM, 1979.
[4] Steinbrunn, M., Moerkotte, G., & Kemper, A. (n.d.). Optimizing Join Orders, 1–55.