解析器生成壹個解析樹,後續子系統可以從普通SQL語句中讀取該解析樹。
例如下面的查詢:
解析樹的根節點是parsenodes.h中定義的【SelectStmt】(JavaScript:void(0))結構
選擇查詢的元素編號與解析樹的相應元素相同。例如,(1)是第壹個目標列表的壹項,即表的“id”列,(4)是WHERE子句,依此類推。
因為解析器在生成解析樹時只檢查輸入的語法,所以只有在查詢中有語法錯誤時才會返回錯誤。
解析器不檢查輸入查詢的語義。例如,即使查詢包含不存在的表名,解析器也不會返回錯誤。語義檢查由解析器完成。
解析器運行解析器生成的解析樹的語義分析,並生成查詢樹。
查詢樹的根是parsenodes.h中定義的【query】(JavaScript:void(0)結構;該結構包含其相應查詢的元數據,例如該命令的類型(SELECT、INSERT或other)和幾個葉;每個葉子形成壹個列表或樹,存儲每個特定子句的數據。
查詢樹簡要描述如下。
重寫器是實現規則系統的系統,如果需要,它根據存儲在pg_rules系統目錄中的規則轉換查詢樹。
PostgreSQL中的視圖是使用規則系統實現的。當視圖由CREATE VIEW命令定義時,相應的規則會自動生成並存儲在目錄中。
假設已經定義了以下視圖,並且相應的規則存儲在pg_rules系統目錄中。
當發出如下所示視圖的查詢時,解析器將創建壹個解析樹,如圖所示。
在這個階段,重寫器將範圍表節點視為子查詢的解析樹,即存儲在pg_rules中的相應視圖。
規劃器從重寫器接收查詢樹,並生成可由執行器最有效處理的(查詢)規劃樹。
PostgreSQL中的規劃器基於純成本優化;它不支持基於規則的優化和提示。該計劃程序是RDBMS中最復雜的子系統。
與其他RDBMS壹樣,PostgreSQL中的EXPLAIN命令顯示計劃樹本身。如下圖。
他對應的計劃樹:
每個計劃節點都有執行者需要處理的信息。在單表查詢的情況下,執行器從計劃樹的末尾到根進行處理。
PostgreSQL的查詢優化是基於成本的。成本是無量綱值。它們不是絕對的績效指標,而是比較運營相對績效的指標。成本由costsize.c中定義的函數進行估計。執行器執行的所有操作都有相應的成本函數。例如,順序掃描和索引掃描的開銷分別由cost_seqscan()和cost_index()估計。
有三種成本,啟動成本、執行成本和總成本。其中總成本=啟動成本+執行成本。
順序掃描的開銷是通過cost_seqscan()函數估計的。
在postgresql.conf文件中設置Seq_page_cost、cpu_tuple_cost和cpu_operator_cost,默認值分別為1.0、0.01和0.0025。Ntuple和Npage是表中所有元組和所有頁面的數量。
從運行成本估算中可以看出,PostgreSQL假設所有頁面都將從存儲中讀取;換句話說,PostgreSQL不考慮掃描的頁面是否在* * *共享緩沖區中。
盡管PostgreSQL支持壹些索引方法,如BTree、GiST、GIN和BRIN,但索引掃描的成本是通過使用壹個常見的成本函數來估計的:cost_index()。
索引掃描的啟動成本是讀取索引頁以訪問目標表中第壹個元組的成本,由以下公式定義:
Hindex是索引樹的高度。
索引掃描的運行成本是表和索引的cpu成本和IO(輸入/輸出)成本之和:
前三項成本定義如下:
其中在postgresql.conf文件中設置cpu_index_tuple_cost和random_page_cost(默認分別為0.005和4.0);Qual_op_cost大致是index的評估成本,值為0.0025。選擇性選擇性是指指定WHERE子句與索引的搜索範圍之比;它是壹個從0到1的浮點數。
查詢謂詞的選擇率通過直方圖邊界值和高頻值進行估計,這些值存儲在系統目錄pg _ staticstics中,可以通過pg_stats視圖進行查詢。
表中每列的高頻值成對存儲在pg_stats視圖的most_common_vals和most_common_freqs中。
排序操作中將使用排序路徑。排序操作包括order by、合並聯接的預處理操作以及其他功能。函數cost_sort()用於估計排序操作的成本。如果所有元組都可以放在工作內存中,那麽排序操作將選擇快速排序。否則,將創建臨時文件並使用文件合並和排序算法。
排序路徑的起始成本是目標表的排序成本,因此成本為O(Nsort)* Log 2(n sort),其中n sort是具有排序的元組的數量。排序路徑的運行成本是讀取已排序元組的成本,因此成本為O(n sort)。
PostgreSQL中的計劃程序執行三個步驟:
估算成本時,訪問路徑是處理單元。例如順序掃描、索引掃描、排序和各種連接操作都有其對應的路徑。訪問路徑僅在計劃員創建查詢計劃樹時使用。最忌諱的訪問路徑數據結構是relation.h中定義的路徑結構,相當於順序掃描。所有其他路徑訪問都基於此結構。
在創建計劃樹之前,計劃員將在計劃信息中預處理查詢書。預處理有許多步驟,本節討論與單表查詢處理相關的主要步驟。
規劃者估計所有可能的訪問路徑的成本,然後選擇成本最低的壹條。
在最後壹步中,計劃員根據成本最低的路徑生成計劃樹。
計劃樹的根節點是plannodes.h中定義的Plannedstmt結構,該結構包含19個字段,包括4個代表性字段:
計劃樹包含各種計劃節點。PlanNode是所有計劃節點的基類,其他計劃節點將包含PlanNode結構。例如,順序掃描節點SeqScanNode包含壹個PlanNode和壹個整型變量scanrelid。PlanNode包含14個字段,以下是7個代表性字段:
在單表查詢的例子中,執行器從計劃樹中取出計劃節點,按照自下而上的順序進行處理,並調用節點對應的處理函數。
每個計劃節點都有相應的功能,用於執行該節點的相應操作。這些函數位於src/backend/executor目錄中。
理解執行器如何工作的最好方法是閱讀explain命令的輸出。
我們可以從下往上閱讀explain的結果,看看執行器是如何工作的。
第6行:首先,執行器通過nodeSeqscan.c中定義的函數執行順序掃描操作
第4行:然後,執行器通過nodeSort.c中定義的函數對順序掃描的結果進行排序
執行器在處理查詢時將使用工作內存和臨時緩沖區,這兩者都是在內存中分配的。如果查詢無法在內存中完成,將使用臨時文件。
使用explain with Analyze選項,將實際執行要解釋的命令,並顯示實際結果行、實際執行時間和實際內存使用情況。
在第6行中,explain命令顯示執行器使用了壹個10000KB的臨時文件。將在base/pg_tmp子目錄中臨時創建臨時文件,並遵循以下命令規則:{“pgsql _ tmp“}+{用於創建此文件的postgres進程PID。{基於0的序列號}
例如,臨時文件pgsql_tmp8903.5是postgres進程創建的第六個臨時文件,pid為8903。
PostgreSQL支持三種連接操作,即嵌套循環連接、合並連接和哈希連接。在pg中,嵌套循環連接和合並連接有幾種變體。
這三種連接方式支持pg中的所有連接操作,如註入內連接、左/右外連接、全外連接等。
循環嵌套連接不需要任何啟動成本,因此:啟動成本= 0。
運行成本與內外維度的乘積成正比,即運行成本為O(outer * Ninner),outer和Niner分別是外表和內表中元組的數量。運行成本定義如下:
Couter和Cinner分別是內表和外表順序掃描的價格。
循環嵌套連接的成本總是估計的,但這種連接操作在實踐中很少使用,因為它有幾種更有效的變體。
在上述循環嵌套連接中,每次讀取外部表面中的元組時,都需要掃描內部標準中的所有元組。用每個外部元組掃描全表中的內部標準是昂貴的。pg支持物化嵌套循環連接,可以降低內標全表掃描的成本。
在運行嵌套循環連接之前,執行器將使用臨時元組存儲模塊掃描內部表並將內部表元組加載到工作文件或臨時文件中。在處理內部表元組時,臨時元組存儲比緩沖區管理器更有效,尤其是當所有元組都可以放入工作內存時。
Qg內部提供了壹個臨時元組存儲模塊,可用於各種操作,如五花、創建批量混合哈希連接等。這個模塊包含壹系列函數,都在tuplestore.c中。這些函數用於從工作內存或臨時文件中讀取和寫入元組。工作內存是否是臨時文件取決於要存儲的元組總數。
上面顯示了執行者要執行的動作,執行者對這些計劃節點的處理過程如下:
第7行:執行器使用順序掃描來物化內部表TBL _ b..
第4行:執行器執行嵌套循環join操作,tbl_a作為外表面,tbl_b作為內表面。
如果內部表上有索引,並且該索引可用於搜索滿足連接條件的元組,則在為外表面上的每個元組搜索內部標準中的匹配元組時,計劃員將考慮使用該索引進行直接搜索,而不是順序掃描。這種變體稱為索引嵌套循環連接,如下圖所示。雖然這種變體被稱為“索引嵌套循環連接”,但該算法基本上只需要在表面上循環壹次,因此連接操作的執行非常高效。
與嵌套循環聯接不同,合並聯接只能用於自然聯接和等值聯接。
函數initial_cost_merge_join()和final_cost_merge_join()用於估計合並連接的成本。
合並連接的啟動成本是內部表和外部表的排序成本之和,因此其啟動成本為:
這裏Nouter和Niner分別是外標和內標中的元素個數,運行成本為O(Nouter+Niner)。
下圖是合並連接的示意圖。
如果所有元組都可以存儲在內存中,則排序操作可以在內存中執行,否則,將使用臨時文件。
第9行:執行程序對內部表tbl_b進行排序,並使用順序掃描(第11行)。
第6行:執行器對外部tbl_a進行排序,並使用順序掃描(第8行)。
第4行:執行器執行合並連接操作,排序後的tbl_a在外部,排序後的tbl_b在內部。
與嵌套循環連接類似,合並連接也支持物化合並連接和物化內表,這使得內表掃描更加高效。
以下是物化合並連接的解釋結果。很容易發現與普通合並連接的區別是第9行:materialize。
與合並聯接類似,哈希聯接只能用於自然聯接和等值聯接。
PostgreSQL中散列連接的行為隨表的大小而變化。如果標簽足夠小(確切地說,內部表的大小不超過工作內存的25%),則哈希連接是簡單的兩階段內存哈希連接,否則將使用具有傾斜批次的混合哈希連接。
內存中的哈希連接在work_mem中處理,在pg中,哈希表區域稱為處理批處理。壹個批處理將有多個哈希槽,這些槽在內部稱為桶,桶的數量由nodeHash.c中定義的ExecChooseHashTableSize()函數確定。桶的數量是2的整數次方。
內存哈希連接有兩個階段,即構造階段和檢測階段。在構造階段,內存表中的所有元組都將被插入到處理批處理中;在檢測階段,每個觀察元組將與處理批中的內部表元組進行比較,如果滿足連接條件,則將連接這兩個元組。
當內部表中的所有元組不能存儲在工作內部表中的單個處理批處理中時,pg使用帶有傾斜批處理的混合哈希連接算法,這是混合哈希連接的壹種變體。
在第壹次構建和檢測階段,postgresql準備了多個批次,宇通的數量也差不多。處理批次的數據由函數ExecChooseHashTableSize()確定,該函數是2的整數冪。Wisdom在工作內存中分配壹個處理批次,而其他批次以臨時文件的形式創建。屬於這些批次的元組將通過臨時元組存儲功能寫入相應的文件。
為了得到最佳的計劃樹,計劃者必須考慮索引和各種連接方法之間的所有可能組合。如果表的數量超過壹定水平,由於組合爆炸,該過程的成本將變得如此昂貴,以至於根本不可行。
如果表的數量小於12,計劃員可以使用動態規劃來獲得最佳計劃。