簡介
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
