支援的版本: 目前 (17) / 16 / 15 / 14 / 13
開發版本: devel
不支援的版本: 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2

14.1. 使用 EXPLAIN #

PostgreSQL 為收到的每個查詢設計一個查詢計畫。選擇正確的計畫來匹配查詢結構和資料的屬性對於良好的效能至關重要,因此系統包含一個複雜的規劃器,嘗試選擇好的計畫。您可以使用 EXPLAIN 命令來查看規劃器為任何查詢建立的查詢計畫。計畫閱讀是一門需要一些經驗才能掌握的藝術,但本節試圖涵蓋基礎知識。

本節中的範例取自回歸測試資料庫,在執行 VACUUM ANALYZE 後,使用 v17 開發來源。如果您自己嘗試這些範例,您應該能夠獲得類似的結果,但您的預估成本和列計數可能會略有不同,因為 ANALYZE 的統計資訊是隨機樣本而不是精確的,並且因為成本本質上在某種程度上取決於平台。

這些範例使用 EXPLAIN 的預設「text」輸出格式,這種格式緊湊且方便人閱讀。如果您想將 EXPLAIN 的輸出提供給程式以進行進一步分析,您應該改用其機器可讀的輸出格式之一(XML、JSON 或 YAML)。

14.1.1. EXPLAIN 基礎 #

查詢計畫的結構是一個計畫節點的樹狀結構。樹狀結構底層的節點是掃描節點:它們從資料表中傳回原始列。對於不同的資料表存取方法,有不同類型的掃描節點:循序掃描、索引掃描和位元圖索引掃描。還有非資料表列來源,例如 VALUES 子句和 FROM 中的設定傳回函數,它們有自己的掃描節點類型。如果查詢需要對原始列進行聯結、彙總、排序或其他運算,則在掃描節點之上會有額外的節點來執行這些運算。同樣,通常有多種可能的方式來執行這些運算,因此也可能出現不同的節點類型。 EXPLAIN 的輸出對於計畫樹中的每個節點都有一行,顯示基本節點類型以及規劃器對該計畫節點執行所做的成本預估。額外的行可能會出現,從節點的摘要行縮排,以顯示節點的額外屬性。第一行(最頂層節點的摘要行)包含計畫的預估總執行成本;這是規劃器試圖最小化的數字。

這裡有一個簡單的範例,只是為了展示輸出的外觀

EXPLAIN SELECT * FROM tenk1;

                         QUERY PLAN
-------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..445.00 rows=10000 width=244)

由於此查詢沒有 WHERE 子句,因此它必須掃描資料表的所有列,因此規劃器已選擇使用簡單的循序掃描計畫。括號中引用的數字(從左到右)是

  • 預估啟動成本。這是輸出階段可以開始之前花費的時間,例如,在排序節點中進行排序的時間。

  • 預估總成本。這是假設計畫節點執行到完成,即檢索所有可用列的情況下說明的。實際上,節點的父節點可能會在讀取所有可用列之前停止(請參閱下面的 LIMIT 範例)。

  • 此計畫節點輸出的預估列數。同樣,假設節點執行到完成。

  • 此計畫節點輸出的預估平均列寬(以位元組為單位)。

成本以任意單位測量,由規劃器的成本參數決定(請參閱第 19.7.2 節)。傳統的做法是以磁碟頁面提取的單位來測量成本;也就是說,seq_page_cost 通常設定為 1.0,而其他成本參數相對於它設定。本節中的範例使用預設成本參數執行。

重要的是要了解,上層節點的成本包括其所有子節點的成本。重要的是還要意識到,成本僅反映規劃器關心的事項。特別是,成本不考慮將輸出值轉換為文字格式或將它們傳輸到用戶端所花費的時間,這可能是實際經過時間的重要因素;但規劃器忽略了這些成本,因為它無法透過更改計畫來改變它們。(我們相信,每個正確的計畫都將輸出相同的列集合。)

rows 值有點棘手,因為它不是計畫節點處理或掃描的列數,而是節點發出的列數。由於在節點上套用的任何 WHERE 子句條件進行篩選,這通常少於掃描的數量。理想情況下,頂層列預估值將近似於查詢實際傳回、更新或刪除的列數。

回到我們的範例

EXPLAIN SELECT * FROM tenk1;

                         QUERY PLAN
-------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..445.00 rows=10000 width=244)

這些數字的推導非常直接。如果你執行

SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1';

