在這裏插入代碼片
執行計劃
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條。