Postgresql中的explain


PostgreSQL為每個收到的查詢設計一個查詢規划。選擇正確的匹配查詢結構和數據屬性的規划對執行效率是至關重要要的,所以系統包含一個復雜的規划器來試圖選擇好的規划。你可以使用EXPLAIN命令查看查詢規划器創建的任何查詢。閱讀查詢規划是一門藝術,需要掌握一定的經驗,本節試圖涵蓋一些基礎知識。
以下的例子來自PostgreSQL 9.5開發版。

EXPLAIN基礎

查詢規划是以規划為節點的樹形結構。樹的最底節點是掃描節點:他返回表中的原數據行。

不同的表有不同的掃描節點類型:順序掃描,索引掃描和位圖索引掃描。

也有非表列源,如VALUES子句並設置FROM返回,他們有自己的掃描類型。

如果查詢需要關聯,聚合,排序或其他操作,會在掃描節點之上增加節點執行這些操作。通常有不只一種可能的方式做這些操作,所以可能出現不同的節點類型。

EXPLAIN的輸出是每個樹節點顯示一行,內容是基本節點類型和執行節點的消耗評估。可能會出現其他行,從匯總行節點縮進顯示節點的其他屬性。第一行(最上節點的匯總行)是評估執行計划的總消耗,這個值越小越好。

下面是一個簡單的例子:

EXPLAIN SELECT * FROM tenk1;  
  
                         QUERY PLAN  
-------------------------------------------------------------  
 Seq Scan on tenk1  (cost=0.00..458.00 rows=10000 width=244)  

因為這個查詢沒有WHERE子句,所以必須掃描表中的所有行,所以規划器選擇使用簡單的順序掃描規划。括號中的數字從左到右依次是:

  • 評估開始消耗。這是可以開始輸出前的時間,比如排序節點的排序的時間。
  • 評估總消耗。假設查詢從執行到結束的時間。有時父節點可能停止這個過程,比如LIMIT子句。
  • 評估查詢節點的輸出行數,假設該節點執行結束。
  • 評估查詢節點的輸出行的平均字節數。

這個消耗的計算依賴於規划器的設置參數,這里的例子都是在默認參數下運行。
需要知道的是:上級節點的消耗包括其子節點的消耗。這個消耗值只反映規划器關心的內容,一般這個消耗不包括將數據傳輸到客戶端的時間。

評估的行數不是執行和掃描查詢節點的數量,而是節點返回的數量。它通常會少於掃描數量,因為有WHERE條件會過濾掉一些數據。理想情況頂級行數評估近似於實際返回的數量.

回到剛才的例子,表tenk1有10000條數據分布在358個磁盤頁,評估時間是(磁盤頁*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 SELECT * FROM tenk1 WHERE unique1 < 7000;  
  
                         QUERY PLAN  
------------------------------------------------------------  
 Seq Scan on tenk1  (cost=0.00..483.00 rows=7001 width=244)  
   Filter: (unique1 < 7000)  

查詢節點增加了“filter”條件。這意味着查詢節點為掃描的每一行數據增加條件檢查,只輸入符合條件數據。評估的輸出記錄數因為where子句變少了,但是掃描的數據還是10000條,所以消耗沒有減少,反而增加了一點cup的計算時間。

這個查詢實際輸出的記錄數是7000,但是評估是個近似值,多次運行可能略有差別,這中情況可以通過ANALYZE命令改善。

現在再修改一下條件:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100;  
  
                                  QUERY PLAN  
------------------------------------------------------------------------------  
 Bitmap Heap Scan on tenk1  (cost=5.07..229.20 rows=101 width=244)  
   Recheck Cond: (unique1 < 100)  
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)  
         Index Cond: (unique1 < 100)  

查詢規划器決定使用兩步規划:首先子查詢節點查看索引找到符合條件的記錄索引,然后外層查詢節點將這些記錄從表中提取出來。分別提取數據的成本要高於順序讀取,但因為不需要讀取所有磁盤頁,所以總消耗比較小。(其中Bitmap是系統排序的一種機制)

現在,增加另一個查詢條件:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND stringu1 = 'xxx';  
  
                                  QUERY PLAN  