你會發現 tenk1 有 345 個磁碟頁面和 10000 列。估計成本的計算方式為 (讀取的磁碟頁面 * seq_page_cost) + (掃描的列數 * cpu_tuple_cost)。預設情況下,seq_page_cost 為 1.0,cpu_tuple_cost 為 0.01,所以估計成本為 (345 * 1.0) + (10000 * 0.01) = 445。

現在讓我們修改查詢,新增一個 WHERE 條件

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 7000;

                         QUERY PLAN
------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..470.00 rows=7000 width=244)
   Filter: (unique1 < 7000)

請注意,EXPLAIN 的輸出顯示 WHERE 子句被應用為附加到 Seq Scan 計劃節點的篩選條件。這意味著該計劃節點會檢查它掃描的每一列的條件,並且僅輸出通過該條件的列。由於 WHERE 子句,輸出列的估計值已經減少。但是,掃描仍然必須訪問所有 10000 列,因此成本沒有降低;事實上,它略有上升(確切地說是 10000 * cpu_operator_cost),以反映檢查 WHERE 條件所花費的額外 CPU 時間。

此查詢實際會選擇的列數為 7000,但 rows 的估計值僅是近似值。如果您嘗試複製此實驗,您可能會得到略有不同的估計值;此外,它可以在每次 ANALYZE 命令後發生變化,因為 ANALYZE 產生的統計資料取自表格的隨機樣本。

現在,讓我們使條件更加嚴格

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100;

                                  QUERY PLAN
-------------------------------------------------------------------​-----------
 Bitmap Heap Scan on tenk1  (cost=5.06..224.98 rows=100 width=244)
   Recheck Cond: (unique1 < 100)
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0)
         Index Cond: (unique1 < 100)

在這裡,規劃器決定使用一個兩步驟的計劃:子計劃節點訪問索引以找到符合索引條件的列的位置,然後上層計劃節點實際從表格本身獲取這些列。單獨獲取列比依序讀取它們昂貴得多,但是因為不必訪問表格的所有頁面,所以這仍然比循序掃描便宜。(使用兩個計劃層的原因是上層計劃節點在讀取它們之前,將索引識別的列位置排序成物理順序,以最大程度地減少單獨獲取的成本。節點名稱中提到的 bitmap 是執行排序的機制。)

現在讓我們在 WHERE 子句中新增另一個條件

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND stringu1 = 'xxx';

                                  QUERY PLAN
-------------------------------------------------------------------​-----------
 Bitmap Heap Scan on tenk1  (cost=5.04..225.20 rows=1 width=244)
   Recheck Cond: (unique1 < 100)
   Filter: (stringu1 = 'xxx'::name)
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0)
         Index Cond: (unique1 < 100)

新增的條件 stringu1 = 'xxx' 減少了輸出列數的估計值,但沒有減少成本,因為我們仍然需要訪問同一組列。這是因為 stringu1 子句不能作為索引條件應用,因為此索引僅在 unique1 欄位上。相反,它被用作對使用索引檢索的列的篩選。因此,成本實際上略有上升,以反映此額外的檢查。

在某些情況下,規劃器會偏好 簡單 的索引掃描計劃

EXPLAIN SELECT * FROM tenk1 WHERE unique1 = 42;

                                 QUERY PLAN
-------------------------------------------------------------------​----------
 Index Scan using tenk1_unique1 on tenk1  (cost=0.29..8.30 rows=1 width=244)
   Index Cond: (unique1 = 42)

在這種計劃類型中,表格列是以索引順序提取的,這使得它們的讀取成本更高,但是列太少,以至於對列位置進行排序的額外成本是不值得的。對於僅提取單個列的查詢,您最常看到這種計劃類型。它也經常用於具有與索引順序匹配的 ORDER BY 條件的查詢,因為這樣就不需要額外的排序步驟來滿足 ORDER BY。在此範例中,新增 ORDER BY unique1 將使用相同的計劃,因為索引已經隱含地提供了所請求的排序。

規劃器可以透過幾種方式實作 ORDER BY 子句。上面的範例顯示此類的排序子句可以隱式實作。規劃器也可以新增一個明確的 Sort 步驟

EXPLAIN SELECT * FROM tenk1 ORDER BY unique1;

                            QUERY PLAN
-------------------------------------------------------------------
 Sort  (cost=1109.39..1134.39 rows=10000 width=244)
   Sort Key: unique1
   ->  Seq Scan on tenk1  (cost=0.00..445.00 rows=10000 width=244)

如果計劃的一部分保證了對所需排序鍵的前綴進行排序,那麼規劃器可以改為決定使用 Incremental Sort 步驟

