EXPLAIN
#PostgreSQL 會為它收到的每個查詢生成一個查詢計劃。選擇正確的計劃以匹配查詢結構和資料屬性對於良好的效能至關重要,因此係統包含一個複雜的規劃器,它會嘗試選擇好的計劃。您可以使用 EXPLAIN
命令來檢視規劃器為任何查詢生成的查詢計劃。閱讀計劃是一門需要一些經驗才能掌握的藝術,但本節將嘗試涵蓋基礎知識。
本節中的示例來自迴歸測試資料庫,在執行了 VACUUM ANALYZE
之後,使用 v18 的開發原始碼。如果您嘗試自行執行示例,應該會得到類似的結果,但您估計的成本和行數可能會略有不同,因為 ANALYZE
的統計資訊是隨機抽樣而不是精確的,並且成本本質上在某種程度上是平臺相關的。
示例使用 EXPLAIN
的預設“文字”輸出格式,這種格式緊湊且方便人類閱讀。如果您想將 EXPLAIN
的輸出提供給程式進行進一步分析,則應使用其機器可讀輸出格式(XML、JSON 或 YAML)之一。
EXPLAIN
基礎知識 #查詢計劃的結構是計劃節點的樹。樹的底層節點是掃描節點:它們從表中返回原始行。對於不同的表訪問方法,存在不同的掃描節點型別:順序掃描、索引掃描和點陣圖索引掃描。還有非錶行源,例如 FROM
中的 VALUES
子句和集合返回函式,它們有自己的掃描節點型別。如果查詢需要對原始行進行連線、聚合、排序或其他操作,那麼在掃描節點之上將會有額外的節點來執行這些操作。同樣,通常有不止一種執行這些操作的方法,因此這裡也可能出現不同的節點型別。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
子句作為附加到順序掃描計劃節點的“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
列上。相反,它被用作檢索到的使用索引的行的過濾器。因此,成本實際上略有增加,以反映這種額外的檢查。
在某些情況下,規劃器會傾向於使用“簡單”索引掃描計劃
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)
在此計劃中,我們有一個巢狀迴圈連線節點,帶有兩個表掃描作為輸入或子節點。節點摘要行的縮排反映了計劃樹結構。連線的第一個(或“outer”)子節點是類似我們之前看到的點陣圖掃描。其成本和行數與我們從 SELECT ... WHERE unique1 < 10
得到的相同,因為我們在該節點應用了 WHERE
子句 unique1 < 10
。 t1.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
索引中測試,因此它在連線節點處應用。這減少了連線節點的估計輸出行數,但並未更改任何輸入掃描。
請注意,在這裡規劃器選擇“物化”連線的內部關係,方法是將 Materialize 計劃節點放在它之上。這意味著 t2
索引掃描將只執行一次,即使巢狀迴圈連線節點需要讀取該資料十次,每次從外部關係讀取一行。Materialize 節點在讀取資料時將其儲存在記憶體中,然後在每次後續傳遞中從記憶體返回資料。
在處理外部連線時,您可能會看到帶有 “Join Filter” 和普通 “Filter” 條件的連線計劃節點。Join Filter 條件來自外部連線的 ON
子句,因此一個失敗 Join Filter 條件的行仍然可以作為空擴充套件行發出。但普通 Filter 條件在外部連線規則之後應用,因此會無條件地移除行。在內部連線中,這些型別的過濾器之間沒有語義差異。
如果我們稍微更改查詢的選擇性,我們可能會得到一個非常不同的連線計劃
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 節中描述的 enable/disable 標誌。(這是一個粗略的工具,但很有用。另請參閱 第 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
進行調查,如下文所述。
在使用 enable/disable 標誌停用計劃節點型別時,許多標誌僅阻止使用相應的計劃節點,而不會完全剝奪規劃器使用計劃節點型別的能力。這是故意的,以便規劃器仍能為給定的查詢形成一個計劃。當生成的計劃包含一個停用的節點時,EXPLAIN
輸出將指示此事實。
SET enable_seqscan = off; EXPLAIN SELECT * FROM unit; QUERY PLAN --------------------------------------------------------- Seq Scan on unit (cost=0.00..21.30 rows=1130 width=44) Disabled: true
由於 unit
表沒有索引,因此沒有其他方法可以讀取表資料,因此順序掃描是查詢規劃器可用的唯一選項。
某些查詢計劃涉及子計劃,這些子計劃來自原始查詢中的子 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
不引用外部查詢的任何變數,並且子 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
結構時發生的情況大不相同。
EXPLAIN ANALYZE
#可以使用 EXPLAIN
的 ANALYZE
選項來檢查規劃器估計的準確性。使用此選項時,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.00 loops=1) Buffers: shared hit=36 read=6 -> Bitmap Heap Scan on tenk1 t1 (cost=4.36..39.38 rows=10 width=244) (actual time=0.009..0.017 rows=10.00 loops=1) Recheck Cond: (unique1 < 10) Heap Blocks: exact=10 Buffers: shared hit=3 read=5 written=4 -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10.00 loops=1) Index Cond: (unique1 < 10) Index Searches: 1 Buffers: shared hit=2 -> 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.00 loops=10) Index Cond: (unique2 = t1.unique2) Index Searches: 10 Buffers: shared hit=24 read=6 Planning: Buffers: shared hit=15 dirtied=9 Planning Time: 0.485 ms Execution Time: 0.073 ms
請注意,“actual time”值以毫秒為單位的真即時間表示,而 cost
估計以任意單位表示;因此不太可能匹配。通常最重要的是檢視估計的行數是否與實際情況 reasonably close。在此示例中,估計值都非常準確,但這在實踐中相當不尋常。
在某些查詢計劃中,子計劃節點可能被執行多次。例如,在上面的巢狀迴圈計劃中,內部索引掃描將為每個外部行執行一次。在這種情況下,loops
值報告節點執行的總次數,並且顯示的實際時間和行值是每次執行的平均值。這樣做是為了使數字與成本估計的顯示方式可比。乘以 loops
值可獲得在節點中實際花費的總時間。在上例中,我們在執行 tenk2
上的索引掃描中總共花費了 0.030 毫秒。
在某些情況下,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.00 loops=1) Sort Key: t1.fivethous Sort Method: quicksort Memory: 74kB Buffers: shared hit=440 -> Hash Join (cost=226.23..709.73 rows=100 width=488) (actual time=0.515..2.920 rows=100.00 loops=1) Hash Cond: (t2.unique2 = t1.unique2) Buffers: shared hit=437 -> Seq Scan on tenk2 t2 (cost=0.00..445.00 rows=10000 width=244) (actual time=0.026..1.790 rows=10000.00 loops=1) Buffers: shared hit=345 -> Hash (cost=224.98..224.98 rows=100 width=244) (actual time=0.476..0.477 rows=100.00 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 35kB Buffers: shared hit=92 -> Bitmap Heap Scan on tenk1 t1 (cost=5.06..224.98 rows=100 width=244) (actual time=0.030..0.450 rows=100.00 loops=1) Recheck Cond: (unique1 < 100) Heap Blocks: exact=90 Buffers: shared hit=92 -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100.00 loops=1) Index Cond: (unique1 < 100) Index Searches: 1 Buffers: shared hit=2 Planning: Buffers: shared hit=12 Planning Time: 0.187 ms Execution Time: 3.036 ms
Sort 節點顯示使用的排序方法(特別是,排序是在記憶體中還是在磁碟上)以及所需的記憶體或磁碟空間量。Hash 節點顯示雜湊桶和批次的數量以及雜湊表使用的峰值記憶體量。(如果批次數超過一個,也會涉及磁碟空間使用,但此處未顯示。)
Index Scan 節點(以及 Bitmap Index Scan 和 Index-Only Scan 節點)顯示“Index Searches”行,該行報告 所有 節點執行/ loops
的總搜尋次數
EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE thousand IN (1, 500, 700, 999); QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on tenk1 (cost=9.45..73.44 rows=40 width=244) (actual time=0.012..0.028 rows=40.00 loops=1) Recheck Cond: (thousand = ANY ('{1,500,700,999}'::integer[])) Heap Blocks: exact=39 Buffers: shared hit=47 -> Bitmap Index Scan on tenk1_thous_tenthous (cost=0.00..9.44 rows=40 width=0) (actual time=0.009..0.009 rows=40.00 loops=1) Index Cond: (thousand = ANY ('{1,500,700,999}'::integer[])) Index Searches: 4 Buffers: shared hit=8 Planning Time: 0.029 ms Execution Time: 0.034 ms
這裡我們看到一個 Bitmap Index Scan 節點,它需要 4 次單獨的索引搜尋。對於謂詞的 IN
構造的每個 integer
值,掃描需要從 tenk1_thous_tenthous
索引的根頁面開始搜尋索引一次。但是,索引搜尋的數量通常與查詢謂詞沒有這種簡單的對應關係
EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE thousand IN (1, 2, 3, 4); QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on tenk1 (cost=9.45..73.44 rows=40 width=244) (actual time=0.009..0.019 rows=40.00 loops=1) Recheck Cond: (thousand = ANY ('{1,2,3,4}'::integer[])) Heap Blocks: exact=38 Buffers: shared hit=40 -> Bitmap Index Scan on tenk1_thous_tenthous (cost=0.00..9.44 rows=40 width=0) (actual time=0.005..0.005 rows=40.00 loops=1) Index Cond: (thousand = ANY ('{1,2,3,4}'::integer[])) Index Searches: 1 Buffers: shared hit=2 Planning Time: 0.029 ms Execution Time: 0.026 ms
此 IN
查詢的變體只執行了 1 次索引搜尋。它在遍歷索引上花費的時間(與原始查詢相比)更少,因為其 IN
構造使用的值匹配儲存在相鄰索引元組上的值,在同一個 tenk1_thous_tenthous
索引葉子頁面上。
“Index Searches”行對於應用跳過掃描最佳化以更有效地遍歷索引的 B-tree 索引掃描也很有用
EXPLAIN ANALYZE SELECT four, unique1 FROM tenk1 WHERE four BETWEEN 1 AND 3 AND unique1 = 42; QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------------- Index Only Scan using tenk1_four_unique1_idx on tenk1 (cost=0.29..6.90 rows=1 width=8) (actual time=0.006..0.007 rows=1.00 loops=1) Index Cond: ((four >= 1) AND (four <= 3) AND (unique1 = 42)) Heap Fetches: 0 Index Searches: 3 Buffers: shared hit=7 Planning Time: 0.029 ms Execution Time: 0.012 ms
這裡我們看到一個 Index-Only Scan 節點使用了 tenk1_four_unique1_idx
,這是一個多列索引,位於 tenk1
表的 four
和 unique1
列上。掃描執行 3 次搜尋,每次讀取一個索引葉子頁面:““four = 1 AND unique1 = 42
””、““four = 2 AND unique1 = 42
””和““four = 3 AND unique1 = 42
””。此索引通常是跳過掃描的一個好目標,因為正如第 11.3 節中所述,其前導列(four
列)僅包含 4 個不同的值,而其第二/最後一列(unique1
列)包含許多不同的值。
另一種型別的額外資訊是透過過濾器條件移除的行數
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.00 loops=1) Filter: (ten < 7) Rows Removed by Filter: 3000 Buffers: shared hit=345 Planning Time: 0.102 ms Execution Time: 2.145 ms
這些計數對於應用於連線節點的過濾器條件尤其有價值。“Rows Removed”行僅在至少一行掃描或連線節點情況下的潛在連線對被過濾器條件拒絕時出現。
與過濾器條件類似的情況發生在“有損”索引掃描中。例如,考慮以下搜尋包含特定點的多邊形
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.00 loops=1) Filter: (f1 @> '((0.5,2))'::polygon) Rows Removed by Filter: 7 Buffers: shared hit=1 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.00 loops=1) Index Cond: (f1 @> '((0.5,2))'::polygon) Rows Removed by Index Recheck: 1 Index Searches: 1 Buffers: shared hit=1 Planning Time: 0.039 ms Execution Time: 0.098 ms
在這裡,我們可以看到索引返回了一個候選行,然後該行被索引條件的重查拒絕。發生這種情況是因為 GiST 索引對於多邊形包含測試是“有損”的:它實際返回與目標重疊的多邊形所在的行,然後我們必須在這些行上執行精確的包含測試。
EXPLAIN
有一個 BUFFERS
選項,它提供了有關查詢的計劃和執行期間執行的 I/O 操作的額外詳細資訊。顯示的緩衝區編號顯示了給定節點及其所有子節點使用的非重複緩衝區(命中、讀取、髒、寫入)的計數。ANALYZE
選項會自動啟用 BUFFERS
選項。如果不需要,可以顯式停用 BUFFERS
EXPLAIN (ANALYZE, BUFFERS OFF) 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.00 loops=1) Recheck Cond: ((unique1 < 100) AND (unique2 > 9000)) Heap Blocks: exact=10 -> BitmapAnd (cost=25.07..25.07 rows=10 width=0) (actual time=0.100..0.101 rows=0.00 loops=1) -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100.00 loops=1) Index Cond: (unique1 < 100) Index Searches: 1 -> Bitmap Index Scan on tenk1_unique2 (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999.00 loops=1) Index Cond: (unique2 > 9000) Index Searches: 1 Planning Time: 0.162 ms Execution Time: 0.143 ms
請記住,由於 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.00 loops=1) -> Bitmap Heap Scan on tenk1 (cost=5.06..225.23 rows=100 width=10) (actual time=0.065..0.141 rows=100.00 loops=1) Recheck Cond: (unique1 < 100) Heap Blocks: exact=90 Buffers: shared hit=4 read=2 -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100.00 loops=1) Index Cond: (unique1 < 100) Index Searches: 1 Buffers: shared read=2 Planning Time: 0.151 ms Execution Time: 1.856 ms ROLLBACK;
如本例所示,當查詢是 INSERT
、UPDATE
、DELETE
或 MERGE
命令時,實際應用表更改的工作由頂層 Insert、Update、Delete 或 Merge 計劃節點完成。該節點以下的計劃節點執行定位舊行和/或計算新資料的操作。因此,上面我們看到了與之前相同的位圖表掃描,其輸出被饋送到一個 Update 節點,該節點儲存更新後的行。值得注意的是,雖然資料修改節點可能需要相當多的執行時間(在此處,它消耗了大部分時間),但規劃器目前不會在成本估計中新增任何內容來考慮這些工作。這是因為工作量對於每個正確的查詢計劃都是相同的,因此它不影響規劃決策。
當 UPDATE
、DELETE
或 MERGE
命令影響分割槽表或繼承層次結構時,輸出可能如下所示
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
觸發器在整個計劃完成後才觸發。每個觸發器(BEFORE
或 AFTER
)的總時間也單獨顯示。請注意,延遲的約束觸發器將在事務結束時執行,因此 EXPLAIN ANALYZE
完全不考慮它們。
頂層節點顯示的時間不包括將查詢的輸出資料轉換為可顯示格式或將其傳送到客戶端所需的任何時間。雖然 EXPLAIN ANALYZE
永遠不會將資料傳送到客戶端,但可以透過指定 SERIALIZE
選項來指示它將查詢的輸出資料轉換為可顯示格式並測量所需的時間。該時間將單獨顯示,並且也包含在總 Execution time
中。
透過 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.00 loops=1) Buffers: shared hit=16 -> Index Scan using tenk1_unique2 on tenk1 (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2.00 loops=1) Index Cond: (unique2 > 9000) Filter: (unique1 < 100) Rows Removed by Filter: 287 Index Searches: 1 Buffers: shared hit=16 Planning Time: 0.077 ms Execution Time: 0.086 ms
索引掃描節點的估計成本和行數顯示為假定已完全執行。但實際上,Limit 節點在獲得兩行後就停止請求行,因此實際行數僅為 2,執行時間也小於成本估計值。這不是估計錯誤,僅僅是估計和真實值顯示方式上的差異。
合併連線也有可能使不熟悉的人感到困惑的測量偽影。如果合併連線耗盡了另一個輸入,並且第一個輸入中的下一個鍵值大於第二個輸入的最後一個鍵值,那麼合併連線將停止讀取一個輸入;在這種情況下,不可能有更多匹配,因此無需掃描第一個輸入的其餘部分。這會導致未讀取所有子節點,結果與 LIMIT
中提到的類似。此外,如果外部(第一個)子節點包含重複鍵值的行,則內部(第二個)子節點將被備份並重新掃描其與該鍵值匹配的行部分。EXPLAIN ANALYZE
將這些內部行的重複發出計算為實際附加行。當存在許多外部重複項時,報告的內部子計劃節點的實際行數可能遠大於內部關係中實際存在的行數。
BitmapAnd 和 BitmapOr 節點由於實現限制,始終報告其實際行數為零。
通常,EXPLAIN
會顯示規劃器建立的每個計劃節點。但是,在某些情況下,執行器可以根據計劃時間不可用的引數值確定某些節點不需要執行,因為它們無法產生任何行。(目前這隻能發生在掃描分割槽表的 Append 或 MergeAppend 節點的子節點上。)發生這種情況時,這些計劃節點將從 EXPLAIN
輸出中省略,而是顯示 Subplans Removed:
註釋。N
如果您在文件中看到任何不正確、與您對特定功能的體驗不符或需要進一步澄清的內容,請使用此表格來報告文件問題。