正如我們在上一節中看到的那樣,查詢規劃程序需要估計查詢檢索的資料列數量,以便對查詢計劃做出正確的選擇。本節簡要介紹系統用於這些估算的統計訊息。
統計訊息的其中一部分是每個資料表和索引中的項目總數,以及每個資料表和索引佔用的磁磁區塊數。此訊息保存在資料表 pg_class 中,列在 reltuples 和 relpages 欄位中。我們可以使用與此類似的查詢來查看它:
在這裡我們可以看到 tenk1 包含 10000 個資料列,其索引也是如此,但索引(不出所料)比資料表小得多。
由於效率因素,reltuples 和 relpup 並不會即時更新,因此它們通常包含一些過時的值。它們由 VACUUM,ANALYZE 和一些 DDL 指令(如 CREATE INDEX)更新。不掃描整個資料表的 VACUUM 或 ANALYZE 操作(通常是這種情況)將根據其掃描資料表的部分逐步更新 reltuples 計數,從而得到近似值。在任何情況下,規劃程序將縮放它在 pg_class 中找到的值以匹配目前實際上資料表大小,從而獲得更接近的近似值。
由於 WHERE 子句會限制要檢查的資料列,因此大多數查詢僅檢索資料表中的一小部分行。因此,規劃程序需要估計 WHERE 子句的篩選性,即與 WHERE 子句中的每個條件匹配的資料列比率。用於此任務的訊息儲存在 pg_statistic 系統目錄中。pg_statistic 中的項目由 ANALYZE 和 VACUUM ANALYZE 指令更新,即使在剛更新時也都是近似值。
除了直接查看 pg_statistic,最好在手動檢查統計訊息時查看其檢視表 pg_stats。pg_stats 旨在於更容易閱讀。此外,所有人都可以讀取 pg_stats,而 pg_statistic 只能由超級使用者讀取。(這可以防止非特權使用者從統計訊息中學習有關其他人表的內容。pg_stats 檢視表僅限於顯示目前使用者可以讀取的資料表資訊。)例如,我們可能會這樣做:
請注意,同一欄位顯示兩行,一行對應於從路徑表開始的完整繼承層次結構(inherited = t),另一行僅包含路徑表本身(inherited = f)。
ANALYZE 儲存在 pg_statistic 中的資訊量,特別是每欄位的 most_common_vals 和 histogram_bounds 陣列中的最大項目數,可以使用 ALTER TABLE SET STATISTICS 指令逐個欄位設定,也可以透過設定全域的 default_statistics_target 組態變數。預設限制目前是 100 個項目。提高限制可能允許進行更準確的計劃程序估算,特別是對於具有不規則資料分佈的欄位,其代價是在 pg_statistic 中消耗更多空間並且計算估計的時間稍長。相反地,對於具有簡單資料分佈的欄位,下限可能就足夠了。
有關規劃程序使用統計資料的更多詳細訊息,請參閱第 68 章。
通常會看到執行錯誤執行計劃的緩慢查詢,因為查詢子句中使用的多個欄位是相關的。規劃程序通常假設多個條件彼此獨立,這一假設在欄位值相關時並不成立。由於每個欄位的性質,一般的統計數據無法捕獲有關跨欄位關聯的任何知識。但是,PostgreSQL 能夠計算此類信息的多變量統計訊息。
由於可能的欄位組合數量非常大,因此自動計算多變量統計數據是不切實際的。相反,可以建立延伸統計物件(通常稱為統計物件),以指示伺服器獲取有趣的欄位集合之間的統計訊息。
使用 CREATE STATISTICS 建立統計物件,可以查看更多詳細訊息。建立這樣的物件僅建立表示對統計訊息感興趣的目錄項目。實際數據收集由 ANALYZE(手動命令或背景自動分析)執行。可以在 pg_statistic_ext 目錄中檢查收集的數據。
ANALYZE 根據計算一般單欄位統計訊息所需的資料表中資料列樣本計算延伸統計訊息。由於透過增加資料表或其任何欄位的統計目標來增加樣本大小(如上一節中所述),因此較大的統計目標通常會産生更準確的延伸統計訊息,但也會讓計算它們時間花費更多。
以下小節介紹了目前支援延伸統計訊息的種類。
最簡單的延伸統計訊息是追踪功能相依性,這是資料庫一般資料表定義中所使用的概念。我們說如果 a 的值足以決定 b 的值,那麼欄位 b 在功能上相依於欄位 a,即沒有兩個資料列具有相同的 a 值而是具有不同的 b 值。在完全正規化的資料庫中,功能相依性應僅存在於主鍵和超級鍵(superkey)上。 然而,在實務中,由於各種原因,許多資料集未完全正規化;出於效能原因的故意非正規化是一種常見的例子。即使在完全正規化的資料庫中,某些欄位之間也可能存在部分關連性,這可以表示為部分功能相依性。
功能相依性的存在直接影響某些查詢中估計的準確性。如果查詢包含獨立欄位和從屬欄位的條件,則相依欄位上的條件不會進一步減小結果大小;但是,如果不了解功能相依性,查詢計劃程序將假定條件是獨立的,從而導致低估結果大小。
為了向計劃程序告知功能相依性,ANALYZE 可以收集跨欄位相依性的度量。評估所有欄位集合之間的相依程度非常昂貴,因此數據收集僅限於在使用 dependencies 選項定義的統計物件中一起出現的那些欄位組合。建議僅為強關聯的欄位組合建立相依關係統計訊息,以避免在 ANALYZE 和以後的查詢規劃中產生不必要的開銷。
以下是收集功能相依性統計訊息的範例:
在這裡可以看出,第 1 欄位(zip code)完全決定第 5 欄位(city),因此係數為 1.0,而 city 僅在 42% 的時間內決定 zip code,這意味著有許多城市(58%)是由多個郵政編碼所代表。
在計算涉及功能相關欄位的查詢的選擇性時,計劃程序使用相依性係數調整每個條件的選擇性估計,以便不低估它們。
功能相依性目前僅在考慮將欄位與常數值進行比較的簡單相等條件時套用。它們不會用於改善比較兩欄位或將欄位與表示式進行比較等式條件的估計,也不用於範圍子句,LIKE 或任何其他類型的條件。
在使用函數相依性進行估計時,計劃程序假定所涉及欄位的條件是相容的,因此是多餘的。如果它們不相容,則正確的估計值將為零個資料列,但不考慮這種可能性。例如,給出一個類似的查詢
規劃程序將忽視 city 子句,因為不改變選擇性,這是正確的。但是,它會做出相同的假設
即使確實有零個資料列滿足此查詢。但是,功能相依性統計訊息不能提供足夠的訊息來得到結論。
在許多實際情況中,通常會滿足這種假設; 例如,應用程序中可能存在一個 GUI,它只允許選擇要在查詢中使用的相容的城市和郵政編碼值。但如果情況並非如此,那麼功能相依性可能不是一個可行的選擇。
單欄位統計訊息儲存每欄位中不同值的數量。當計劃程序僅具有單欄位統計數據時,估計組合多個欄位時的不同值的數量(例如,對於 GROUP BY a, b)通常是錯誤的,從而導致它選擇錯誤的計劃。
為了改進這樣的估計,ANALYZE 可以為欄位組合收集 n 個不同的統計數據。和以前一樣,為每個可能的欄位分組執行此操作是不切實際的,因此僅為在使用 ndistinct 選項定義的統計物件中出現的那些欄位組合收集數據。將從列出的欄位集合中的兩個或更多欄位的每個可能組合收集數據。
繼續前面的範例,郵政編碼表中的 n 個不同計數可能如下所示:
這表明有三種具有 33,178 個不同值的欄位組合:ZIP code 和 state;ZIP code 和 city;和 ZIP code,city 和 state(由於此表中的郵政編碼是唯一的,因此預計它們都是相同的)。另一方面,city 和 state 的組合只有 27,435 個不同的值。
建議僅在實際用於 GROUP 的欄位組合上建立 ndistinct 統計物件,對於那些因為群組數量錯誤估計導致錯誤計劃的組合。否則,ANALYZE 工作只是一種浪費。
Durability (耐用性)是一種資料庫特性,即使伺服器當機或斷電,也可以保證保存好已提交的交易事務。但是,耐用性會增加大量的資料庫成本。因此,如果您的服務不需要這樣的保證,則可以將 PostgreSQL 設定可以執行得更快。以下是在這種情況下可以進行以提高效能的配置調整。除非另有說明,否則在資料庫軟體當機的情況下仍然可以保證持久性;使用這些設定時,只有突然的作業系統停止才會造成資料遺失或損壞的風險。
將資料庫叢集的資料目錄放置在記憶體中的檔案系統(即 RAM 磁碟)中。這樣可以完全消除所有資料庫磁碟的 I/O,但將資料儲存限制為可用記憶體大小(可能會包含 SWAP 空間)。
關閉 fsync;毌須主動將資料更新到磁碟。
關閉 sync_commit;可以不用在每次交易提交時都強制將 WAL 寫到磁碟。如果資料庫服務當掉,此設定會產生交易結果消失(但不會破壞資料)的風險。
關閉 full_page_writes; 沒有必要防止部分頁面寫入。
增加 max_wal_size 和 checkpoint_timeout; 這減少了檢查點的頻率,但是增加了 /pg_wal 的儲存需求。
建立 unlogged table 以避免 WAL 寫入,儘管這會使資料表是 non-crash-safe。
查詢效能可能會受到很多因素的影響。其中一些可以由使用者控制,而另一些則是系統底層設計的基礎。本章提供了關於理解和調整 PostgreSQL 效能的一些提示。
PostgreSQL 會為它收到的每個查詢設計一個查詢計劃。選擇正確的計劃以搭配查詢結構和資料屬性對於提高效能至關重要,所以系統包含一個複雜的計劃程序,試圖選擇好的計劃。您可以使用 EXPLAIN 指令查看計劃程序為每一個查詢所建立的查詢計劃。計劃內容閱讀是一門需要掌握一定經驗的藝術,但本部分主要涵蓋基本知識。
本節中的範例是在使用 9.3 開發原始碼進行 VACUUM ANALYZE 後進行迴歸測試資料庫中提取的。如果您自己嘗試這些範例,應該能夠獲得類似的結果,但您的估計成本和行數可能略有不同,因為 ANALYZE 的統計資訊是隨機樣本而不是精確的,而且因為成本本質上與平台有關。
這些範例使用 EXPLAIN 的預設「文字」輸出格式,該格式緊湊且便於人類閱讀。 如果要將 EXPLAIN 的輸出提供給程式以進行進一步分析,則應使用其機器可讀輸出格式之一(XML、JSON 或 YAML)。
EXPLAIN
基本概念查詢計劃的結構是計劃節點樹。樹底層的節點是掃描節點:它們從資料表中回傳原始資料列。對於不同的資料表存取方法,存在不同類型的掃描節點:循序掃描、索引掃描和 bitmap 索引掃描。還有非資料表的來源,例如 VALUES 中的 VALUES 子句和 set-returns 函數,它們有自己的掃描節點類型。如果查詢需要對原始資料列進行交叉查詢、彙總、排序或其他操作,則掃描節點上方將有其他節點來執行這些操作。同樣,通常有多種可能的方法來執行這些操作,因此這裡也可以顯示不同的節點類型。EXPLAIN 的輸出對於計劃樹中的每個節點都有一行,顯示基本節點類型以及計劃程序為執行該計劃節點所做的成本估算。可能會顯示從節點的摘要行縮進的其他行,以顯示節點的其他屬性。第一行(最頂層節點的摘要行)具有計劃的估計總執行成本;計劃程序會試圖最小化這個數字。
這裡有一個簡單的例子,只是為了顯示輸出的樣子:
由於此查詢沒有 WHERE 子句,因此它必須掃描資料表的所有資料列,因此規劃程序選擇使用簡單的循序掃描計劃。括號中引用的數字是(從左到右):
估計的啟動成本。這是在輸出階段開始之前花費的時間,例如,在排序節點中進行排序的時間。
估計總成本。這是在假設計劃節點執行完成,即檢索所有可用資料列的情況下評估的。實際上,節點的父節點可能會停止讀取所有可用的資料列(請參閱下面的 LIMIT 範例)。
此計劃節點輸出的估計資料列數量。同樣地,假定節點完全執行。
此計劃節點輸出的資料列估計的平均資料大小(以 byte 為單位)。
成本按照規劃程序的成本參數決定的各個單位計量(見第 19.7.2 節)。傳統做法是以磁碟頁面讀取為單位來衡量成本;也就是說,seq_page_cost 通常設定為 1.0,其他成本參數相對於此來設定。本節中的範例使用預設的成本參數進行。
很重要的是要了解上層節點的成本包括其所有子節點的成本。同樣重要的是要意識到成本只反映了計劃程序所關心的事情。特別是,成本不考慮將結果資料列傳輸到用戶端所花費的時間,這可能是實際經過時間的一個重要因素;但是計劃者忽略了它,因為它不能透過改變計劃來改善它。(我們相信,每個正確的計劃都會輸出相同的資料列集合。)
資料列的數目有點棘手,因為它不是計劃節點處理或掃描的數量,而是節點發出的資料列數量。這通常小於掃描的數量,這是透過在節點上套用的任何 WHHERE 子句條件進行過濾的結果。理想情況下,最上層級資料列數量估計值將近似於查詢實際回傳、更新或刪除的資料列數目。
回到我們的例子:
這些數字非常直觀。如果你這樣做:
你會發現 tenk1 有 358 個磁碟頁面和 10000 個資料列。估計的成本計算為(磁碟頁讀取 _ * _seq_page_cost)+(資料列掃描 * cpu_tuple_cost)。預設的情況下,seq_page_cost 為 1.0,cpu_tuple_cost 為 0.01,因此估計成本為(358 * _ _1.0)+(10000 * 0.01)= 458。
現在讓我們修改查詢加入 WHERE 條件:
請注意,EXPLAIN 輸出顯示 WHERE 子句作為附加到 Seq Scan 計劃節點的「filter」條件應用。這意味著計劃節點檢查它掃描的每一筆資料的條件,並僅輸出通過該條件的那些資料列。由於 WHERE 子句,輸出資料列的估計已經減少。只是,掃描仍然需要讀取所有 10000 筆資料,因此成本並沒有降低;實際上它已經上升了一點(確切地說是 10000 * cpu_operator_cost)以反映檢查 WHERE 條件所花費的額外 CPU 時間。
此查詢將回傳的實際筆數為 7000,但筆數估計值僅為近似值。如果您嘗試複製此實驗,您可能會得到略微不同的估計;此外,它可能在每個 ANALYZE 指令之後改變,因為 ANALYZE 産成的統計訊息來自於資料表的隨機樣本。
現在,我們加上更多條件限制:
規劃程序決定使用兩個步驟的計劃:子計劃節點讀取索引以查詢與索引條件匹配的資料列位置,然後上層計劃節點實際從資料表本身中提取這些資料列。單獨獲取資料列比按順序讀取它們要昂貴得多,但由於不是必須讀取該資料表的所有頁面,因此這仍然比循序掃描便宜。(使用兩個計劃層級的原因是上層計劃節點在讀取之前將索引標示的資料列位置排序為物理順序,以最小化單獨提取的成本。節點名稱中提到的「bitmap」是排序機制。)
現在讓我們為 WHERE 子句增加另一個條件:
增加的條件 stringu1 ='xxx' 減少了輸出資料列數的估計,但不是成本,因為我們仍然必須讀取同一組資料列。請注意,stringu1 子句無法作為索引條件套用,因為此索引僅在 unique1 欄位上。而是將其作為過濾器套用於索引檢索的資料列。因此,成本實際上略有上升,以反映這種額外的檢查。
在某些情況下,規劃程序更喜歡「簡單」的索引掃描計劃:
在這種類型的計劃中,資料列按索引順序讀取,這使得它們讀取起來更加昂貴,但是很少有人覺得資料列位置進行排序的額外成本是不值得的。對於只獲取一個資料列的查詢,您通常會看到此計劃類型。它也經常用於具有與索引順序匹配的 ORDER BY 條件的查詢,因為這樣就不需要額外的排序步驟來滿足 ORDER BY。
如果在 WHERE 中引用的幾個欄位上有單獨的索引,則查詢規劃器可能會選擇使用索引的 AND 及 OR 組合:
但這需要存取兩個索引,因此與僅使用一個索引並將另一個條件視為過濾器相比,它不一定更好。如果您改變所涉及的範圍,您將看到相應的計劃變更。
以下是顯示 LIMIT 效果的範例:
這是與上面相同的查詢,但是我們增加了一個 LIMIT,以便不需要檢索所有資料列,並且計劃程序改變了想要做什麼的想法。請注意,「索引掃描」節點的總成本和資料列數量顯示為執行完成。但是,限制節點預計僅檢索這些資料列中的五分之一後停止,因此其總成本僅為五分之一,這是查詢的實際估計成本。此計劃優於將 Limit 節點加到上一個計劃,因為 Limit 無法避免支付 bitmap 掃描的啟動成本,因此使用該方法的總成本將超過 25 個單位。
讓我們嘗試使用我們一直在討論的欄位來交叉查詢兩個資料表:
在此計劃中,我們有一個巢狀循環的交叉查詢節點,其中有兩個資料表掃描作為輸入或子節點。節點摘要行的縮進反映了計劃樹狀結構。交叉查詢的第一個或「外部」子節點是一個類似於我們之前看到的 bitmap 掃描。它的成本和行數與我們從SELECT ... WHERE unique1 <10 得到的相同,因為我們在該節點上使用了WHERE 子句 unique1 <10。 t1.unique2 = t2.unique2 子句尚未相關,因此它不會影響外部掃描的行數。巢狀循環交叉查詢節點將為從外部子節點獲取的每一行運行其第二個或“內部”子節點一次。來自當下外部交叉查詢資料列的欄位值可以插入內部掃瞄;在這裡,來自外部交叉查詢資料列的 t1.unique2 值是可用的,因此我們得到一個類似於我們在上面看到的簡單「SELECT ... WHERE t2.unique2 = 常數」的情況。 (估計的成本實際上比上面看到的要低一些,因為在 t2 上重複索引掃描期間預計會發生快取。)然後根據成本確定循環節點的成本。外部交叉查詢掃描,每個外部交叉查詢資料列重複一次內部交叉查詢掃描(此處為10 * 7.91),加上一點 CPU 時間進行交叉查詢處理。
在此範例中,交叉查詢的輸出資料列計數與兩個掃描的資料列計數的乘積相同,但在所有情況下都不是這樣,因為可以有其他 WHERE 子句提及兩個資料表,因此只會用於交叉查詢的節點,不論其他輸入任何掃描。這是一個例子:
條件 t1.hundred < t2.hundred 無法在 tenk2_unique2 索引中進行測試,因此它套用於 join 節點。這會減少連接節點的估計輸出資料列數,但不會更改任何輸入掃描。
請注意,此處規劃程序已選擇透過在其上放置 Materialize 計劃節點來「具體化」交叉查詢的內部關係。這意味著 t2 索引掃描將只執行一次,即使 nested-loop join 節點需要讀取該資料十次,對於來自外部關係的每一筆資料一次。Materialize 節點在讀取資料時將資料保存在記憶體中,然後在每次後續傳遞時從記憶體中回傳資料。
處理外部交叉查詢時,您可能會看到連接計劃節點同時附加了「Join Filter」和簡單的「Filter」情境。 Join Filter 情境來自外部交叉查詢的 ON 子句,因此交叉查詢過濾條件失敗的資料列仍然可以作為 null-extended 資料列發出。但是在外部交叉查詢之後套用了一個普通的 Filter 條件,因此可以無條件地刪除資料列。在內部交叉查詢中,這些類型的過濾程序之間沒有實質差異。
如果我們稍微改變查詢的過濾條件,我們可能會得到一個非常不同的交叉查詢計劃:
這裡,規劃程序選擇使用 hash 交叉查詢,其中一個資料表的資料列被輸入到記憶體中的 hash 資料表中,之後掃描另一個資料表並且檢查 hash 資料表以匹配每一筆資料。再次注意,縮排如何反映計劃結構:tenk1 上的 bitmap 掃描是 hash 節點的輸入,它建構 hash 資料表。然後返回到 Hash Join 節點,該節點從其外部子計劃中讀取資料列並在 hash 資料表中搜索每一筆資料。
另一種可能的交叉查詢類型是 merge join,如下所示:
合併交叉查詢要求其輸入資料在交叉查詢主鍵上排序。在此計劃中,tenk1 資料使用索引掃描進行排序,以正確的順序存取資料列,但是對於 onek,偏好順序掃描和排序,因為在該資料表中要存取的筆數要多得多。(循序掃描和排序經常擊敗索引掃描以排序多筆資料,因為索引掃描需要非循序磁碟存取。)
查看變形計劃的一種方法是使用第 19.7.1 節中描述的啟用/禁用旗標強制規劃程予忽略它認為最便宜的策略。(這是一個粗略的工具,但很有用。另請參閱第 14.3 節。)例如,如果我們不相信循序掃描和排序是在前一個範例中處理資料表 onek 的最佳方法,我們可以嘗試
這表明計劃程予認為透過索引掃描排序 onek 比循序掃描和排序貴 12%。當然,接下來的問題是它是否正確。 我們可以使用 EXPPLIN ANALYZE 來調查,如下所述。
EXPLAIN ANALYZE
可以使用 EXPLAIN 的 ANALYZE 選項檢查計劃員估算的準確性。 使用此選項,EXPLAIN 實際執行查詢,然後顯示每個計劃節點中累積的真實資料列計數和真實執行時間,以及簡單 EXPLAIN 顯示的相同估計值。例如,我們可能得到這樣的結果:
注意,「實際時間」值是實際執行的時間,以毫秒為單位,而成本估算會以任意單位表示;所以他們不太可能匹配。通常最重要的事情是估計的資料列數量是否與現實相當接近。在這個例子中,估計都是死的,但這在實務上很不尋常。
在某些查詢計劃中,子計劃節點可能不止一次執行。例如,內部索引掃描將在上述巢狀循環計劃中的每個外部資料列執行一次。在這種情況下,循環的值回報節點的總執行次數,顯示的實際時間和資料列的值是每次執行的平均值。這樣做是為了使數字與顯示成本估算的方式相當。乘以 loopsvalue 得到實際花費在節點上的總時間。在上面的例子中,我們總共花了 0.220 毫秒來執行 tenk2 上的索引掃描。
在某些情況下,EXPLAIN ANALYZE 會在計劃節點執行時間和資料列計數之外顯示其他執行統計訊息。例如,Sort 和 Hash 節點提供了額外的訊息:
Sort 節點顯示使用的排序方法(特別是,排序是在記憶體中還是在磁碟上)以及所需的記憶體量或磁盤空間量。Hash 節點顯示 hash buckets 和批次的數量以及用於雜湊表的尖峰記憶體用量。(如果批次次數超過 1,則還會涉及磁碟空間使用,但不會顯示出來。)
另一種類型的額外訊息是過濾條件移除的資料列數目:
這些計數對於在連接節點處應用的過濾條件特別有用。僅當過濾條件拒絕了至少一個掃描資料列或加入節點情況下的潛在交叉查詢配對時,才會顯示「Rows Removed」。
類似於過濾條件的情況發生在「lossy」索引掃描中。例如,考慮搜尋包含特定點的多邊形:
規劃程序(非常正確地)認為這個樣本資料表太小而無法進行索引掃描,因此我們進行了簡單的循序掃描,其中所有資料列都被過濾條件拒絕。但是如果我們強制使用索引掃描,我們會看到:
在這裡我們可以看到索引回傳了一個候選資料列,然後透過重新檢查索引條件來拒絕該資料列。發生這種情況是因為 GiST 索引對於多邊形包含測試來說是「lossy」的:它實際上回傳的多個資料列與目標重疊的多邊形,然後我們必須對這些資料列進行精確的測試。
EXPLAIN 有一個 BUFFERS 選項,可以與 ANALYZE 一起使用以獲得更多的執行時統計訊息:
BUFFERS 提供的數字有助於識別查詢的哪些部分是 I/O 密集程度最高的。
請記住,因為 EXPLAIN ANALYZE 實際上執行查詢,所以任何副作用都會照常發生,即使查詢可能輸出的任何結果都被丟棄,有利於輸出 EXPLAIN 數據。如果要在不更改資料表的情況下分析資料修改查詢,可以在之後回溯事務,例如:
如此範例所示,當查詢是 INSERT,UPDATE 或 DELETE 指令時,套用資料表變更的實際工作由最上層的 INSERT,UPDATE 或 DELETE 計劃節點完成。此節點下的計劃節點執行定位舊的資料列和計算新資料的工作。所以上面,我們看到了我們已經看到的相同類型的 bitmap 資料表掃描,並且它的輸出被遞送到儲存更新資料列的 Update 節點。值得注意的是,儘管資料修改節點可能需要相當長的執行時間(此時,它佔用了大部分時間),但規劃程序目前並未在成本估算中加入任何內容來解釋該工作。這是因為要完成的工作對於每個正確的查詢計劃都是相同的,因此它不會影響計劃決策。
當 UPDATE 或 DELETE 指令影響繼承結構時,輸出可能如下所示:
在此範例中,Update 節點需要考慮三個子資料表以及最初提到的父資料表。因此,有四個輸入掃描子計劃,每個資料表一個。為清楚起見,更新節點的註釋是為了顯示將要更新的特定目標資料表,其順序與相應的子計劃相同。(這些註釋是 PostgreSQL 9.5 的新註釋;在以前的版本中,讀取者必須透過檢查子計劃來看到目標資料表。)
EXPLAIN ANALYZE 顯示的 Planning 時間是從解析的查詢産生查詢計劃並對其進行最佳化所花費的時間。它不包括解析或重寫。
EXPLAIN ANALYZE 顯示的執行時間包括執行程序啟動和關閉時間,以及執行觸發的任何觸發器的時間,但不包括解析,重寫或計劃時間。執行 BEFORE 觸發器所花費的時間(如果有)包含在相關的 Insert,Update 或 Delete 節點的時間中;但是執行 AFTER 觸發器所花費的時間不計算在那裡,因為 AFTER 觸發器在完成整個計劃後被觸發。每個觸發器(BEFORE 或 AFTER)花費的總時間也會單獨顯示。請注意,延遲限制條件觸發器在事務結束之前不會執行,因此 EXPLAIN ANALYZE 根本不會考慮延遲限制條件觸發器。
EXPLAIN ANALYZE 測量的執行時間有兩種主要情況可能偏離同一查詢的實際執行。首先,由於沒有輸出資料列傳送到客戶端,因此不包括網路傳輸成本和 I/O 轉換成本。其次,EXPLAIN ANALYZE 增加的測量開銷可能很大,特別是在具有較慢 gettimeofday() 作業系統的機器上。您可以使用 pg_test_timing 工具來衡量系統計時的成本。
EXPLAIN 結果不應該推斷到與您實際測試的情況大不相同的情況;例如,不能假設小型資料表上的結果適用於大型資料表。規劃程序的成本估算不是線性的,因此可能會為更大或更小的資料表選擇不同的計劃。一個極端的例子是,在只佔用一個磁碟頁面的資料表上,無論索引是否可用,您幾乎總能獲得循順掃描計劃。規劃程序在任何情況下都會讀取一個磁碟頁面來處理資料表,因此在延伸額外的頁面讀取以查看索引時就沒有任何價值。(我們在上面的 polygon_tbl 範例中看到了這種情況。)
在某些情況下,實際值和估計值不能很好地對應,但沒有什麼是真正的錯誤。當計劃節點執行由 LIMIT 或類似效果停止時,就會發生這種情況。例如,在我們之前使用的 LIMIT 查詢中,
索引掃描節點的估計成本和資料列數目顯示為執行完成。但實際上,Limit 節點在獲得兩個資料列後停止請求,因此實際資料列數目僅為 2,執行時間小於成本估算所顯示的。這不是估計誤差,只是估計值和真實值顯示方式有所差異。
交叉查詢也有測量工具,也可能産生混淆。如果一個輸入耗盡了另一個輸入,並且一個輸入中的下一個鍵值大於另一個輸入的最後一個鍵值,則交叉查詢將停止讀取一個輸入;在這種情況下,不可能再有匹配,因此不需要掃描第一個輸入的其餘部分。這導致不讀取所有下一個子項,結果如 LIMIT 所述。此外,如果外部(第一個)子項包含具有重複鍵值的資料列,則內部(第二個)子項將被備份並重新掃描其與該鍵值匹配資料列的部分。EXPLAIN ANALYZE 計算相同內部資料列的這些重複映射,就好像它們是真正的附加資料列一樣。當存在許多外部重複項時,內部子計劃節點的報告實際資料列數目可能明顯大於內部關連中實際存在的資料列數目。
由於實作限制,BitmapAnd 和 BitmapOr 節點始終將其實際資料列計數回報為零。
One might need to insert a large amount of data when first populating a database. This section contains some suggestions on how to make this process as efficient as possible.
When using multiple INSERT
s, turn off autocommit and just do one commit at the end. (In plain SQL, this means issuing BEGIN
at the start and COMMIT
at the end. Some client libraries might do this behind your back, in which case you need to make sure the library does it when you want it done.) If you allow each insertion to be committed separately, PostgreSQL is doing a lot of work for each row that is added. An additional benefit of doing all insertions in one transaction is that if the insertion of one row were to fail then the insertion of all rows inserted up to that point would be rolled back, so you won't be stuck with partially loaded data.
COPY
Use COPY to load all the rows in one command, instead of using a series of INSERT
commands. The COPY
command is optimized for loading large numbers of rows; it is less flexible than INSERT
, but incurs significantly less overhead for large data loads. Since COPY
is a single command, there is no need to disable autocommit if you use this method to populate a table.
If you cannot use COPY
, it might help to use PREPARE to create a prepared INSERT
statement, and then use EXECUTE
as many times as required. This avoids some of the overhead of repeatedly parsing and planning INSERT
. Different interfaces provide this facility in different ways; look for “prepared statements” in the interface documentation.
Note that loading a large number of rows using COPY
is almost always faster than using INSERT
, even if PREPARE
is used and multiple insertions are batched into a single transaction.
COPY
is fastest when used within the same transaction as an earlier CREATE TABLE
or TRUNCATE
command. In such cases no WAL needs to be written, because in case of an error, the files containing the newly loaded data will be removed anyway. However, this consideration only applies when wal_level is minimal
for non-partitioned tables as all commands must write WAL otherwise.
If you are loading a freshly created table, the fastest method is to create the table, bulk load the table's data using COPY
, then create any indexes needed for the table. Creating an index on pre-existing data is quicker than updating it incrementally as each row is loaded.
If you are adding large amounts of data to an existing table, it might be a win to drop the indexes, load the table, and then recreate the indexes. Of course, the database performance for other users might suffer during the time the indexes are missing. One should also think twice before dropping a unique index, since the error checking afforded by the unique constraint will be lost while the index is missing.
Just as with indexes, a foreign key constraint can be checked “in bulk” more efficiently than row-by-row. So it might be useful to drop foreign key constraints, load data, and re-create the constraints. Again, there is a trade-off between data load speed and loss of error checking while the constraint is missing.
What's more, when you load data into a table with existing foreign key constraints, each new row requires an entry in the server's list of pending trigger events (since it is the firing of a trigger that checks the row's foreign key constraint). Loading many millions of rows can cause the trigger event queue to overflow available memory, leading to intolerable swapping or even outright failure of the command. Therefore it may be necessary, not just desirable, to drop and re-apply foreign keys when loading large amounts of data. If temporarily removing the constraint isn't acceptable, the only other recourse may be to split up the load operation into smaller transactions.
maintenance_work_mem
Temporarily increasing the maintenance_work_mem configuration variable when loading large amounts of data can lead to improved performance. This will help to speed up CREATE INDEX
commands and ALTER TABLE ADD FOREIGN KEY
commands. It won't do much for COPY
itself, so this advice is only useful when you are using one or both of the above techniques.
max_wal_size
Temporarily increasing the max_wal_size configuration variable can also make large data loads faster. This is because loading a large amount of data into PostgreSQL will cause checkpoints to occur more often than the normal checkpoint frequency (specified by the checkpoint_timeout
configuration variable). Whenever a checkpoint occurs, all dirty pages must be flushed to disk. By increasing max_wal_size
temporarily during bulk data loads, the number of checkpoints that are required can be reduced.
When loading large amounts of data into an installation that uses WAL archiving or streaming replication, it might be faster to take a new base backup after the load has completed than to process a large amount of incremental WAL data. To prevent incremental WAL logging while loading, disable archiving and streaming replication, by setting wal_level to minimal
, archive_mode to off
, and max_wal_senders to zero. But note that changing these settings requires a server restart.
Aside from avoiding the time for the archiver or WAL sender to process the WAL data, doing this will actually make certain commands faster, because they are designed not to write WAL at all if wal_level
is minimal
. (They can guarantee crash safety more cheaply by doing an fsync
at the end than by writing WAL.) This applies to the following commands:
CREATE TABLE AS SELECT
CREATE INDEX
(and variants such as ALTER TABLE ADD PRIMARY KEY
)
ALTER TABLE SET TABLESPACE
CLUSTER
COPY FROM
, when the target table has been created or truncated earlier in the same transaction
ANALYZE
AfterwardsWhenever you have significantly altered the distribution of data within a table, running ANALYZE is strongly recommended. This includes bulk loading large amounts of data into the table. Running ANALYZE
(or VACUUM ANALYZE
) ensures that the planner has up-to-date statistics about the table. With no statistics or obsolete statistics, the planner might make poor decisions during query planning, leading to poor performance on any tables with inaccurate or nonexistent statistics. Note that if the autovacuum daemon is enabled, it might run ANALYZE
automatically; see Section 24.1.3 and Section 24.1.6 for more information.
Dump scripts generated by pg_dump automatically apply several, but not all, of the above guidelines. To reload a pg_dump dump as quickly as possible, you need to do a few extra things manually. (Note that these points apply while restoring a dump, not while creating it. The same points apply whether loading a text dump with psql or using pg_restore to load from a pg_dump archive file.)
By default, pg_dump uses COPY
, and when it is generating a complete schema-and-data dump, it is careful to load data before creating indexes and foreign keys. So in this case several guidelines are handled automatically. What is left for you to do is to:
Set appropriate (i.e., larger than normal) values for maintenance_work_mem
and max_wal_size
.
If using WAL archiving or streaming replication, consider disabling them during the restore. To do that, set archive_mode
to off
, wal_level
to minimal
, and max_wal_senders
to zero before loading the dump. Afterwards, set them back to the right values and take a fresh base backup.
Experiment with the parallel dump and restore modes of both pg_dump and pg_restore and find the optimal number of concurrent jobs to use. Dumping and restoring in parallel by means of the -j
option should give you a significantly higher performance over the serial mode.
Consider whether the whole dump should be restored as a single transaction. To do that, pass the -1
or --single-transaction
command-line option to psql or pg_restore. When using this mode, even the smallest of errors will rollback the entire restore, possibly discarding many hours of processing. Depending on how interrelated the data is, that might seem preferable to manual cleanup, or not. COPY
commands will run fastest if you use a single transaction and have WAL archiving turned off.
If multiple CPUs are available in the database server, consider using pg_restore's --jobs
option. This allows concurrent data loading and index creation.
Run ANALYZE
afterwards.
A data-only dump will still use COPY
, but it does not drop or recreate indexes, and it does not normally touch foreign keys. [13] So when loading a data-only dump, it is up to you to drop and recreate indexes and foreign keys if you wish to use those techniques. It's still useful to increase max_wal_size
while loading the data, but don't bother increasing maintenance_work_mem
; rather, you'd do that while manually recreating indexes and foreign keys afterwards. And don't forget to ANALYZE
when you're done; see Section 24.1.3 and Section 24.1.6 for more information.\
[13] You can get the effect of disabling foreign keys by using the --disable-triggers
option — but realize that that eliminates, rather than just postpones, foreign key validation, and so it is possible to insert bad data if you use it.
It is possible to control the query planner to some extent by using the explicit JOIN
syntax. To see why this matters, we first need some background.
In a simple join query, such as:
the planner is free to join the given tables in any order. For example, it could generate a query plan that joins A to B, using the WHERE
condition a.id = b.id
, and then joins C to this joined table, using the other WHERE
condition. Or it could join B to C and then join A to that result. Or it could join A to C and then join them with B — but that would be inefficient, since the full Cartesian product of A and C would have to be formed, there being no applicable condition in the WHERE
clause to allow optimization of the join. (All joins in the PostgreSQLexecutor happen between two input tables, so it's necessary to build up the result in one or another of these fashions.) The important point is that these different join possibilities give semantically equivalent results but might have hugely different execution costs. Therefore, the planner will explore all of them to try to find the most efficient query plan.
When a query only involves two or three tables, there aren't many join orders to worry about. But the number of possible join orders grows exponentially as the number of tables expands. Beyond ten or so input tables it's no longer practical to do an exhaustive search of all the possibilities, and even for six or seven tables planning might take an annoyingly long time. When there are too many input tables, the PostgreSQL planner will switch from exhaustive search to a genetic probabilistic search through a limited number of possibilities. (The switch-over threshold is set by the geqo_threshold run-time parameter.) The genetic search takes less time, but it won't necessarily find the best possible plan.
When the query involves outer joins, the planner has less freedom than it does for plain (inner) joins. For example, consider:
Although this query's restrictions are superficially similar to the previous example, the semantics are different because a row must be emitted for each row of A that has no matching row in the join of B and C. Therefore the planner has no choice of join order here: it must join B to C and then join A to that result. Accordingly, this query takes less time to plan than the previous query. In other cases, the planner might be able to determine that more than one join order is safe. For example, given:
it is valid to join A to either B or C first. Currently, only FULL JOIN
completely constrains the join order. Most practical cases involving LEFT JOIN
or RIGHT JOIN
can be rearranged to some extent.
Explicit inner join syntax (INNER JOIN
, CROSS JOIN
, or unadorned JOIN
) is semantically the same as listing the input relations in FROM
, so it does not constrain the join order.
Even though most kinds of JOIN
don't completely constrain the join order, it is possible to instruct the PostgreSQL query planner to treat all JOIN
clauses as constraining the join order anyway. For example, these three queries are logically equivalent:
But if we tell the planner to honor the JOIN
order, the second and third take less time to plan than the first. This effect is not worth worrying about for only three tables, but it can be a lifesaver with many tables.
To force the planner to follow the join order laid out by explicit JOIN
s, set the join_collapse_limit run-time parameter to 1. (Other possible values are discussed below.)
You do not need to constrain the join order completely in order to cut search time, because it's OK to use JOIN
operators within items of a plain FROM
list. For example, consider:
With join_collapse_limit
= 1, this forces the planner to join A to B before joining them to other tables, but doesn't constrain its choices otherwise. In this example, the number of possible join orders is reduced by a factor of 5.
Constraining the planner's search in this way is a useful technique both for reducing planning time and for directing the planner to a good query plan. If the planner chooses a bad join order by default, you can force it to choose a better order via JOIN
syntax — assuming that you know of a better order, that is. Experimentation is recommended.
A closely related issue that affects planning time is collapsing of subqueries into their parent query. For example, consider:
This situation might arise from use of a view that contains a join; the view's SELECT
rule will be inserted in place of the view reference, yielding a query much like the above. Normally, the planner will try to collapse the subquery into the parent, yielding:
This usually results in a better plan than planning the subquery separately. (For example, the outer WHERE
conditions might be such that joining X to A first eliminates many rows of A, thus avoiding the need to form the full logical output of the subquery.) But at the same time, we have increased the planning time; here, we have a five-way join problem replacing two separate three-way join problems. Because of the exponential growth of the number of possibilities, this makes a big difference. The planner tries to avoid getting stuck in huge join search problems by not collapsing a subquery if more than from_collapse_limit
FROM
items would result in the parent query. You can trade off planning time against quality of plan by adjusting this run-time parameter up or down.
from_collapse_limit and join_collapse_limit are similarly named because they do almost the same thing: one controls when the planner will “flatten out” subqueries, and the other controls when it will flatten out explicit joins. Typically you would either set join_collapse_limit
equal to from_collapse_limit
(so that explicit joins and subqueries act similarly) or set join_collapse_limit
to 1 (if you want to control join order with explicit joins). But you might set them differently if you are trying to fine-tune the trade-off between planning time and run time.