EXPLAIN SELECT * FROM tenk1 ORDER BY hundred, ten LIMIT 100;

                                              QUERY PLAN
-------------------------------------------------------------------​-----------------------------
 Limit  (cost=19.35..39.49 rows=100 width=244)
   ->  Incremental Sort  (cost=19.35..2033.39 rows=10000 width=244)
         Sort Key: hundred, ten
         Presorted Key: hundred
         ->  Index Scan using tenk1_hundred on tenk1  (cost=0.29..1574.20 rows=10000 width=244)

與常規排序相比,增量排序允許在整個結果集排序之前返回元組,這特別可以優化 LIMIT 查詢。它還可以減少記憶體使用量和將排序溢出到磁碟的可能性,但代價是增加了將結果集拆分為多個排序批次的開銷。

如果 WHERE 中引用的多個欄位上都有單獨的索引,則規劃器可能會選擇使用索引的 AND 或 OR 組合

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000;

                                     QUERY PLAN
-------------------------------------------------------------------​------------------
 Bitmap Heap Scan on tenk1  (cost=25.07..60.11 rows=10 width=244)
   Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))
   ->  BitmapAnd  (cost=25.07..25.07 rows=10 width=0)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0)
               Index Cond: (unique1 < 100)
         ->  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0)
               Index Cond: (unique2 > 9000)

但是這需要訪問兩個索引,因此與僅使用一個索引並將另一個條件視為篩選器相比,這不一定是一個優勢。如果您改變所涉及的範圍,您會看到計劃相應地變化。

這是一個顯示 LIMIT 效果的範例

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000 LIMIT 2;

                                     QUERY PLAN
-------------------------------------------------------------------​------------------
 Limit  (cost=0.29..14.28 rows=2 width=244)
   ->  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.27 rows=10 width=244)
         Index Cond: (unique2 > 9000)
         Filter: (unique1 < 100)

這與上面的查詢相同,但是我們新增了一個 LIMIT,以便不需要檢索所有列,並且規劃器改變了它的想法。請注意,索引掃描節點的總成本和列數顯示為如果它運行到完成。但是,預計 Limit 節點會在僅檢索了這些列的五分之一後停止,因此其總成本僅為五分之一,而這才是查詢的實際估計成本。與在先前的計劃中新增 Limit 節點相比,此計劃更受歡迎,因為 Limit 無法避免支付點陣圖掃描的啟動成本,因此使用該方法總成本將超過 25 個單位。

讓我們嘗試使用我們一直在討論的欄位來連接兩個表格

EXPLAIN SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;

                                      QUERY PLAN
-------------------------------------------------------------------​-------------------
 Nested Loop  (cost=4.65..118.50 rows=10 width=488)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.38 rows=10 width=244)
         Recheck Cond: (unique1 < 10)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0)
               Index Cond: (unique1 < 10)
   ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244)
         Index Cond: (unique2 = t1.unique2)

在這個計畫中,我們有一個巢狀迴圈結合節點,它有兩個資料表掃描作為輸入或子節點。節點摘要行的縮排反映了計畫樹狀結構。該結合的第一個,或稱外部子節點是一個位元圖掃描,與我們之前看到的類似。它的成本和列數與我們從 SELECT ... WHERE unique1 < 10 得到的相同,因為我們在該節點應用了 WHERE 子句 unique1 < 10t1.unique2 = t2.unique2 子句目前還無關,因此它不會影響外部掃描的列數。巢狀迴圈結合節點將為從外部子節點獲得的每一列執行一次它的第二個,或稱內部子節點。來自當前外部列的欄位值可以插入到內部掃描中;在這裡,外部列的 t1.unique2 值是可用的,因此我們獲得一個計畫和成本,類似於我們上面看到的簡單 SELECT ... WHERE t2.unique2 = constant 的情況。(估計成本實際上比上面看到的略低,這是由於在 t2 上重複索引掃描期間預期會發生的快取。)然後,迴圈節點的成本是根據外部掃描的成本,加上每個外部列內部掃描的一次重複(這裡為 10 * 7.90),再加上少量的 CPU 時間來進行結合處理來設定的。

在這個範例中,結合的輸出列數與兩個掃描的列數的乘積相同,但在所有情況下並非如此,因為可能還有額外的 WHERE 子句,這些子句同時提及了兩個資料表,因此只能在結合點應用,而不能應用於任何一個輸入掃描。這是一個範例

EXPLAIN SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10 AND t2.unique2 < 10 AND t1.hundred < t2.hundred;

                                         QUERY PLAN