------------------------------------------------------------------------------  
 Bitmap Heap Scan on tenk1  (cost=5.04..229.43 rows=1 width=244)  
   Recheck Cond: (unique1 < 100)  
   Filter: (stringu1 = 'xxx'::name)  
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)  
         Index Cond: (unique1 < 100)  

增加的條件stringu1='xxx'減少了輸出記錄數的評估,但沒有減少時間消耗,因為系統還是要查詢相同數量的記錄。請注意stringu1不是索引條件。

如果在不同的字段上有獨立的索引,規划器可能選擇使用AND或者OR組合索引:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000;  
  
                                     QUERY PLAN  
-------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tenk1  (cost=25.08..60.21 rows=10 width=244)  
   Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))  
   ->  BitmapAnd  (cost=25.08..25.08 rows=10 width=0)  
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 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)  

這個查詢條件的兩個字段都有索引,索引不需要filter。

下面我們來看看LIMIT的影響:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000 LIMIT 2;  
  
                                     QUERY PLAN  
-------------------------------------------------------------------------------------  
 Limit  (cost=0.29..14.48 rows=2 width=244)  
   ->  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..71.27 rows=10 width=244)  
         Index Cond: (unique2 > 9000)  
         Filter: (unique1 < 100)  

這條查詢的where條件和上面的一樣,只是增加了LIMIT,所以不是所有數據都需要返回,規划器改變了規划。在索引掃描節點總消耗和返回記錄數是運行玩查詢之后的數值,但Limit節點預期時間消耗是15,所以總時間消耗是15.增加LIMIT會使啟動時間小幅增加(0.25->0.29)。

來看一下通過索引字段的表連接:

EXPLAIN SELECT *  
FROM tenk1 t1, tenk2 t2  
WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;  
  
                                      QUERY PLAN  
--------------------------------------------------------------------------------------  
 Nested Loop  (cost=4.65..118.62 rows=10 width=488)  
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.47 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.91 rows=1 width=244)  
         Index Cond: (unique2 = t1.unique2)  

這個規划中有一個內連接的節點,它有兩個子節點。節點摘要行的縮進反映了規划樹的結構。最外層是一個連接節點,子節點是一個Bitmap掃描。外部節點位圖掃描的消耗和記錄數如同我們使用SELECT...WHERE unique1 < 10,因為這時t1.unique2 = t2.unique2還不相關。接下來為每一個從外部節點得到的記錄運行內部查詢節點。這里外部節點得到的數據的t1.unique2值是可用的,所以我們得到的計划和SELECT...WHEREt2.unique2=constant的情況類似。(考慮到緩存的因素評估的消耗可能要小一些)
外部節點的消耗加上循環內部節點的消耗(39.47+10*7.91)再加一點CPU時間就得到規划的總消耗。

再看一個例子:

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.46 rows=33 width=488)  
   Join Filter: (t1.hundred < t2.hundred)  
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.47 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索引中,所以這個條件出現在連接節點中。這將減少連接節點的評估輸出記錄數,但不會改變子節點的掃描數。
注意這次規划器選擇使用Meaterialize節點,將條件加入內部節點,這以為着內部節點的索引掃描只做一次,即使嵌套循環需要讀取這些數據10次,Meterialize節點將數據保存在內存中,每次循環都從內存中讀取數據。

如果我們稍微改變一下查詢,會看到完全不同的規划:

EXPLAIN SELECT *  
FROM tenk1 t1, tenk2 t2  
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;  
  
                                        QUERY PLAN  
------------------------------------------------------------------------------------------  
 Hash Join  (cost=230.47..713.98 rows=101 width=488)  
   Hash Cond: (t2.unique2 = t1.unique2)  
   ->  Seq Scan on tenk2 t2  (cost=0.00..445.00 rows=10000 width=244)  
   ->  Hash  (cost=229.20..229.20 rows=101 width=244)  
         ->  Bitmap Heap Scan on tenk1 t1  (cost=5.07..229.20 rows=101 width=244)  
               Recheck Cond: (unique1 < 100)  
               ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)  
                     Index Cond: (unique1 < 100)  

