支援的版本:目前版本 (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 的預設 文字 輸出格式,這種格式簡潔且方便人類閱讀。如果您想要將 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 計劃節點的 filter(過濾器) 條件應用。這表示該計劃節點會檢查它掃描的每一列的條件,並且只輸出通過條件的列。由於 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 列上。而是將其作為使用索引檢索的列上的過濾器應用。因此,成本實際上略有上升,以反映此額外的檢查。

在某些情況下,計劃器將更喜歡 simple(簡單) 索引掃描計劃

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,因此不需要檢索所有列,並且計劃器改變了主意。請注意,Index Scan 節點的總成本和列計數顯示為好像它已執行完成。但是,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)

在此計劃中,我們有一個巢狀迴圈聯接節點,其中有兩個表格掃描作為輸入或子節點。節點摘要行的縮排反映了計劃樹結構。聯接的第一個或 outer(外部) 子節點是一個類似於我們之前看到的位圖掃描。它的成本和列計數與我們從 SELECT ... WHERE unique1 < 10 獲得的相同,因為我們在該節點應用了 WHERE 子句 unique1 < 10t1.unique2 = t2.unique2 子句尚未相關,因此它不會影響外部掃描的列計數。巢狀迴圈聯接節點將為從外部子節點獲得的每一列執行一次第二個或 inner(內部) 子節點。來自目前外部列的列值可以插入到內部掃描中;在這裡,可以使用來自外部列的 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 索引中進行測試,因此它會在 join 節點上應用。這會減少 join 節點的估計輸出列數,但不會更改任何一個輸入掃描。

請注意,此處規劃器已選擇 實體化(materialize) join 的內部關係,方法是在其上方放置一個 Materialize 計畫節點。 這表示 t2 索引掃描只會執行一次,即使 nested-loop join 節點需要讀取該資料十次,即針對外部關係中的每一列一次。 Materialize 節點會在讀取資料時將其儲存在記憶體中,然後在每次後續傳遞時從記憶體中傳回資料。

在處理 outer join 時,您可能會看到 join 計畫節點同時附加了 Join Filter 和普通的 Filter 條件。 Join Filter 條件來自 outer join 的 ON 子句,因此即使某列未能滿足 Join Filter 條件,仍然可以作為 null 擴展列發出。 但是,普通的 Filter 條件會在 outer-join 規則之後應用,因此會無條件地刪除列。 在 inner join 中,這些類型的篩選器之間沒有語義上的差異。

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

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)

在這裡,規劃器選擇使用 hash join,其中一個表格的列會輸入到記憶體中的 hash 表格中,然後掃描另一個表格,並在 hash 表格中探測與每一列的匹配項。 再次注意縮排如何反映計畫結構:tenk1 上的 bitmap 掃描是 Hash 節點的輸入,該節點建構 hash 表格。 然後將其傳回到 Hash Join 節點,該節點從其外部子計畫讀取列,並搜尋每個列的 hash 表格。

另一種可能的 join 類型是 merge 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)

Merge join 要求其輸入資料按 join 鍵排序。 在此範例中,每個輸入都使用索引掃描以正確的順序訪問列來進行排序; 但是也可以使用循序掃描和排序。 (由於索引掃描需要非循序的磁碟存取,因此對於排序許多列,循序掃描和排序通常優於索引掃描。)

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

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)

這表明規劃器認為在這種情況下,hash join 的成本幾乎比 merge join 高 50%。 當然,下一個問題是它對此是否正確。 我們可以使用 EXPLAIN ANALYZE 進行調查,如下討論

某些查詢計畫涉及子計畫(subplan),這些子計畫來自原始查詢中的子-SELECT。 此類查詢有時可以轉換為普通的 join 計畫,但是當它們不能轉換時,我們會得到像這樣的計畫

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 會被傳遞下去),並且子選取(sub-select)的結果可用於外部計畫。 這些結果值由 EXPLAIN 顯示,並帶有類似 (subplan_name).colN 的標記,它指的是子-SELECT 的第 N 個輸出欄位。

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

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)

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

如果子-SELECT 除了不引用外部查詢的任何變數之外,還不能傳回多個列,則可以改為將其實現為initplan

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

Initplan 對於外部計畫的每次執行只執行一次,並且其結果會被儲存,以便在外部計畫的後續列中重複使用。 因此,在此範例中,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

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

在某些查詢計畫中,子計畫節點可能會多次執行。 例如,在上面的 nested-loop 計畫中,內部索引掃描將針對每個外部列執行一次。 在這種情況下,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 節點顯示了 hash 儲存桶和批次的數量,以及 hash 表格使用的記憶體峰值量。 (如果批次的數量超過一個,則也會涉及磁碟空間的使用,但不會顯示出來。)

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

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

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

合併排序(Merge join)也存在一些測量上的假象,可能會使不熟悉的人感到困惑。如果合併排序在其中一個輸入已耗盡,且另一個輸入中的下一個鍵值大於另一個輸入的最後一個鍵值時,它將停止讀取其中一個輸入;在這種情況下,不可能再有匹配項,因此無需掃描第一個輸入的其餘部分。這導致不會讀取所有子節點,結果與 LIMIT 類似。此外,如果外部(第一個)子節點包含具有重複鍵值的行,則內部(第二個)子節點會備份並重新掃描,以尋找與該鍵值匹配的部分行。EXPLAIN ANALYZE 會將這些相同內部行的重複發送次數,視為真實的額外行。當外部節點存在許多重複項時,內部子節點計畫節點報告的實際行數可能遠大於內部關係中實際存在的行數。

由於實作上的限制,BitmapAnd 和 BitmapOr 節點始終將其報告的實際行數顯示為零。

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

提交更正

如果您在文件中發現任何不正確、與您使用特定功能的經驗不符或需要進一步說明的地方,請使用此表單報告文件問題。