-------------------------------------------------------------------​--------------------------
 Nested Loop  (cost=4.65..49.36 rows=33 width=488)
   Join Filter: (t1.hundred < t2.hundred)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.38 rows=10 width=244)
         Recheck Cond: (unique1 < 10)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0)
               Index Cond: (unique1 < 10)
   ->  Materialize  (cost=0.29..8.51 rows=10 width=244)
         ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..8.46 rows=10 width=244)
               Index Cond: (unique2 < 10)

條件 t1.hundred < t2.hundred 無法在 tenk2_unique2 索引中測試,因此它是在結合節點應用的。這降低了結合節點的估計輸出列數,但不會更改任何一個輸入掃描。

請注意,在這裡,規劃器選擇實體化結合的內部關係,方法是將 Materialize 計畫節點放在其頂部。這表示即使巢狀迴圈結合節點需要讀取該資料十次,對於來自外部關係的每一列,t2 索引掃描也只會執行一次。Materialize 節點會在讀取資料時將資料儲存在記憶體中,然後在每次後續傳遞時從記憶體中返回資料。

在處理外部結合時,您可能會看到同時附加了結合篩選器和普通篩選器條件的結合計畫節點。結合篩選器條件來自外部結合的 ON 子句,因此未能通過結合篩選器條件的列仍然可以作為空值擴展列發出。但是,普通的篩選器條件是在外部結合規則之後應用的,因此會無條件地移除列。在內部結合中,這些類型的篩選器之間沒有語義差異。

如果我們稍微更改查詢的選擇性,我們可能會得到一個非常不同的結合計畫

EXPLAIN SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;

                                        QUERY PLAN
-------------------------------------------------------------------​-----------------------
 Hash Join  (cost=226.23..709.73 rows=100 width=488)
   Hash Cond: (t2.unique2 = t1.unique2)
   ->  Seq Scan on tenk2 t2  (cost=0.00..445.00 rows=10000 width=244)
   ->  Hash  (cost=224.98..224.98 rows=100 width=244)
         ->  Bitmap Heap Scan on tenk1 t1  (cost=5.06..224.98 rows=100 width=244)
               Recheck Cond: (unique1 < 100)
               ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0)
                     Index Cond: (unique1 < 100)

在這裡,規劃器選擇使用雜湊結合,其中一個資料表的列會輸入到一個記憶體中的雜湊表,然後掃描另一個資料表,並探測雜湊表以尋找與每一列的匹配項。再次注意縮排如何反映計畫結構:tenk1 上的位元圖掃描是 Hash 節點的輸入,它會建構雜湊表。然後,它會返回給 Hash Join 節點,該節點從其外部子節點計畫讀取列,並搜尋雜湊表以尋找每一列。

另一種可能的結合類型是合併結合,如下圖所示

EXPLAIN SELECT *
FROM tenk1 t1, onek t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;

                                        QUERY PLAN
-------------------------------------------------------------------​-----------------------
 Merge Join  (cost=0.56..233.49 rows=10 width=488)
   Merge Cond: (t1.unique2 = t2.unique2)
   ->  Index Scan using tenk1_unique2 on tenk1 t1  (cost=0.29..643.28 rows=100 width=244)
         Filter: (unique1 < 100)
   ->  Index Scan using onek_unique2 on onek t2  (cost=0.28..166.28 rows=1000 width=244)

合併結合要求其輸入資料根據結合鍵進行排序。在這個範例中,每個輸入都透過使用索引掃描以正確的順序訪問列來進行排序;但是也可以使用循序掃描和排序。(由於索引掃描所需的非循序磁碟訪問,循序掃描和排序通常比索引掃描在排序多列時更有效率。)

查看變體計畫的一種方法是強制規劃器忽略它認為最便宜的策略,使用 第 19.7.1 節 中描述的啟用/停用標誌。(這是一個粗略的工具,但很有用。另請參閱 第 14.3 節。)例如,如果我們不相信合併結合是前一個範例的最佳結合類型,我們可以嘗試

SET enable_mergejoin = off;

EXPLAIN SELECT *
FROM tenk1 t1, onek t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;

                                        QUERY PLAN
-------------------------------------------------------------------​-----------------------
 Hash Join  (cost=226.23..344.08 rows=10 width=488)
   Hash Cond: (t2.unique2 = t1.unique2)
   ->  Seq Scan on onek t2  (cost=0.00..114.00 rows=1000 width=244)
   ->  Hash  (cost=224.98..224.98 rows=100 width=244)
         ->  Bitmap Heap Scan on tenk1 t1  (cost=5.06..224.98 rows=100 width=244)
               Recheck Cond: (unique1 < 100)
               ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0)
                     Index Cond: (unique1 < 100)

