PostgreSQL執行計劃

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

在這裏插入代碼片

執行計劃

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條。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章