http://toplchx.iteye.com/blog/2091860
使用EXPLAIN
PostgreSQL爲每一個收到的查詢設計一個查詢規劃。選擇正確的匹配查詢結構和數據屬性的規劃對執行效率是相當重要要的,因此係統包含一個複雜的規劃器來試圖選擇好的規劃。你可使用EXPLAIN命令查看查詢規劃器建立的任何查詢。閱讀查詢規劃是一門藝術,須要掌握必定的經驗,本節試圖涵蓋一些基礎知識。
如下的例子來自PostgreSQL 9.3開發版。
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)
這個查詢條件的兩個字段都有索引,索引不須要filre。
下面咱們來看看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不會顯示。