這表示規劃器認為在這種情況下,雜湊結合的成本幾乎比合併結合高出 50%。當然,下一個問題是它是否正確。我們可以透過使用 EXPLAIN ANALYZE 來調查,如下面 討論

一些查詢計畫涉及子計畫,這些子計畫來自原始查詢中的子 SELECT。這樣的查詢有時可以轉換為普通的結合計畫,但如果不能,我們會得到像這樣的計畫

EXPLAIN VERBOSE SELECT unique1
FROM tenk1 t
WHERE t.ten < ALL (SELECT o.ten FROM onek o WHERE o.four = t.four);

                               QUERY PLAN
-------------------------------------------------------------------​------
 Seq Scan on public.tenk1 t  (cost=0.00..586095.00 rows=5000 width=4)
   Output: t.unique1
   Filter: (ALL (t.ten < (SubPlan 1).col1))
   SubPlan 1
     ->  Seq Scan on public.onek o  (cost=0.00..116.50 rows=250 width=4)
           Output: o.ten
           Filter: (o.four = t.four)

這個相當人工的範例旨在說明幾個要點:來自外部計畫層級的值可以向下傳遞到子計畫中(在這裡,t.four 被向下傳遞),並且子選擇的結果可用於外部計畫。這些結果值由 EXPLAIN 顯示,帶有類似 (subplan_name).colN 的註釋,它指的是子 SELECT 的第 N 個輸出欄位。

在上面的範例中,ALL 運算符會為外部查詢的每一列再次執行子計畫(這說明了高估計成本)。某些查詢可以使用雜湊子計畫來避免這種情況

EXPLAIN SELECT *
FROM tenk1 t
WHERE t.unique1 NOT IN (SELECT o.unique1 FROM onek o);

                                         QUERY PLAN
-------------------------------------------------------------------​-------------------------
 Seq Scan on tenk1 t  (cost=61.77..531.77 rows=5000 width=244)
   Filter: (NOT (ANY (unique1 = (hashed SubPlan 1).col1)))
   SubPlan 1
     ->  Index Only Scan using onek_unique1 on onek o  (cost=0.28..59.27 rows=1000 width=4)
(4 rows)

在這裡,子計畫只執行一次,其輸出會載入到記憶體中的雜湊表中,然後外部 ANY 運算符會探測該雜湊表。這要求子 SELECT 不引用外部查詢的任何變數,並且 ANY 的比較運算符適用於雜湊。

如果子 SELECT 除了不引用外部查詢的任何變數之外,還不能返回多於一列,則可以改為將其實作為初始計畫

EXPLAIN VERBOSE SELECT unique1
FROM tenk1 t1 WHERE t1.ten = (SELECT (random() * 10)::integer);

                             QUERY PLAN
------------------------------------------------------------​--------
 Seq Scan on public.tenk1 t1  (cost=0.02..470.02 rows=1000 width=4)
   Output: t1.unique1
   Filter: (t1.ten = (InitPlan 1).col1)
   InitPlan 1
     ->  Result  (cost=0.00..0.02 rows=1 width=4)
           Output: ((random() * '10'::double precision))::integer

初始計畫對於外部計畫的每次執行只執行一次,其結果會被儲存起來,以便在外部計畫的後續列中重複使用。因此,在這個範例中,random() 只會被評估一次,並且 t1.ten 的所有值都會與同一個隨機選擇的整數進行比較。這與沒有子 SELECT 建構的情況截然不同。

14.1.2. EXPLAIN ANALYZE #

可以使用 EXPLAINANALYZE 選項來檢查規劃器估計的準確性。使用此選項,EXPLAIN 實際上會執行查詢,然後顯示每個計畫節點中累積的真實列數和真實執行時間,以及普通 EXPLAIN 顯示的相同估計值。例如,我們可能會得到像這樣的結果

EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;

                                                           QUERY PLAN
-------------------------------------------------------------------​--------------------------------------------------------------
 Nested Loop  (cost=4.65..118.50 rows=10 width=488) (actual time=0.017..0.051 rows=10 loops=1)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.38 rows=10 width=244) (actual time=0.009..0.017 rows=10 loops=1)
         Recheck Cond: (unique1 < 10)
         Heap Blocks: exact=10
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
               Index Cond: (unique1 < 10)
   ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
         Index Cond: (unique2 = t1.unique2)
 Planning Time: 0.485 ms
 Execution Time: 0.073 ms

