當SQL查詢性能不佳或查詢結果不符合預期時,Hologres提供了EXPLAIN
和EXPLAIN ANALYZE
命令用于分析查詢語句執行計劃,可以幫助您了解Hologres如何執行查詢語句,以便您對查詢語句或數據庫結構進行調整優化。本文介紹在Hologres中通過EXPLAIN
和EXPLAIN ANALYZE
查看執行計劃及每個算子的含義。
執行計劃簡介
在Hologres中,優化器(Query Optimizer,QO)會為每一條SQL生成一個執行計劃,執行引擎(Query Engine,QE)會根據該執行計劃生成最終的執行計劃,然后執行并獲取SQL結果。執行計劃中會反映出SQL的統計信息、執行算子、算子耗時等信息,一個好的執行計劃,能夠用最少的資源更快的返回結果。因此對于日常開發來說,執行計劃至關重要,它可以反應出SQL的問題,從而去針對性優化。
Hologres兼容Postgres,可以根據EXPLAIN
和EXPLAIN ANALYZE
語法了解SQL的執行計劃。
EXPLAIN:代表優化器QO根據SQL特征預估的SQL執行計劃,并非實際的執行計劃,對SQL的運行有一定參考意義。
EXPLAIN ANALYZE:代表SQL真實的運行計劃,相比EXPLAIN會包含更多的實際運行信息,能準確的反映出SQL的執行算子和算子耗時,可以根據算子耗時去做針對性的SQL優化。
從Hologres V1.3.4x版本開始,支持通過EXPLAIN和EXPLAIN ANALYZE查看更加清晰且閱讀性更高的執行計劃,本文檔基于V1.3.4x版本撰寫,建議將實例升級至V1.3.4x及以上版本。
EXPLAIN
語法格式
EXPLAIN
可以反映出優化器的預估執行計劃,語法如下:EXPLAIN <sql>;
使用示例
以TPC-H中的SQL為例。
說明該示例引用TPC-H的SQL,但是不代表TPC-H的測試結果。
EXPLAIN SELECT l_returnflag, l_linestatus, sum(l_quantity) AS sum_qty, sum(l_extendedprice) AS sum_base_price, sum(l_extendedprice * (1 - l_discount)) AS sum_disc_price, sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) AS sum_charge, avg(l_quantity) AS avg_qty, avg(l_extendedprice) AS avg_price, avg(l_discount) AS avg_disc, count(*) AS count_order FROM lineitem WHERE l_shipdate <= date '1998-12-01' - interval '120' day GROUP BY l_returnflag, l_linestatus ORDER BY l_returnflag, l_linestatus;
返回結果
QUERY PLAN Sort (cost=0.00..7795.30 rows=3 width=80) Sort Key: l_returnflag, l_linestatus -> Gather (cost=0.00..7795.27 rows=3 width=80) -> Project (cost=0.00..7795.27 rows=3 width=80) -> Project (cost=0.00..7794.27 rows=3 width=104) -> Final HashAggregate (cost=0.00..7793.27 rows=3 width=76) Group Key: l_returnflag, l_linestatus -> Redistribution (cost=0.00..7792.95 rows=1881 width=76) Hash Key: l_returnflag, l_linestatus -> Partial HashAggregate (cost=0.00..7792.89 rows=1881 width=76) Group Key: l_returnflag, l_linestatus -> Local Gather (cost=0.00..7791.81 rows=44412 width=76) -> Decode (cost=0.00..7791.80 rows=44412 width=76) -> Partial HashAggregate (cost=0.00..7791.70 rows=44412 width=76) Group Key: l_returnflag, l_linestatus -> Project (cost=0.00..3550.73 rows=584421302 width=33) -> Project (cost=0.00..2585.43 rows=584421302 width=33) -> Index Scan using Clustering_index on lineitem (cost=0.00..261.36 rows=584421302 width=25) Segment Filter: (l_shipdate <= '1998-08-03 00:00:00+08'::timestamp with time zone) Cluster Filter: (l_shipdate <= '1998-08-03 00:00:00+08'::timestamp with time zone)
結果解釋
執行計劃需要從下往上看,每個箭頭(->)代表一個節點,每個子節點會返回使用的算子,以及預估的行數等。需要關注的算子包括:
參數
描述
cost
算子的預估耗時,父節點的cost也包含子節點的cost。包含預估啟動cost和預估總cost,中間使用
..
分割。預估啟動cost:輸出階段開始之前的cost。
預估總cost:是假設算子運行完成的總cost。
例如上述返回結果中的Final HashAggregate節點,預估啟動cost是
0.00
,總cost是7793.27
。rows
算子的預估輸出行數,主要基于統計信息進行估算。
對于上述返回結果中Scan的預估值,默認值為
1000
。說明通常情況下如果出現
rows=1000
則說明表的統計信息不正確,沒有根據統計信息進行估算,可以通過執行analyze <tablename>
命令對表進行統計信息更新。width
算子的預估輸出平均寬度(單位:字節),值越大說明列越寬。
EXPLAIN ANALYZE
語法格式
EXPLAIN ANALYZE
可以反應出SQL的實際執行計劃,以及對應的算子耗時,從而幫助診斷SQL性能。語法如下:EXPLAIN ANALYZE <sql>;
使用示例
以TPC-H中的SQL為例。
EXPLAIN ANALYZE SELECT l_returnflag, l_linestatus, sum(l_quantity) AS sum_qty, sum(l_extendedprice) AS sum_base_price, sum(l_extendedprice * (1 - l_discount)) AS sum_disc_price, sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) AS sum_charge, avg(l_quantity) AS avg_qty, avg(l_extendedprice) AS avg_price, avg(l_discount) AS avg_disc, count(*) AS count_order FROM lineitem WHERE l_shipdate <= date '1998-12-01' - interval '120' day GROUP BY l_returnflag, l_linestatus ORDER BY l_returnflag, l_linestatus;
返回結果
QUERY PLAN Sort (cost=0.00..7795.30 rows=3 width=80) Sort Key: l_returnflag, l_linestatus [id=21 dop=1 time=2427/2427/2427ms rows=4(4/4/4) mem=3/3/3KB open=2427/2427/2427ms get_next=0/0/0ms] -> Gather (cost=0.00..7795.27 rows=3 width=80) [20:1 id=100003 dop=1 time=2426/2426/2426ms rows=4(4/4/4) mem=1/1/1KB open=0/0/0ms get_next=2426/2426/2426ms] -> Project (cost=0.00..7795.27 rows=3 width=80) [id=19 dop=20 time=2427/2426/2425ms rows=4(1/0/0) mem=87/87/87KB open=2427/2425/2425ms get_next=1/0/0ms] -> Project (cost=0.00..7794.27 rows=0 width=104) -> Final HashAggregate (cost=0.00..7793.27 rows=3 width=76) Group Key: l_returnflag, l_linestatus [id=16 dop=20 time=2427/2425/2424ms rows=4(1/0/0) mem=574/570/569KB open=2427/2425/2424ms get_next=1/0/0ms] -> Redistribution (cost=0.00..7792.95 rows=1881 width=76) Hash Key: l_returnflag, l_linestatus [20:20 id=100002 dop=20 time=2427/2424/2423ms rows=80(20/4/0) mem=3528/1172/584B open=1/0/0ms get_next=2426/2424/2423ms] -> Partial HashAggregate (cost=0.00..7792.89 rows=1881 width=76) Group Key: l_returnflag, l_linestatus [id=12 dop=20 time=2428/2357/2256ms rows=80(4/4/4) mem=574/574/574KB open=2428/2357/2256ms get_next=1/0/0ms] -> Local Gather (cost=0.00..7791.81 rows=44412 width=76) [id=11 dop=20 time=2427/2356/2255ms rows=936(52/46/44) mem=7/6/6KB open=0/0/0ms get_next=2427/2356/2255ms pull_dop=9/9/9] -> Decode (cost=0.00..7791.80 rows=44412 width=76) [id=8 dop=234 time=2435/1484/5ms rows=936(4/4/4) mem=0/0/0B open=2435/1484/5ms get_next=4/0/0ms] -> Partial HashAggregate (cost=0.00..7791.70 rows=44412 width=76) Group Key: l_returnflag, l_linestatus [id=5 dop=234 time=2435/1484/3ms rows=936(4/4/4) mem=313/312/168KB open=2435/1484/3ms get_next=0/0/0ms] -> Project (cost=0.00..3550.73 rows=584421302 width=33) [id=4 dop=234 time=2145/1281/2ms rows=585075720(4222846/2500323/3500) mem=142/141/69KB open=10/1/0ms get_next=2145/1280/2ms] -> Project (cost=0.00..2585.43 rows=584421302 width=33) [id=3 dop=234 time=582/322/2ms rows=585075720(4222846/2500323/3500) mem=142/142/69KB open=10/1/0ms get_next=582/320/2ms] -> Index Scan using Clustering_index on lineitem (cost=0.00..261.36 rows=584421302 width=25) Segment Filter: (l_shipdate <= '1998-08-03 00:00:00+08'::timestamp with time zone) Cluster Filter: (l_shipdate <= '1998-08-03 00:00:00+08'::timestamp with time zone) [id=2 dop=234 time=259/125/1ms rows=585075720(4222846/2500323/3500) mem=1418/886/81KB open=10/1/0ms get_next=253/124/0ms] ADVICE: [node id : 1000xxx] distribution key miss match! table lineitem defined distribution keys : l_orderkey; request distribution columns : l_returnflag, l_linestatus; shuffle data skew in different shards! max rows is 20, min rows is 0 Query id:[300200511xxxx] ======================cost====================== Total cost:[2505] ms Optimizer cost:[47] ms Init gangs cost:[4] ms Build gang desc table cost:[2] ms Start query cost:[18] ms - Wait schema cost:[0] ms - Lock query cost:[0] ms - Create dataset reader cost:[0] ms - Create split reader cost:[0] ms Get the first block cost:[2434] ms Get result cost:[2434] ms ====================resource==================== Memory: 921(244/230/217) MB, straggler worker id: 72969760xxx CPU time: 149772(38159/37443/36736) ms, straggler worker id: 72969760xxx Physical read bytes: 3345(839/836/834) MB, straggler worker id: 72969760xxx Read bytes: 41787(10451/10446/10444) MB, straggler worker id: 72969760xxx DAG instance count: 41(11/10/10), straggler worker id: 72969760xxx Fragment instance count: 275(70/68/67), straggler worker id: 72969760xxx
結果解釋
EXPLAIN ANALYZE
的執行結果反映的是真實執行路徑,其結果是一個由多個算子組成的樹狀結構,會反映出每個階段每個算子的詳細執行信息。EXPLAIN ANALYZE
的結果主要包括Query Plan、Advice、Cost耗時、Resource資源消耗情況。
QUERY PLAN
在QUERY PLAN中,會反映每個算子的詳細執行信息。同EXPLAIN
一樣,EXPLAIN ANALYZE
中的Query Plan需要從下往上看,每個箭頭(->)代表一個節點。
示例 | 說明 |
(cost=0.00..2585.43 rows=584421302 width=33) | 都代表優化器的預估值,非真實值,同Explain含義一致。
|
[20:20 id=100002 dop=20 time=2427/2424/2423ms rows=80(20/4/0) mem=3528/1172/584B open=1/0/0ms get_next=2426/2424/2423ms] | 代表實際執行的消耗值,即真實數據。
|
由于一條SQL語句可能涉及到多個算子,下面將會有單獨章節對算子進行詳細介紹,請參見算子含義。
對于time、row、mem這類數值:
當前算子的time值是下方算子耗時的累加,所以當前算子的耗時需要用當前算子的time值減去下方算子的耗時。
對于row、mem則是每個算子的獨立計算,不是累加值。
ADVICE
ADVICE中主要系統根據當前EXPLAIN ANALYZE
的執行結果,自動生成的調優建議如下:
建議為表設置distribution key,clustering key,bitmap等,如
Table xxx misses bitmap index
。表缺少統計信息:
Table xxx Miss Stats! please run 'analyze xxx';
。數據可能存在傾斜:
shuffle data xxx in different shards! max rows is 20, min rows is 0
。
Advice的結果只是根據當前SQL的Explain Analyze結果給出的建議,不一定完全適用,需要根據業務具體分析,并合理采取相應的操作。
Cost耗時
Cost代表Query的總耗時以及在每個階段運行的詳細耗時,可以通過每個階段的耗時初步判斷性能瓶頸。
Total cost:代表Query執行的總耗時,單位毫秒(ms),其中:
Optimizer cost:代表優化器(QO)生成執行計劃的耗時,單位毫秒(ms)。
Build gang desc table cost:將查詢優化器生成的執行計劃,轉換為執行引擎所需的數據結構所花費的時間,單位毫秒(ms)。
Init gangs cost:將QO生成的執行計劃進一步預處理,并將請求發送給執行引擎,從而開始Start Query階段的耗時,單位毫秒(ms)。
Start query cost:從Init gangs步驟執行完成時開始計算,真正開始執行Query前的初始化階段,包括了加鎖,對齊Schema版本等過程,主要有以下幾個節點:
Wait schema cost:存儲引擎(Storage Engine,SE)和Frontend(FE)對齊Schema版本所耗時間。當表的Schema發生變化時,FE會更新節點版本,同理SE也會更新節點版本,如果FE和SE的版本不對齊,則會出現Schema延遲。如果延遲較高,一般原因是因為SE處理慢,尤其是分區父表的DDL較多時,延遲較高,導致數據寫入/查詢慢,建議針對性優化DDL頻率。
Lock query cost:Query加鎖所耗時間,如果耗時較高,則說明Query在等鎖。
Create dataset reader cost:創建索引數據讀取器的耗時,耗時較高有可能是未命中Cache。
Create split reader cost:打開文件消耗的時間,耗時高則說明文件meta沒有命中緩存,IO開銷較高。
Get result cost:從Start Query階段結束開始計算,到所有結果均返回所花費的時間。Get result cost包含Get the first block cost,單位毫秒(ms)。
Get the first block cost:從Start Query階段結束開始計算,到返回第一批數據(即第一個record batch)為止的時間。某些場景下該指標會與比Get result cost指標非常接近或者一致。例如當查詢計劃的頂部是Hash Agg算子,建立Hash Table做聚合運算需要依賴下游算子的全量數據。而對于普通帶過濾條件的查詢,數據流式進行計算和返回,此場景下,該指標通常會與Get result cost數值有較大的差距(取決于數據量)。
Resource資源消耗情況
Resource代表Query運行的資源消耗,格式為:total(max/avg/min)
,包含總的資源累加和消耗資源的最大值、平均值和最小值。
因為Hologres是分布式引擎,一個實例有多個Worker節點,每個Worker節點計算完成會進行結果合并,并最終返回給客戶端。因此在資源消耗中,會使用total(max worker/avg worker/min worker)
的格式來反應整體消耗,其中:
total:代表Query的總消耗。
max:代表單個Worker節點消耗的最大值。
avg:代表平均Worker節點的消耗(avg=total/worker節點數)。
min:代表單個Worker節點消耗的最小值。
以下為各資源消耗指標的詳細解釋:
指標 | 描述 |
Memory | Query運行過程中所消耗的內存,包括消耗總內存、以及Worker節點消耗的最大內存、平均內存和最小內存。 |
CPU time | Query運行時消耗的總CPU時間(單位毫秒,非精確值)。 體現所有計算任務所耗費的CPU時間,是多個CPU Core計算時間的累加,粗略地體現復雜度。 |
Physical read bytes | 從磁盤上讀取的數據量,單位Bytes,當Query沒有命中緩存Cache時,則需要去磁盤讀取數據。 |
Read bytes | Query讀取的總字節大小,單位Bytes,包括物理讀(Physical Read)和從內存Cache中讀取的數據大小。反映Query執行獲取的數據量的大小。 |
Affected rows | DML影響的行數,只有DML才會展示該指標。 |
Dag instance count | 查詢計劃DAG instance數量,數值越大表示查詢越復雜,并行度越高。 |
Fragment instance count | 查詢計劃Fragment instance數量,數值越大表示計劃越多,文件數越多。 |
straggler_worker_id | 表示該資源指標消耗最大的Worker節點的id。 |
算子含義
SCAN
seq scan
Seq Scan表示順序地從表中讀取數據,會進行全表掃描。
on
后對應的是所掃描的表名。示例:對一張普通內表查詢,在執行計劃中會有Seq scan。
EXPLAIN SELECT * FROM public.holo_lineitem_100g;
返回結果:
查詢分區表
如果是分區表,執行計劃中則是
Seq Scan on Partitioned Table
,且會通過Partitions selected: x out of y來展示SQL中掃描了多少分區。示例:查詢一個分區父表,且只掃描了一個分區。
EXPLAIN SELECT * FROM public.hologres_parent;
返回結果:
查詢外部表
如果查詢的是外部表,執行計劃中則會有
Foreign Table Type
區分外部表的來源。type分類包括:MaxCompute、OSS、Hologres。示例:查詢MaxCompute外部表。
EXPLAIN SELECT * FROM public.odps_lineitem_100;
返回結果:
Index Scan和 Index Seek
如果掃描表有命中索引,根據表的存儲格式(行存或列存)不同,Hologres在底層使用的索引也不同,根據索引掃描時主要有Clustering_index和Index Seek(又名pk_index)兩種索引。其中:
Clustering_index:表示使用了列存表的索引(例如segment 、clustering等),只要查詢命中索引就會使用Clustering_index。
Seq Scan Using Clustering_index
通常會跟Filter一起出現,Filter是子節點,會列出命中的索引,包括clustering filter、segment filter、bitmap filter等,詳情請參見列存表原理。示例1:查詢命中索引。
BEGIN; CREATE TABLE column_test ( "id" bigint not null , "name" text not null , "age" bigint not null ); CALL set_table_property('column_test', 'orientation', 'column'); CALL set_table_property('column_test', 'distribution_key', 'id'); CALL set_table_property('column_test', 'clustering_key', 'id'); COMMIT; INSERT INTO column_test VALUES(1,'tom',10),(2,'tony',11),(3,'tony',12); EXPLAIN SELECT * FROM column_test WHERE id>2;
返回結果:
示例2:假如查詢沒有命中索引,則不會使用clustering_index。
EXPLAIN SELECT * FROM column_test WHERE age>10;
返回結果:
Index Seek(又名pk_index):表示使用了行存表的索引,主要是主鍵索引。一般情況下,基于主鍵的行存表點查是走Fixed Plan,但是未走Fixed Plan的行存表且帶有主鍵的查詢就會走pk_index,更多原理請參見行存表原理。
示例:查詢行存表。
BEGIN; CREATE TABLE row_test_1 ( id bigint not null, name text not null, class text , PRIMARY KEY (id) ); CALL set_table_property('row_test_1', 'orientation', 'row'); CALL set_table_property('row_test_1', 'clustering_key', 'name'); COMMIT; INSERT INTO row_test_1 VALUES ('1','qqq','3'),('2','aaa','4'),('3','zzz','5'); BEGIN; CREATE TABLE row_test_2 ( id bigint not null, name text not null, class text , PRIMARY KEY (id) ); CALL set_table_property('row_test_2', 'orientation', 'row'); CALL set_table_property('row_test_2', 'clustering_key', 'name'); COMMIT; INSERT INTO row_test_2 VALUES ('1','qqq','3'),('2','aaa','4'),('3','zzz','5'); --pk_index EXPLAIN SELECT * FROM (SELECT id FROM row_test_1 WHERE id = 1) t1 JOIN row_test_2 t2 ON t1.id = t2.id;
返回結果:
Filter
Filter代表將數據根據SQL條件進行過濾,一般會跟隨seq scan on table一起,是seq scan的子節點,表示掃描表時是否有過濾,以及過濾條件是否命中索引。主要包括以下幾種Filter:
Filter
如果執行計劃中,僅包含Filter,則說明過濾條件沒有命中任何索引。此時需要去檢查表索引,重新為SQL設置合理的索引,從而提升查詢性能。
說明如果執行計劃中存在
One-Time Filter: false
,說明輸出結果為空集。示例:
BEGIN; CREATE TABLE clustering_index_test ( "id" bigint not null , "name" text not null , "age" bigint not null ); CALL set_table_property('clustering_index_test', 'orientation', 'column'); CALL set_table_property('clustering_index_test', 'distribution_key', 'id'); CALL set_table_property('clustering_index_test', 'clustering_key', 'age'); COMMIT; INSERT INTO clustering_index_test VALUES (1,'tom',10),(2,'tony',11),(3,'tony',12); EXPLAIN SELECT * FROM clustering_index_test WHERE id>2;
返回結果:
Segment Filter
Segment Filter表示查詢時命中了segment索引,與index_scan一起出現,詳情請參見Event Time Column(Segment Key)。
Cluster Filter
Cluster Filter表示查詢時命中了clustering索引,詳情請參見聚簇索引Clustering Key。
Bitmap Filter
Bitmap Filter表示查詢時命中了Bitmap索引,詳情請參見位圖索引Bitmap。
Join Filter
join完后還需要對數據做一遍filter。
Decode
Decode表示對數據進行解碼或者編碼,以加速text等文本類數據的計算。
Local Gather和Gather
在Hologres中,數據會在shard內按照文件的形式存儲。Local Gather代表數據從多個文件合并到一個shard上。Gather則代表將多個shard的數據合并匯總成最終結果。
示例:
EXPLAIN SELECT * FROM public.lineitem;
返回結果:其執行計劃如下,代表數據被掃描出來后,會先通過Local Gather在shard級匯總,然后通過Gather合并成最終數據。
Redistribution
Redistribution表示數據通過哈希分布或者隨機分布,查詢時shuffle到一個或者多個shard。
Redistribution算子常見的場景:
通常出現在join、count distinct(本質上也是join)以及group by的場景中,表的distribution key沒有設置,或者設置不合理,導致查詢時數據在多個shard間shuffle。尤其是在多表join的場景,如果出現redistribution,則說明沒有利用local join的能力,導致查詢性能不佳。
當對應的key(如join key、group by key)涉及到表達式時,例如對字段cast類型改變了原來的字段類型等,則無法利用上local join的能力,會出現redistribution。
示例:
示例1:兩表join,distribution key設置不合理導致出現redistribution。
BEGIN; CREATE TABLE tbl1( a int not null, b text not null ); CALL set_table_property('tbl1', 'distribution_key', 'a'); CREATE TABLE tbl2( c int not null, d text not null ); CALL set_table_property('tbl2', 'distribution_key', 'd'); COMMIT; EXPLAIN SELECT * FROM tbl1 JOIN tbl2 ON tbl1.a=tbl2.c;
返回結果:執行計劃如下,出現redistribution,說明distribution key設置不合理(SQL中join條件是
tbl1.a=tbl2.c
,而兩個表的distribution key分別為a和d,join時數據就會shuffle)。調優建議:如果出現redistribution算子,建議關注表的distribution key設置是否合理。更多關于redistribution的場景和distribution key的設置請參見分布鍵Distribution Key。
示例2:如下執行計劃中,join key涉及到表達式,改變了原字段的類型,無法使用local join的能力,出現redistribution。
調優建議:盡量不使用表達式。
Join
與標準數據庫的定義一致,多表關聯(Join)根據SQL的書寫方式又分為hash join、nested loop和merge join。
Hash Join
hash join是指兩個表或者多表join時,基于其中一個表(一般為小表)在內存中構建一個hash表,并把join的列值進行hash計算后放進hash表中,之后逐行的讀取另外的表,計算出其hash值并在hash表中查找,最終返回匹配的數據。根據hash join的細分類,還可以繼續劃分,如下表:
分類
描述
Hash Left Join
多表關聯時,根據join條件從左表中返回所有滿足條件的行,然后匹配右表,如果沒有匹配到數據,則返回null。
Hash Right Join
多表關聯時,返回右表的全部行和左邊滿足join條件的行,如果右表的行在左表中沒有匹配數據,那么左表則返回null。
Hash Inner Join
多表關聯時,只返回滿足join條件的行。
Hash Full Join
多表關聯時,從左表和右表返回所有的行,如果其中一個表的數據在另外一個表中沒有匹配,則無法匹配的表返回null。
Hash Anti Join
僅返回未匹配到的數據,多用于not exists查詢條件。
Hash Semi Join
有任意匹配則返回一項,通常是由exists查詢,結果不會產生重復項。
當查看hash join相關的執行計劃時,還需要關注其子節點:
hash cond:代表join的關聯條件,如
hash cond(tmp.a=tmp1.b)
。hash key:一般出現在group by場景中,表示將group by的key在多個shard上進行hash計算。
當出現hash join時,我們需要額外關注join表中的小表(數據量較小的表)是否是做了hash表,可以通過如下幾種方式查看:
執行計劃中,有hash字樣的表是hash表。
執行計劃中,從下往上看,最下面的表則是hash表。
調優建議:
更新統計信息
hash join的核心調優思路就是盡可能的將小表作為hash表,如果是大表作為hash表,因為要在內存中構建hash表,那么就會消耗較多的資源,大多數情況都是因為表的統計信息沒有更新導致優化器QO將大表作為了hash表。
示例:因為統計信息未更新(rows=1000),兩表join時大表hash_join_test_2作為hash表(tbl2數據量有100萬,tbl1數據量只有1萬),導致查詢效率較低。
BEGIN ; CREATE TABLE public.hash_join_test_1 ( a integer not null, b text not null ); CALL set_table_property('public.hash_join_test_1', 'distribution_key', 'a'); CREATE TABLE public.hash_join_test_2 ( c integer not null, d text not null ); CALL set_table_property('public.hash_join_test_2', 'distribution_key', 'c'); COMMIT ; INSERT INTO hash_join_test_1 SELECT i, i+1 FROM generate_series(1, 10000) AS s(i); INSERT INTO hash_join_test_2 SELECT i, i::text FROM generate_series(10, 1000000) AS s(i); EXPLAIN SELECT * FROM hash_join_test_1 tbl1 JOIN hash_join_test_2 tbl2 ON tbl1.a=tbl2.c;
執行計劃中大表hash_join_test_2作為hash表,如下:
如果統計信息沒有及時更新,可以手動執行
analyze <tablename>
更新統計信息。示例如下:ANALYZE hash_join_test_1; ANALYZE hash_join_test_2;
更新后的執行計劃如下,小表hash_join_test_1變成了hash表,且優化器預估的行數正確。
調整join order
通常情況下,更新統計信息能解決大部分的join關聯問題。但是當SQL比較復雜且是多表關聯時(至少5張表),Hologres優化器(QO)的默認機制會根據SQL選擇更加合理的執行計劃,導致優化器在選擇執行計劃上會耗費較多時間。我們可以通過如下GUC控制join order,以降低QO的耗時。
SET optimizer_join_order = '<value>';
value參數的取值如下:
value取值
描述
exhaustive(默認)
通過算法進行Join Order轉換,會生成最優的執行計劃,但多表關聯時會導致優化器開銷變高。
query
按照SQL的方式生成執行計劃,優化器不做任何改變,僅適用于多表關聯且表數據量不大(低于億級)的場景,從而降低QO開銷。同時不建議將該參數設置成DB級別,否則會影響其余join的性能。
greedy
通過貪心算法生成Join Order,優化器開銷適中。
Nested Loop Join和Materialize
Nested Loop代表嵌套循環連接,多表關聯時,先從一張表中讀取數據,成為外層表,再將外層驅動表的每條數據遍歷另外的表(即內層表),然后內外層表嵌套循環進行Join,相當于計算笛卡爾積。在執行計劃中第一內層表通常有Materialize算子。
優化建議:
Nested Loop的原理是內層表被外層表驅動,外層表返回的每一行都要在內層表中檢索與之匹配的行,因此返回的結果集不能太大,否則會消耗較多資源,盡量將返回結果較小的表作為外層表。
非等值的join通常會生成Nested Loop join,在SQL書寫中盡量避免非等值join。
Nested Loop join的示例如下:
BEGIN; CREATE TABLE public.nestedloop_test_1 ( a integer not null, b integer not null ); CALL set_table_property('public.nestedloop_test_1', 'distribution_key', 'a'); CREATE TABLE public.nestedloop_test_2 ( c integer not null, d text not null ); CALL set_table_property('public.nestedloop_test_2', 'distribution_key', 'c'); COMMIT; INSERT INTO nestedloop_test_1 SELECT i, i+1 FROM generate_series(1, 10000) AS s(i); INSERT INTO nestedloop_test_2 SELECT i, i::text FROM generate_series(10, 1000000) AS s(i); EXPLAIN SELECT * FROM nestedloop_test_1 tbl1,nestedloop_test_2 tbl2 WHERE tbl1.a>tbl2.c;
從下述執行計劃中可以看出存在Materialize和Nested Loop算子,說明該SQL走的是Nested Loop join路徑。
Cross Join
從V3.0版本開始,Cross Join作為Nested Loop Join的一種實現優化,主要用于處理包含小表的非等值Join等場景。與Nested Loop Join每次從外層循環提取一行數據并遍歷內層循環所有數據,然后重置內層循環子查詢狀態的計算流程相比,Cross Join直接將小表數據全部加載到內存中,然后依次與流式讀取的大表數據進行Join計算,從而顯著提升計算性能。然而,相較于Nested Loop Join,Cross Join會占用更多的內存資源。
您可以查看查詢計劃,如果出現如下Cross Join算子,表示使用了Cross Join。
如需禁用Cross Join,可以使用以下SQL語句進行關閉。
-- session級別關閉 SET hg_experimental_enable_cross_join_rewrite = off; -- database級別關閉,新建連接生效 ALTER database <database name> hg_experimental_enable_cross_join_rewrite = off;
Broadcast
Broadcast指通過廣播的方式將數據分發到各個shard,通常用在Broadcast Join的場景中,一般是小表join大表。下發SQL時,優化器QO會比較redistribution和Broadcast的代價,然后根據算法生成執行計劃。
調優建議:
當查詢表的數據量較小且實例的shard數比較少時(比如shard count=5)時,走Broadcast比較劃算。
示例:兩表join,broadcast_test_1和broadcast_test_2的數據量差異比較明顯。
BEGIN; CREATE TABLE broadcast_test_1 ( f1 int, f2 int); CALL set_table_property('broadcast_test_1','distribution_key','f2'); CREATE TABLE broadcast_test_2 ( f1 int, f2 int); COMMIT; INSERT INTO broadcast_test_1 SELECT i AS f1, i AS f2 FROM generate_series(1, 30)i; INSERT INTO broadcast_test_2 SELECT i AS f1, i AS f2 FROM generate_series(1, 30000)i; ANALYZE broadcast_test_1; ANALYZE broadcast_test_2; EXPLAIN SELECT * FROM broadcast_test_1 t1, broadcast_test_2 t2 WHERE t1.f1=t2.f1;
返回結果:
如果不是小表但又生成了Broadcast算子,通常情況是因為統計信息未及時更新導致(比如統計信息中rows=1000,實際掃描100萬),需要及時更新統計信息,執行
analyze <tablename>
。
Shard prune和Shards selected
Shard prune
表示獲取Shard的方式,包括:
lazaily:根據節點中的Shard ID先標記對應的Shard,在后續計算時選擇對應的Shard。
eagerly:根據命中的Shard選擇對應的Shard,不需要的Shard則不需要選擇。
優化器會根據執行計劃來自動匹配Shard prume的方式,無需手動調節。
Shards selected
Shards selected表示選中了多少個Shard,例如1 out of 20表示在20個Shard中選中了一個Shard。
ExecuteExternalSQL
如Hologres的產品架構介紹,Hologres的計算引擎會分為HQE、PQE、SQE等,其中PQE是原生Postgres引擎,部分Hologres自研引擎HQE還沒有支持的算子和函數,會通過PQE執行,相比于HQE,PQE的執行效率會更低。當我們在執行計劃中看到有ExecuteExternalSQL算子,說明有函數或者算子走了PQE。
示例1:走PQE的SQL示例如下。
CREATE TABLE pqe_test(a text); INSERT INTO pqe_test VALUES ('2023-01-28 16:25:19.082698+08'); EXPLAIN SELECT a::timestamp FROM pqe_test;
執行計劃如下:有ExecuteExternalSQL說明
::timestamp
算子走的是PQE。示例2:將
::timestamp
改寫成to_timestamp
,可以走HQE,SQL示例如下。EXPLAIN SELECT to_timestamp(a,'YYYY-MM-DD HH24:MI:SS') FROM pqe_test;
執行計劃如下,結果中沒有ExecuteExternalSQL,表明沒有走PQE。
調優建議:通過執行計劃找到SQL走了PQE的函數或者算子,并通過改寫使其走到HQE,提升查詢效率。常見的算子改寫列表請參見優化查詢性能。
Hologres的每個版本在不斷優化PQE的支持,將更多的PQE下推至HQE實現,因此部分函數可以通過升級版本實現自動下推,詳情請參見函數功能發布記錄。
Aggregate
Aggregate代表將數據聚合,可以是一個聚合函數或者多個聚合函數的組合。根據SQL書寫,系統又會將Aggregate分為HashAggregate,GroupAggregate等。其中:
GroupAggregate:表示數據已經按照group by進行了預排序。
HashAggregate(最常見):表示數據先進行hash計算,然后通過hash值分發至不同的shard進行聚合,最終通過Gather算子聚合。
EXPLAIN SELECT l_orderkey,count(l_linenumber) FROM public.holo_lineitem_100g GROUP BY l_orderkey;
多階段HashAggregate:數據是在shard中按照文件存儲的,文件有不同的層級,當數據量多時,聚合的階段也會分為多個階段。主要的子算子包括:
Partial HashAggregate:文件和shard內的聚合。
Final HashAggregate:多個shard上的數據聚合在一起。
示例:TPC-H Q6查詢,走了多階段HashAggregate。
EXPLAIN SELECT sum(l_extendedprice * l_discount) AS revenue FROM lineitem WHERE l_shipdate >= date '1996-01-01' AND l_shipdate < date '1996-01-01' + interval '1' year AND l_discount BETWEEN 0.02 - 0.01 AND 0.02 + 0.01 AND l_quantity < 24;
返回結果:
調優建議:一般情況下,優化器會根據數據量決定是單階段HashAggregate還是多階段HashAggregate,如果Explain Analyze中Aggregate算子的耗時較高,通常情況是數據量較大,但是優化器只讓Aggregate只做了shard級別的聚合,沒有在文件級別聚合,可以通過如下GUC參數執行多階段HashAggregate。如果本身SQL已經是多階段Aggregate了,則不需要去做額外的調整。
SET optimizer_force_multistage_agg = on;
Sort
sort表示將數據按順序排序(升序ASC或者降序DESC),通常是order by子句的結果。
示例:對TPC-H lineitem表的l_shipdate列進行排序輸出。
EXPLAIN SELECT l_shipdate FROM public.lineitem ORDER BY l_shipdate;
返回結果:
調優建議:如果order by的數據量較大,將會消耗較多的資源,需要盡量避免大數據量的排序查詢。
Limit
limit表示SQL最終允許返回的數據行數。但需要注意的是,limit僅代表最終返回的行數,并不代表實際計算中掃描的行數,實際掃描的行數要看limit是否下推至Seq Scan節點。如果下推至Seq Scan節點,才是掃描limit N的數據。
示例:如下SQL,有limit 1且下推到了Seq Scan節點,只需要掃描一行數據即可出結果。
EXPLAIN SELECT * FROM public.lineitem limit 1;
返回結果:
調優建議:
并不是所有的Limit都會被下推,所以在SQL查詢中,盡量避免全表掃描,盡量多增加過濾條件。
盡量避免Limit N時,N為超大值(例如,N為十萬或百萬)場景,否則即使Limit下推了,也會因為掃描過多的數據量而增加耗時。
Append
子查詢的結果合并,通常為Union All操作。
Exchange
Shard內的數據交換。無需過多關注。
Forward
Forward代表將算子的數據在HQE與PQE或者SQE之間傳輸,一般是HQE+PQE或者HQE+SQE的組合會出現。
Project
Project一般表示子查詢與外層查詢的映射關系,無需過多關注。
相關文檔
HoloWeb可視化查看執行計劃,請參見查看執行計劃。