這里規划器選擇使用hash join,將一個表的數據存入內存中的哈希表,然后掃描另一個表並和哈希表中的每一條數據進行匹配。

注意縮進反應的規划結構。在tenk1表上的bitmap掃描結果作為Hash節點的輸入建立哈希表。然后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=198.11..268.19 rows=10 width=488)  
   Merge Cond: (t1.unique2 = t2.unique2)  
   ->  Index Scan using tenk1_unique2 on tenk1 t1  (cost=0.29..656.28 rows=101 width=244)  
         Filter: (unique1 < 100)  
   ->  Sort  (cost=197.83..200.33 rows=1000 width=244)  
         Sort Key: t2.unique2  
         ->  Seq Scan on onek t2  (cost=0.00..148.00 rows=1000 width=244)  

Merge Join需要已經排序的輸入數據。在這個規划中按正確順序索引掃描tenk1的數據,但是對onek表執行排序和順序掃描,因為需要在這個表中查詢多條數據。因為索引掃描需要訪問不連續的磁盤,所以索引掃描多條數據時會頻繁使用排序順序掃描(Sequential-scan-and-sort)。

有一種方法可以看到不同的規划,就是強制規划器忽略任何策略。例如,如果我們不相信排序順序掃描(sequential-scan-and-sort)是最好的辦法,我們可以嘗試這樣的做法:

SET enable_sort = off;  
  
EXPLAIN SELECT *  
FROM tenk1 t1, onek t2  
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;  
  
                                        QUERY PLAN  
------------------------------------------------------------------------------------------  
 Merge Join  (cost=0.56..292.65 rows=10 width=488)  
   Merge Cond: (t1.unique2 = t2.unique2)  
   ->  Index Scan using tenk1_unique2 on tenk1 t1  (cost=0.29..656.28 rows=101 width=244)  
         Filter: (unique1 < 100)  
   ->  Index Scan using onek_unique2 on onek t2  (cost=0.28..224.79 rows=1000 width=244)  

顯示測結果表明,規划器認為索引掃描比排序順序掃描消耗高12%。當然下一個問題就是規划器的評估為什么是正確的。我們可以通過EXPLAIN ANALYZE進行考察。

EXPLAIN ANALYZE

通過EXPLAIN ANALYZE可以檢查規划器評估的准確性。使用ANALYZE選項,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.62 rows=10 width=488) (actual time=0.128..0.377 rows=10 loops=1)  
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.47 rows=10 width=244) (actual time=0.057..0.121 rows=10 loops=1)  
         Recheck Cond: (unique1 < 10)  
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.024..0.024 rows=10 loops=1)  
               Index Cond: (unique1 < 10)  
   ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.91 rows=1 width=244) (actual time=0.021..0.022 rows=1 loops=10)  
         Index Cond: (unique2 = t1.unique2)  
 Total runtime: 0.501 ms  

注意,實際時間(actual time)的值是已毫秒為單位的實際時間,cost是評估的消耗,是個虛擬單位時間,所以他們看起來不匹配。

通常最重要的是看評估的記錄數是否和實際得到的記錄數接近。在這個例子里評估數完全和實際一樣,但這種情況很少出現。

某些查詢規划可能執行多次子規划。比如之前提過的內循環規划(nested-loop),內部索引掃描的次數是外部數據的數量。在這種情況下,報告顯示循環執行的總次數、平均實際執行時間和數據條數。這樣做是為了和評估值表示方式一至。由循環次數和平均值相乘得到總消耗時間。

某些情況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=717.34..717.59 rows=101 width=488) (actual time=7.761..7.774 rows=100 loops=1)  
   Sort Key: t1.fivethous  
   Sort Method: quicksort  Memory: 77kB  
   ->  Hash Join  (cost=230.47..713.98 rows=101 width=488) (actual time=0.711..7.427 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.007..2.583 rows=10000 loops=1)  
         ->  Hash  (cost=229.20..229.20 rows=101 width=244) (actual time=0.659..0.659 rows=100 loops=1)  
               Buckets: 1024  Batches: 1  Memory Usage: 28kB  
               ->  Bitmap Heap Scan on tenk1 t1  (cost=5.07..229.20 rows=101 width=244) (actual time=0.080..0.526 rows=100 loops=1)  
                     Recheck Cond: (unique1 < 100)  
                     ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0) (actual time=0.049..0.049 rows=100 loops=1)  
                           Index Cond: (unique1 < 100)  
 Total runtime: 8.008 ms  

