862
技術社區[雲棲]
PostgreSQL SQL 語言:並行查詢
本文檔為PostgreSQL 9.6.0文檔,本轉載已得到原譯者彭煜瑋授權。
當優化器判斷對於某一個特定的查詢,並行查詢是最快的執行策略時,優化器將創建一個查詢計劃。該計劃包括一個 Gather 節點。下麵是一個簡單的例子:
EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%';
QUERY PLAN
-------------------------------------------------------------------------------------
Gather (cost=1000.00..217018.43 rows=1 width=97)
Workers Planned: 2
-> Parallel Seq Scan on pgbench_accounts (cost=0.00..216018.33 rows=1 width=97)
Filter: (filler ~~ '%x%'::text)
(4 rows)
在所有的情形下,Gather節點都隻有一個子計劃,它是將被並行執行的計劃的一部分。如果 Gather節點 位於計劃樹的最頂層,那麼整個查詢將並行執行。如果它位於計劃樹的其他位置,那麼隻有查詢的那一部分會並行執行。在上麵的例子中,查詢隻訪問了一個表,因此除Gather節點本身之外隻有一個計劃節點。因為該計劃節點是 Gather節點的孩子節點,所以它會並行執行。
使用 EXPLAIN命令, 你能看到規劃器選擇的工作者數量。當查詢執行期間到達Gather節點時,實現用戶會話的進程將會請求和規劃器選中的工作者數量一樣多的後台工作者進程 。任何時候能夠存在的後台工作者進程的總數由max_worker_processes限製,因此一個並行查詢可能會使用比規劃中少的工作者來運行,甚至有可能根本不使用工作者。最優的計劃可能取決於可用的工作者的數量,因此這可能會導致不好的查詢性能。如果這種情況經常發生,那麼就應當考慮一下提高max_worker_processes的值,這樣更多的工作者可以同時運行;或者降低max_parallel_workers_per_gather,這樣規劃器會要求少一些的工作者。
為一個給定並行查詢成功啟動的後台工作者進程都將會執行Gather 節點的後代計劃的一部分。這些工作者的領導者也將執行該計劃,不過它還有一個額外的任務:它還必須讀取所有由工作者產生的元組。當整個計劃的並行部分隻產生了少量元組時,領導者通常將表現為一個額外的加速查詢執行的工作者。反過來,當計劃的並行部分產生大量的元組時,領導者將幾乎全用來讀取由工作者產生的元組並且執行Gather節點上層計劃節點所要求的任何進一步處理。在這些情況下,領導者所作的執行並行部分的工作將會很少。
有幾種設置會導致查詢規劃器在任何情況下都不生成並行查詢計劃。為了讓並行查詢計劃能夠被生成,必須配置好下列設置。
- max_parallel_workers_per_gather必須被設置為大於零的值。這是一種特殊情況,更加普遍的原則是所用的工作者數量不能超過max_parallel_workers_per_gather所配置的數量。
- dynamic_shared_memory_type必須被設置為除none之外的值。並行查詢要求動態共享內存以便在合作的進程之間傳遞數據。
此外,係統一定不能運行在單用戶模式下。因為在單用戶模式下,整個數據庫係統運行在單個進程中,沒有後台工作者進程可用。
如果下麵的任一條件為真,即便對一個給定查詢通常可以產生並行查詢計劃,規劃器都不會為它產生並行查詢計劃:
- 查詢要寫任何數據或者鎖定任何數據庫行。如果一個查詢在頂層或者 CTE 中包含了數據修改操作,那麼不會為該查詢產生並行計劃。這是當前實現的一個限製,未來的版本中可能會有所改進。
- 查詢可能在執行過程中被暫停。隻要在係統認為可能發生部分或者增量式執行,就不會產生並行計劃。例如:用DECLARE CURSOR創建的遊標將永遠不會使用並行計劃。類似地,一個FOR x IN query LOOP .. END LOOP形式的 PL/pgsql 循環也永遠 不會使用並行計劃,因為當並行查詢進行時,並行查詢係統無法驗證循環中的代碼執行起來是安全的。
- 使用了任何被標記為PARALLEL UNSAFE的函數的查詢。大多數係統定義的函數都被標記為PARALLEL SAFE,但是用戶定義的函數默認被標記為PARALLEL UNSAFE。參見Section 15.4中的討論。
- 該查詢運行在另一個已經存在的並行查詢內部。例如,如果一個被並行查詢調用的函數自己發出一個 SQL 查詢,那麼該查詢將不會使用並行計劃。這是當前實現的一個限製,但是或許不值得移除這個限製,因為它會導致單個查詢使用大量的進程。
- 事務隔離級別是可串行化。這是當前實現的一個限製。
即使對於一個特定的查詢已經產生了並行查詢計劃,在一些情況下執行時也不會並行執行該計劃。如果發生這種情況,那麼領導者將會自己執行該計劃在Gather節點之下的部分,就好像Gather節點不存在一樣。上述情況將在滿足下麵的任一條件時發生:
- 因為後台工作者進程的總數不能超過max_worker_processes,導致不能得到後台工作者進程。
- 客戶端發送了一個執行消息,並且消息中要求取元組的數量不為零。執行消息可見擴展查詢協議中的討論。因為libpq當前沒有提供方法來發送這種消息,所以這種情況隻可能發生在不依賴 libpq 的客戶端中。如果這種情況經常發生,那在它可能發生的會話中設置 max_parallel_workers_per_gather是一個很好的主意,這樣可以避免產生連續運行時次優的查詢計劃。
- 事務隔離級別是可串行化。這種情況通常不會出現,因為當事務隔離級別是可串行化時不會產生並行查詢計劃。不過,如果在產生計劃之後並且在執行計劃之前把事務隔離級別改成可串行化,這種情況就有可能發生。
因為每個工作者隻執行完成計劃的並行部分,所以不可能簡單地產生一個普通查詢計劃並使用多個工作者運行它。每個工作者都會產生輸出結果集的一個完全拷貝,因而查詢並不會比普通查詢運行得更快甚至還會產生不正確的結果。相反,計劃的並行部分一定被查詢優化器在內部當作一個部分計劃。也就是說,一定要這樣來創建計劃,使得每個將執行該計劃的進程隻產生輸出行的一個子集,這樣可以保證每個需要被輸出的行剛好會被合作進程產生一次。
3.1. 並行掃描
當前唯一一種被修改用於並行查詢的掃描類型是順序掃描。因此在並行計劃中的驅動表將總是被使用並行順序掃描進行掃描。關係的塊將被劃分給合作進程。一次發放一個文件塊,這樣對於關係的訪問仍然保持為順序訪問。在請求一個新頁麵之前,每一個進程將訪問分配給它的頁麵上的每一個元組。
3.2. 並行連接
驅動表將被使用嵌套循環或者哈希連接連接到一個或者多個其他表。在連接的外側可以是任何一種被規劃器支持可以安全地在並行工作者中運行的非並行計劃。例如,它可以是一個索引掃描,基於從內表取得的一列來查找值。每個工作者都將會完整地執行外側的計劃,這也是為什麼這裏不能支持歸並連接的原因。歸並連接的外側常常涉及到排序整個內表,即便使用索引,多次在內表上進行完全索引掃描也效率不高。
3.3. 並行聚集
將查詢的聚集部分整個地並行執行是不可能的。例如,如果一個查詢涉及到選擇COUNT(*),每個工作者可以計算一個總和,但是這些總和需要被整合在一起以產生最終的答案。如果一個計劃涉及到GROUP BY子句,為每個組需要計算出一個單獨的總和。即使聚集不能完全地並行執行,但是涉及聚集的查詢常常是並行查詢很好的候選,因為它們通常都是讀很多行但隻返回少數幾行給客戶端。返回很多行給客戶端的查詢常常受製於客戶端讀取數據的速度,這種情況下並行查詢幫助不大。
PostgreSQL通過做兩次聚集來支持並行聚集。第一次,每個參與查詢計劃並行部分執行的進程執行一個聚集步驟,為進程發現的每個分組產生一個部分結果。這在計劃中反映為一個PartialAggregate節點。第二次,部分結果被通過Gather節點傳輸給領導者。最後,領導者對所有工作者的部分結果進行重聚集以得到最終的結果。這在計劃中反映為一個FinalizeAggregate節點。
並行聚集並不能支持所有的情況。每個聚集對於並行機製一定要是安全的,並且必須有一個結合函數。如果聚集有一個internal類型的轉移狀態,它必須有序列化和反序列化函數。對於有序集聚集或者查詢涉及GROUPING SETS時不支持並行聚集。隻有當查詢中涉及的所有連接也是計劃中並行不分的一部分時,才能使用並行聚集。
3.4. 並行計劃小貼士
如果我們想要一個查詢能產生並行計劃但事實上又沒有產生,可以嚐試減小parallel_setup_cost或者parallel_tuple_cost。當然,這個計劃可能比規劃器優先產生的順序計劃還要慢,但也不總是如此。如果將這些設置為很小的值(例如把它們設置為零)也不能得到並行計劃,那就可能是有某種原因導致查詢規劃器無法為你的查詢產生並行計劃。
在執行一個並行計劃時,可以用EXPLAIN (ANALYZE,VERBOSE)來顯示每個計劃節點在每個工作者上的統計信息。這些信息有助於確定是否所有的工作被均勻地分發到所有計劃節點以及從總體上理解計劃的性能特點。
規劃器把查詢中涉及的操作分類成並行安全、並行受限或者並行不安全。並行安全的操作不會與並行查詢的使用產生衝突。並行受限的操作不能在並行工作者中執行,但是能夠在並行查詢的領導者中執行。因此,並行受限的操作不能出現在Gather節點之下,但是能夠出現在包含有Gather 節點的計劃的其他位置。並行不安全的操作不能在並行查詢中執行,甚至不能在領導者中執行。當一個查詢包含任何並行不安全操作時,並行查詢對這個查詢是完全被禁用的。
下麵的操作總是並行受限的。
- 公共表表達式(CTE)的掃描。
- 臨時表的掃描。
- 外部表的掃描,除非外部數據包裝器有一個IsForeignScanParallelSafe API。
- 對InitPlan或者SubPlan的訪問。
4.1. 為函數和聚集加並行標簽
規劃器無法自動判定一個用戶定義的函數或者聚集是並行安全、並行受限還是並行不安全,因為這需要預測函數可能執行的每一個操作。一般而言,這就相當於一個停機問題,因此是不可能的。甚至對於可以做到判定的簡單函數我們也不會嚐試,因為那會非常昂貴而且容易出錯。相反,除非是被標記出來,所有用戶定義的函數都被認為是並行不安全的。在使用CREATE FUNCTION或者ALTER FUNCTION時,可以通過指定PARALLEL SAFE、PARALLEL RESTRICTED或者PARALLEL UNSAFE來設置標記 。在使用CREATE AGGREGATE時,PARALLEL選項可以被指定為SAFE、RESTRICTED或者 UNSAFE。
如果函數和聚集會寫數據庫、訪問序列、改變事務狀態(即便是臨時改變,例如建立一個EXCEPTION塊來捕捉錯誤的 PL/pgsql)或者對設置做持久化的更改,它們一定要被標記為PARALLEL UNSAFE。類似地,如果函數會訪問臨時表、客戶端連接狀態、遊標、預備語句或者係統無法在工作者之間同步的後端本地狀態,它們必須被標記為PARALLEL RESTRICTED。例如,setseed和 random由於後一種原因而是並行受限的。
一般而言,如果一個函數是受限或者不安全的卻被標記為安全,或者它實際是不安全的卻被標記為受限,把它用在並行查詢中時可能會拋出錯誤或者產生錯誤的回答。如果 C 語言函數被錯誤標記,理論上它會展現出完全不明確的行為,因為係統中無法保護自身不受任意 C 代碼的影響。但是,在最有可能的情況下,結果不會比其他任何函數更糟糕。如果有疑慮,最好還是標記函數為UNSAFE。
如果在並行工作者中執行的函數要求領導者沒有持有的鎖,例如讀該查詢中沒有引用的表,那麼工作者退出時會釋放那些鎖(而不是在事務結束時釋放)。如果你寫了一個這樣做的函數並且這種不同的行為對你很重要,把這類函數標記為PARALLEL RESTRICTED以確保它們隻在領導者中執行。
注意查詢規劃器不會為了獲取一個更好的計劃而考慮延遲計算並行受限的函數或者聚集。所以,如果一個被應用到特定表的WHERE子句是並行受限的,查詢規劃器就不會考慮把對那張表的掃描放置在Gather節點之下。在一些情況中,可以(甚至效率更高)把對表的掃描包括在查詢的並行部分並且延遲對WHERE子句的計算,這樣它會出現在Gather節點之上。不過,規劃器不會這樣做。
最後更新:2017-08-18 16:32:24