請注意,實際時間的值是以真實時間的毫秒為單位,而 cost 估計值則以任意單位表示;因此它們不太可能匹配。通常最重要的是查看估計的列計數是否與實際情況合理接近。在本範例中,估計值完全準確,但在實務中非常罕見。

在某些查詢計畫中,子計畫節點可能會執行多次。 例如,在上述巢狀迴圈計畫中,內部索引掃描將針對每個外部列執行一次。 在這種情況下,loops 值報告節點的總執行次數,而顯示的實際時間和列值是每次執行的平均值。 這樣做是為了使數字與成本估算值的顯示方式具有可比性。 乘以 loops 值即可獲得在節點中實際花費的總時間。 在上面的範例中,我們總共花了 0.030 毫秒來執行 tenk2 上的索引掃描。

在某些情況下,EXPLAIN ANALYZE 除了顯示計畫節點的執行時間和列計數外,還會顯示額外的執行統計資訊。 例如,Sort 和 Hash 節點會提供額外的資訊。

EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;

                                                                 QUERY PLAN
-------------------------------------------------------------------​-------------------------------------------------------------------​------
 Sort  (cost=713.05..713.30 rows=100 width=488) (actual time=2.995..3.002 rows=100 loops=1)
   Sort Key: t1.fivethous
   Sort Method: quicksort  Memory: 74kB
   ->  Hash Join  (cost=226.23..709.73 rows=100 width=488) (actual time=0.515..2.920 rows=100 loops=1)
         Hash Cond: (t2.unique2 = t1.unique2)
         ->  Seq Scan on tenk2 t2  (cost=0.00..445.00 rows=10000 width=244) (actual time=0.026..1.790 rows=10000 loops=1)
         ->  Hash  (cost=224.98..224.98 rows=100 width=244) (actual time=0.476..0.477 rows=100 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 35kB
               ->  Bitmap Heap Scan on tenk1 t1  (cost=5.06..224.98 rows=100 width=244) (actual time=0.030..0.450 rows=100 loops=1)
                     Recheck Cond: (unique1 < 100)
                     Heap Blocks: exact=90
                     ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                           Index Cond: (unique1 < 100)
 Planning Time: 0.187 ms
 Execution Time: 3.036 ms

Sort 節點會顯示使用的排序方法(特別是排序是在記憶體中還是磁碟上進行)以及所需的記憶體或磁碟空間量。 Hash 節點會顯示雜湊桶和批次的數量,以及雜湊表使用的記憶體峰值量。(如果批次數量超過 1,則還會涉及磁碟空間的使用,但不會顯示。)

另一種額外資訊的類型是篩選條件移除的列數。

EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE ten < 7;

                                               QUERY PLAN
-------------------------------------------------------------------​--------------------------------------
 Seq Scan on tenk1  (cost=0.00..470.00 rows=7000 width=244) (actual time=0.030..1.995 rows=7000 loops=1)
   Filter: (ten < 7)
   Rows Removed by Filter: 3000
 Planning Time: 0.102 ms
 Execution Time: 2.145 ms

這些計數對於在聯結節點上應用的篩選條件尤其有價值。 只有當至少有一個掃描的列(或聯結節點的情況下,潛在的聯結對)被篩選條件拒絕時,才會出現 Rows Removed 行。

與篩選條件類似的情況發生在使用 lossy 索引掃描的情況下。 例如,考慮以下搜尋包含特定點的多邊形:

EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)';

                                              QUERY PLAN
-------------------------------------------------------------------​-----------------------------------
 Seq Scan on polygon_tbl  (cost=0.00..1.09 rows=1 width=85) (actual time=0.023..0.023 rows=0 loops=1)
   Filter: (f1 @> '((0.5,2))'::polygon)
   Rows Removed by Filter: 7
 Planning Time: 0.039 ms
 Execution Time: 0.033 ms

規劃器認為(非常正確)此範例表太小,不值得使用索引掃描,因此我們有一個簡單的循序掃描,其中所有列都被篩選條件拒絕。 但是,如果我們強制使用索引掃描,我們會看到:

SET enable_seqscan TO off;

EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)';

                                                        QUERY PLAN
-------------------------------------------------------------------​-------------------------------------------------------
 Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
   Index Cond: (f1 @> '((0.5,2))'::polygon)
   Rows Removed by Index Recheck: 1
 Planning Time: 0.039 ms
 Execution Time: 0.098 ms

在這裡,我們可以看到索引傳回了一個候選列,然後透過重新檢查索引條件將其拒絕。 發生這種情況是因為 GiST 索引對於多邊形包含測試而言是 lossy:它實際上傳回具有與目標重疊的多邊形的列,然後我們必須對這些列執行精確的包含測試。

EXPLAIN 有一個 BUFFERS 選項,可以與 ANALYZE 一起使用,以獲得更多執行時間統計資訊。

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000;

                                                           QUERY PLAN
-------------------------------------------------------------------​--------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=25.07..60.11 rows=10 width=244) (actual time=0.105..0.114 rows=10 loops=1)
   Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))
   Heap Blocks: exact=10
   Buffers: shared hit=14 read=3
   ->  BitmapAnd  (cost=25.07..25.07 rows=10 width=0) (actual time=0.100..0.101 rows=0 loops=1)
         Buffers: shared hit=4 read=3
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
               Index Cond: (unique1 < 100)
               Buffers: shared hit=2
         ->  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
               Index Cond: (unique2 > 9000)
               Buffers: shared hit=2 read=3
 Planning:
   Buffers: shared hit=3
 Planning Time: 0.162 ms
 Execution Time: 0.143 ms

BUFFERS 提供的數字有助於識別查詢中 I/O 密集度最高的部分。

請記住,因為 EXPLAIN ANALYZE 實際上會執行查詢,因此任何副作用都會像往常一樣發生,即使查詢可能輸出的任何結果都會被丟棄,以支持列印 EXPLAIN 數據。 如果您想分析一個修改資料的查詢,而不更改您的表,您可以在之後回滾該命令,例如:

BEGIN;

EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 < 100;

                                                           QUERY PLAN
-------------------------------------------------------------------​-------------------------------------------------------------
 Update on tenk1  (cost=5.06..225.23 rows=0 width=0) (actual time=1.634..1.635 rows=0 loops=1)
   ->  Bitmap Heap Scan on tenk1  (cost=5.06..225.23 rows=100 width=10) (actual time=0.065..0.141 rows=100 loops=1)
         Recheck Cond: (unique1 < 100)
         Heap Blocks: exact=90
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
               Index Cond: (unique1 < 100)
 Planning Time: 0.151 ms
 Execution Time: 1.856 ms

ROLLBACK;

如本範例所示,當查詢是 INSERTUPDATEDELETEMERGE 命令時,套用表變更的實際工作是由頂層的 Insert、Update、Delete 或 Merge 計畫節點完成的。 此節點下的計畫節點執行定位舊列和/或計算新資料的工作。 因此,在上面,我們看到了我們已經看到的相同類型的位元圖表掃描,並且其輸出被饋送到儲存更新列的 Update 節點。 值得注意的是,儘管修改資料的節點可能會花費相當長的時間(在這裡,它消耗了大部分時間),但規劃器目前不會在成本估算中添加任何內容來計算該工作。 這是因為對於每個正確的查詢計畫,要完成的工作都是相同的,因此它不會影響規劃決策。

UPDATEDELETEMERGE 命令影響分割表或繼承層次結構時,輸出可能如下所示:

EXPLAIN UPDATE gtest_parent SET f1 = CURRENT_DATE WHERE f2 = 101;

                                       QUERY PLAN
-------------------------------------------------------------------​---------------------
 Update on gtest_parent  (cost=0.00..3.06 rows=0 width=0)
   Update on gtest_child gtest_parent_1
   Update on gtest_child2 gtest_parent_2
   Update on gtest_child3 gtest_parent_3
   ->  Append  (cost=0.00..3.06 rows=3 width=14)
         ->  Seq Scan on gtest_child gtest_parent_1  (cost=0.00..1.01 rows=1 width=14)
               Filter: (f2 = 101)
         ->  Seq Scan on gtest_child2 gtest_parent_2  (cost=0.00..1.01 rows=1 width=14)
               Filter: (f2 = 101)
         ->  Seq Scan on gtest_child3 gtest_parent_3  (cost=0.00..1.01 rows=1 width=14)
               Filter: (f2 = 101)

在本範例中,Update 節點需要考慮三個子表,而不是最初提到的分割表(因為它從不儲存任何資料)。 因此,每個表都有三個輸入掃描子計畫。 為了清楚起見,Update 節點被註釋以顯示將被更新的特定目標表,其順序與相應的子計畫相同。

EXPLAIN ANALYZE 顯示的 Planning time 是從解析的查詢產生查詢計畫並對其進行最佳化的時間。 它不包括解析或重寫。

EXPLAIN ANALYZE 顯示的 Execution time 包括執行器的啟動和關閉時間,以及執行任何觸發器的時間,但不包括解析、重寫或規劃時間。 如果有任何 BEFORE 觸發器,則執行它們所花費的時間包含在相關的 Insert、Update 或 Delete 節點的時間中; 但執行 AFTER 觸發器所花費的時間不包含在該時間中,因為 AFTER 觸發器在整個計畫完成後觸發。 每個觸發器(BEFOREAFTER)中花費的總時間也單獨顯示。 請注意,延遲約束觸發器將在事務結束時執行,因此 EXPLAIN ANALYZE 完全不考慮。

頂層節點顯示的時間不包括將查詢的輸出資料轉換為可顯示的格式或將其發送到客戶端所需的任何時間。 雖然 EXPLAIN ANALYZE 永遠不會將資料發送到客戶端,但可以透過指定 SERIALIZE 選項來告知它將查詢的輸出資料轉換為可顯示的格式並測量所需的時間。 該時間將單獨顯示,並且也包含在總 Execution time 中。

14.1.3. 注意事項 #

EXPLAIN ANALYZE 測量的執行時間與相同查詢的正常執行時間之間可能存在兩種顯著的偏差。 首先,由於沒有輸出列傳遞到客戶端,因此不包括網路傳輸成本。 除非指定 SERIALIZE,否則也不包括 I/O 轉換成本。 其次,EXPLAIN ANALYZE 添加的測量開銷可能很大,尤其是在 gettimeofday() 作業系統呼叫速度較慢的機器上。 您可以使用 pg_test_timing 工具來測量系統上的計時開銷。

不應將 EXPLAIN 的結果外推到與您實際測試的情況差異很大的情境;例如,不能假設在玩具大小的資料表上獲得的結果適用於大型資料表。規劃器的成本估算不是線性的,因此它可能會為較大或較小的資料表選擇不同的計畫。一個極端的例子是,在僅佔用一個磁碟頁面的資料表上,無論索引是否可用,您幾乎總會得到循序掃描計畫。規劃器意識到無論如何處理資料表都需要讀取一個磁碟頁面,因此沒有必要花費額外的頁面讀取來查看索引。(我們在上面的 polygon_tbl 範例中看到了這種情況。)

在某些情況下,實際值和估計值可能無法很好地匹配,但實際上並沒有任何問題。其中一種情況是,計畫節點的執行被 LIMIT 或類似效果提前停止。例如,在我們之前使用的 LIMIT 查詢中,

EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000 LIMIT 2;

                                                          QUERY PLAN
-------------------------------------------------------------------​------------------------------------------------------------
 Limit  (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1)
   ->  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
         Index Cond: (unique2 > 9000)
         Filter: (unique1 < 100)
         Rows Removed by Filter: 287
 Planning Time: 0.077 ms
 Execution Time: 0.086 ms

索引掃描節點的估計成本和列數顯示為好像它已執行完成。但實際上,Limit 節點在獲得兩列後停止請求列,因此實際列數僅為 2,並且運行時間少於成本估算所建議的時間。這不是估計錯誤,只是估計值和真實值的顯示方式存在差異。

合併聯結也具有可能會混淆不小心的人的測量假象。如果合併聯結耗盡了一個輸入,並且一個輸入中的下一個鍵值大於另一個輸入的最後一個鍵值,則它將停止讀取一個輸入;在這種情況下,不可能再有匹配項,因此不需要掃描第一個輸入的其餘部分。這導致未讀取一個子節點的所有內容,其結果與 LIMIT 提及的結果類似。此外,如果外部(第一個)子節點包含具有重複鍵值的列,則會備份內部(第二個)子節點並重新掃描其與該鍵值匹配的部分列。EXPLAIN ANALYZE 將相同內部列的重複發射計為真實的額外列。當外部重複項很多時,內部子節點計畫節點的報告的實際列數可能顯著大於內部關係中實際存在的列數。

由於實作限制,BitmapAnd 和 BitmapOr 節點始終將其各自的實際列數報告為零。

通常,EXPLAIN 將顯示規劃器建立的每個計畫節點。但是,在某些情況下,執行器可以確定某些節點不需要執行,因為基於規劃時不可用的參數值,它們無法產生任何列。(目前,這僅適用於掃描分割資料表的 Append 或 MergeAppend 節點的子節點。)發生這種情況時,這些計畫節點將從 EXPLAIN 輸出中省略,並顯示 Subplans Removed: N 註解。

提交更正

如果您在文件中發現任何不正確之處、與您使用特定功能的經驗不符或需要進一步澄清之處,請使用此表格來回報文件問題。