排序節點(Sort)顯示排序類型(一般是在內存還是在磁盤)和使用多少內存。哈希節點(Hash)顯示哈希桶和批數以及使用內存的峰值。

另一種額外信息是過濾條件過濾掉的記錄數:

EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE ten < 7;  
  
                                               QUERY PLAN  
---------------------------------------------------------------------------------------------------------  
 Seq Scan on tenk1  (cost=0.00..483.00 rows=7000 width=244) (actual time=0.016..5.107 rows=7000 loops=1)  
   Filter: (ten < 7)  
   Rows Removed by Filter: 3000  
 Total runtime: 5.905 ms  

這個值在join節點上尤其有價值。"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.05 rows=1 width=32) (actual time=0.044..0.044 rows=0 loops=1)  
   Filter: (f1 @> '((0.5,2))'::polygon)  
   Rows Removed by Filter: 4  
 Total runtime: 0.083 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=32) (actual time=0.062..0.062 rows=0 loops=1)  
   Index Cond: (f1 @> '((0.5,2))'::polygon)  
   Rows Removed by Index Recheck: 1  
 Total runtime: 0.144 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.08..60.21 rows=10 width=244) (actual time=0.323..0.342 rows=10 loops=1)  
   Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))  
   Buffers: shared hit=15  
   ->  BitmapAnd  (cost=25.08..25.08 rows=10 width=0) (actual time=0.309..0.309 rows=0 loops=1)  
         Buffers: shared hit=7  
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0) (actual time=0.043..0.043 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.227..0.227 rows=999 loops=1)  
               Index Cond: (unique2 > 9000)  
               Buffers: shared hit=5  
 Total runtime: 0.423 ms   

Buffers提供的數據可以幫助確定哪些查詢是I/O密集型的。

請注意EXPLAIN ANALYZE實際運行查詢,任何實際影響都會發生。如果要分析一個修改數據的查詢又不想改變你的表,你可以使用roll back命令進行回滾,比如:

BEGIN;  
  
EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 < 100;  
  
                                                           QUERY PLAN  
--------------------------------------------------------------------------------------------------------------------------------  
 Update on tenk1  (cost=5.07..229.46 rows=101 width=250) (actual time=14.628..14.628 rows=0 loops=1)  
   ->  Bitmap Heap Scan on tenk1  (cost=5.07..229.46 rows=101 width=250) (actual time=0.101..0.439 rows=100 loops=1)  
         Recheck Cond: (unique1 < 100)  
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0) (actual time=0.043..0.043 rows=100 loops=1)  
               Index Cond: (unique1 < 100)  
 Total runtime: 14.727 ms  
  
ROLLBACK;  

當查詢是INSERT,UPDATE或DELETE命令時,在頂級節點是實施對表的變更。在這下面的節點實行定位舊數據計算新數據的工作。所以我們看到一樣的bitmap索引掃描,並返回給Update節點。值得注意的是雖然修改數據的節點可能需要相當長的運行時間(在這里它消耗了大部分的時間),規划器卻沒有再評估時間中添加任何消耗,這是因為更新工作對於任何查詢規划都是一樣的,所以並不影響規划器的決策。

EXPLAIN ANALYZE的"Total runtime"包括執行啟動和關閉時間,以及運行被激發的任何處觸發器的時間,但不包括分析、重寫或規划時間。執行時間包括BEFORE觸發器,但不包括AFTER觸發器,因為AFTER是在查詢運行結束之后才觸發的。每個觸發器(無論BEFORE還是AFTER)的時間也會單獨顯示出來。注意,延遲的觸發器在事務結束前都不會被執行,所以EXPLAIN ANALYZE不會顯示。

本文內容主要來自:http://www.postgres.cn/docs/9.5/using-explain.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM