PostgreSQL 執行計划


簡介

PostgreSQL是“世界上最先進的開源關系型數據庫”。因為出現較晚,所以客戶人群基數較MySQL少,但是發展勢頭很猛,最大優勢是完全開源。

MySQL是“世界上最流行的開源關系型數據庫”。當前客戶基數大,隨着被Oracle收購,開源程度減小,尤其是近期單獨拉了免費的MariaDB分支,更表明MySQL有閉源的傾向;

至於兩者孰優孰劣,不是本文要討論的重點,在一般的使用中,沒什么大的差別,下面我們只討論PG中執行計划。

執行計划

pg在查詢規划路徑過程中,查詢請求的不同執行方案是通過建立不同的路徑來表達的,在生成較多符合條件的路徑之后,要從中選擇出代價最小的路徑,把它轉化為一個執行計划,傳遞給執行器執行。那么如何生成最小代價的計划呢?基於統計信息估計計划中各個節點的成本,其中與之相關的參數如下所示:

 

計算代價:

# 估算代價:

total_cost = seq_page_cost * relpages + cpu_tuple_cost * reltuples


# 有時我們不想用系統默認的執行計划,這時可以通過禁止/開啟某種運算的語法來強制控制執行計划:

enable_bitmapscan = on
enable_hashagg = on
enable_hashjoin = on
enable_indexscan = on #索引掃描
enable_indexonlyscan = on #只讀索引掃描
enable_material = on #物化視圖
enable_mergejoin = on
enable_nestloop = on
enable_seqscan = on
enable_sort = on
enable_tidscan = on


# 按照上面掃描方式並過濾代價:

Cost = seq_page_cost * relpages + cpu_tuple_cost * reltuples + cpu_operation_cost * reltuples

 

每個SQL語句都會有自己的執行計划,我們可以使用explain指令獲取執行計划,語法如下:

nsc=# \h explain;
Command: EXPLAIN
Description: show the execution plan of a statement
Syntax:
EXPLAIN [ ( option [, ...] ) ] statement
EXPLAIN [ ANALYZE ] [ VERBOSE ] statement

where option can be one of:

ANALYZE [ boolean ] -- 是否真正執行,默認false
VERBOSE [ boolean ] -- 是否顯示詳細信息,默認false
COSTS [ boolean ] -- 是否顯示代價信息,默認true
BUFFERS [ boolean ] -- 是否顯示緩存信息,默認false,前置事件是analyze
TIMING [ boolean ] -- 是否顯示時間信息
FORMAT { TEXT | XML | JSON | YAML } -- 輸格式,默認為text


如下圖所示,cost是比較重要的指標,cost=1000.00..1205.30,執行sql代價,分為兩個部分,前一部分表示啟動時間(startup)是1000ms,執行到返回第一行時需要的cost值,后一部分表示總時間(total)是1205.30ms,執行整個SQL的cost。rows表示預測的行數,與實際的記錄數可能有出入,數據庫經常vacuum或analyze,該值越接近實際值。width表示查詢結果的所有字段的總寬度為285個字節。

可以在explain后添加analyze關鍵字來通過執行這個SQL獲得真實的執行計划和執行時間,actual time中的第一個數字表示返回第一行需要的時間,第二個數字表示執行整個sql花費的時間。loops為該節點循環次數,當loops大於1時,總成本為:actual time * loops

 

 

執行計划節點類型

在PostgreSQL的執行計划中,是自上而下閱讀的,通常執行計划會有相關的索引來表示不同的計划節點,其中計划節點類型分為四類:控制節點(Control Node),掃描節點(Scan Node),物化節點(Materialization Node),連接節點(Join Node)。

控制節點:append,組織多個字表或子查詢的執行節點,主要用於union操作。

掃描節點:用於掃描表等對象以獲取元組

   Seq Scan(全表掃描):把表的所有數據塊從頭到尾讀一遍,篩選出符合條件的數據塊;

   Index Scan(索引掃描):為了加快查詢速度,在索引中找到需要的數據行的物理位置,再到表數據塊中把對應數據讀出來,如B樹,GiST,GIN,BRIN,HASH

   Bitmap Index/Heap Scan(位圖索引/結果掃描):把滿足條件的行或塊在內存中建一個位圖,掃描完索引后,再根據位圖列表的數據文件把對應的數據讀出來,先通過Bitmap Index Scan在索引中找到符合條件的行,在內存中建立位圖,之后再到表中掃描Bitmap Heap Scan。

物化節點:能夠緩存執行結果到緩存中,即第一次被執行時生成的結果元組緩存,等待上層節點使用,例如,sort節點能夠獲取下層節點返回的所有元組並根據指定的屬性排序,並將排序結果緩存,每次上層節點取元組時就從緩存中按需讀取。

   Materialize:對下層節點返回的元組進行緩存(如連接表時)

   Sort:對下層返回的節點進行排序(如果內存超過iwork_mem參數指定大小,則節點工作空間切換到臨時文件,性能急劇下降)

   Group:對下層排序元組進行分組操作

   Agg:執行聚集函數(sum/max/min/avg)

條件過濾,一般在where后加上過濾條件,當掃描數據行時,會找出滿足過濾條件的行,條件過濾在執行計划里面顯示Filter,如果條件的列上面有索引,可能會走索引,不會走過濾。

連接節點:對應於關系代數中的連接操作,可以實現多種連接方式(條件連接/左連接/右連接/全連接/自然連接)

   Nestedloop Join(嵌套連接): 內表被外表驅動,外表返回的每一行都要在內表中檢索找到與它匹配的行,因此整個查詢返回的結果集不能太大,要把返回子集較小的表作為外表,且內表的連接字段上要有索引。 執行過程為,確定一個驅動表(outer table),另一個表為inner table,驅動表中每一行與inner table中的相應記錄關聯;

   Hash Join(哈希連接):優化器使用兩個比較的表,並利用連接屬性在內存中建立散列表,然后掃描較大的表並探測散列表,找出與散列表匹配的行;

   Merge Join(合並連接):通常hash連接的性能要比merge連接好,但如果源數據上有索引,或結果已經被排過序,這時merge連接性能會優於hash連接;

 

運算類型(explain)

運算類型  操作說明  是否有啟動時間
Seq Scan  順序掃描表  無啟動時間
Index Scan  索引掃描  無啟動時間
Bitmap Index Scan  索引掃描  有啟動時間
Bitmap Heap Scan  索引掃描  有啟動時間
Subquery Scan  子查詢  無啟動時間
Tid Scan  行號檢索  無啟動時間
Function Scan  函數掃描  無啟動時間
Nested Loop Join  嵌套連接  無啟動時間
Merge Join  合並連接  有啟動時間
Hash Join  哈希連接  有啟動時間
Sort  排序(order by)  有啟動時間
Hash  哈希運算  有啟動時間
Result  函數掃描,和具體的表無關  無啟動時間
Unique  distinct/union  有啟動時間
Limit  limit/offset  有啟動時間
Aggregate  count, sum,avg等聚集函數  有啟動時間
Group  group by  有啟動時間
Append  union操作  無啟動時間
Materialize  子查詢  有啟動時間
SetOp  intersect/except  有啟動時間

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

示例講解

慢sql如下:

SELECT
te.event_type,
sum(tett.feat_bytes) AS traffic
FROM t_event te
LEFT JOIN t_event_traffic_total tett
ON tett.event_id = te.event_id
WHERE
((te.event_type >= 1 AND te.event_type <= 17) OR (te.event_type >= 23 AND te.event_type <= 26) OR (te.event_type >= 129 AND te.event_type <= 256))
AND te.end_time >= '2017-10-01 09:39:41+08:00'
AND te.begin_time <= '2018-01-01 09:39:41+08:00'
AND tett.stat_time >= '2017-10-01 09:39:41+08:00'
AND tett.stat_time < '2018-01-01 09:39:41+08:00'
GROUP BY te.event_type
ORDER BY total_count DESC
LIMIT 10


耗時:約4s

作用:事件表和事件流量表關聯,查出一段時間內按照總流量大小排列的TOP10事件類型

記錄數:

select count(1) from t_event; -- 535881條
select count(1) from t_event_traffic_total; -- 2123235條


結果:

event_type traffic
17	2.26441505638877E17
2	2.25307250128674E17
7	1.20629298837E15
26	285103860959500
1	169208970599500
13	47640495350000
6	15576058500000
3	12671721671000
15	1351423772000
11	699609230000 


執行計划:

Limit  (cost=5723930.01..5723930.04 rows=10 width=12) (actual time=3762.383..3762.384 rows=10 loops=1)
  Output: te.event_type, (sum(tett.feat_bytes))
  Buffers: shared hit=1899 read=16463, temp read=21553 written=21553
  ->  Sort  (cost=5723930.01..5723930.51 rows=200 width=12) (actual time=3762.382..3762.382 rows=10 loops=1)
        Output: te.event_type, (sum(tett.feat_bytes))
        Sort Key: (sum(tett.feat_bytes))
        Sort Method: quicksort  Memory: 25kB
        Buffers: shared hit=1899 read=16463, temp read=21553 written=21553
        ->  HashAggregate  (cost=5723923.69..5723925.69 rows=200 width=12) (actual time=3762.360..3762.363 rows=18 loops=1)
              Output: te.event_type, sum(tett.feat_bytes)
              Buffers: shared hit=1899 read=16463, temp read=21553 written=21553
              ->  Merge Join  (cost=384982.63..4390546.88 rows=266675361 width=12) (actual time=2310.395..3119.886 rows=2031023 loops=1)
                    Output: te.event_type, tett.feat_bytes
                    Merge Cond: (te.event_id = tett.event_id)
                    Buffers: shared hit=1899 read=16463, temp read=21553 written=21553
                    ->  Sort  (cost=3284.60..3347.40 rows=25119 width=12) (actual time=21.509..27.978 rows=26225 loops=1)
                          Output: te.event_type, te.event_id
                          Sort Key: te.event_id
                          Sort Method: external merge  Disk: 664kB
                          Buffers: shared hit=652, temp read=84 written=84
                          ->  Append  (cost=0.00..1448.84 rows=25119 width=12) (actual time=0.027..7.975 rows=26225 loops=1)
                                Buffers: shared hit=652
                                ->  Seq Scan on public.t_event te  (cost=0.00..0.00 rows=1 width=12) (actual time=0.001..0.001 rows=0 loops=1)
                                      Output: te.event_type, te.event_id
                                      Filter: ((te.end_time >= '2017-10-01 09:39:41+08'::timestamp with time zone) AND (te.begin_time <= '2018-01-01 09:39:41+08'::timestamp with time zone) AND (((te.event_type >= 1) AND (te.event_type <= 17)) OR ((te.event_type >= 23) AND (te.event_type <= 26)) OR ((te.event_type >= 129) AND (te.event_type <= 256))))
                                ->  掃描子表過程,省略...
                    ->  Materialize  (cost=381698.04..392314.52 rows=2123296 width=16) (actual time=2288.881..2858.256 rows=2123235 loops=1)
                          Output: tett.feat_bytes, tett.event_id
                          Buffers: shared hit=1247 read=16463, temp read=21469 written=21469
                          ->  Sort  (cost=381698.04..387006.28 rows=2123296 width=16) (actual time=2288.877..2720.994 rows=2123235 loops=1)
                                Output: tett.feat_bytes, tett.event_id
                                Sort Key: tett.event_id
                                Sort Method: external merge  Disk: 53952kB
                                Buffers: shared hit=1247 read=16463, temp read=21469 written=21469
                                ->  Append  (cost=0.00..49698.20 rows=2123296 width=16) (actual time=0.026..470.610 rows=2123235 loops=1)
                                      Buffers: shared hit=1247 read=16463
                                      ->  Seq Scan on public.t_event_traffic_total tett  (cost=0.00..0.00 rows=1 width=16) (actual time=0.001..0.001 rows=0 loops=1)
                                            Output: tett.feat_bytes, tett.event_id
                                            Filter: ((tett.stat_time >= '2017-10-01 09:39:41+08'::timestamp with time zone) AND (tett.stat_time < '2018-01-01 09:39:41+08'::timestamp with time zone))
                                      ->  掃描子表過程,省略...
Total runtime: 3771.346 ms

 


執行計划解讀:

第40->30行:通過結束時間上創建的索引,順序掃描t_event_traffic_total表,根據時間跨度三個月過濾出符合條件的數據,共2123235條記錄;

第26->21行:根據時間過濾出t_event表中符合條件的記錄,共26225條記錄;

第30->27行:根據流量大小排序,執行sort操作;

第12->09行:兩個表執行join操作,執行完記錄200條;

第08->04行:對最終的200條記錄按照大小排序;

第01行:執行limit取10條記錄。

整個執行計划中花時間最長的是根據時間條件過濾t_event_traffic_total表,因為字表較多,記錄較多,導致花費2.8s之多,所以我們優化的思路就比較簡單了,直接根據actual time,花費較多的子表去查看表中是否有索引,以及記錄是不是很多,有沒有優化的空間,而經過排查,發現一個子表中的數據量達到1531147條。

pg_hint_plan定制執行計划


原文鏈接:https://blog.csdn.net/JAVA528416037/article/details/91998019


免責聲明!

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



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