這里對查詢計划的學習主要是對TPC-H中Query2的分析。
1.Query的查詢語句
select s_acctbal, s_name, n_name, p_partkey, p_mfgr, s_address, s_phone, s_comment from part, supplier, partsupp, nation, region where p_partkey = ps_partkey and s_suppkey = ps_suppkey and p_size = 6 and p_type like '%COPPER' and s_nationkey = n_nationkey and n_regionkey = r_regionkey and r_name = 'AFRICA' and ps_supplycost = ( select min(ps_supplycost) from partsupp, supplier, nation, region where p_partkey = ps_partkey and s_suppkey = ps_suppkey and s_nationkey = n_nationkey and n_regionkey = r_regionkey and r_name = 'AFRICA' ) order by s_acctbal desc, n_name, s_name, p_partkey LIMIT 100;
2.查看查詢計划
Greenplum中有語句可以查看查詢計划,使用explain命令即可:
例:testDB=#explain select * from test1;
所以Query2的查詢計划查看命令即Query2的語句之前加explain。
3.查詢中涉及到的表
testdb=# \d part Append-Only Columnar Table "public.part" Column | Type | Modifiers ---------------+-----------------------+---------------------------------------------------------- p_partkey | bigint | not null default nextval('part_p_partkey_seq'::regclass) p_name | character varying(55) | p_mfgr | character(25) | p_brand | character(10) | p_type | character varying(25) | p_size | integer | p_container | character(10) | p_retailprice | numeric | p_comment | character varying(23) | Checksum: t Distributed by: (p_partkey) testdb=# \d partsupp Append-Only Columnar Table "public.partsupp" Column | Type | Modifiers ---------------+------------------------+----------- ps_partkey | bigint | not null ps_suppkey | bigint | not null ps_availqty | integer | ps_supplycost | numeric | ps_comment | character varying(199) | Checksum: t Indexes: "idx_partsupp_partkey" btree (ps_partkey) "idx_partsupp_suppkey" btree (ps_suppkey) Distributed by: (ps_partkey, ps_suppkey) testdb=# \d supplier Append-Only Columnar Table "public.supplier" Column | Type | Modifiers -------------+------------------------+-------------------------------------------------------------- s_suppkey | bigint | not null default nextval('supplier_s_suppkey_seq'::regclass) s_name | character(25) | s_address | character varying(40) | s_nationkey | bigint | not null s_phone | character(15) | s_acctbal | numeric | s_comment | character varying(101) | Checksum: t Indexes: "idx_supplier_nation_key" btree (s_nationkey) Distributed by: (s_suppkey) testdb=# \d nation Append-Only Columnar Table "public.nation" Column | Type | Modifiers -------------+------------------------+-------------------------------------------------------------- n_nationkey | bigint | not null default nextval('nation_n_nationkey_seq'::regclass) n_name | character(25) | n_regionkey | bigint | not null n_comment | character varying(152) | Checksum: t Indexes: "idx_nation_regionkey" btree (n_regionkey) Distributed by: (n_nationkey) testdb=# \d region Append-Only Columnar Table "public.region" Column | Type | Modifiers -------------+------------------------+-------------------------------------------------------------- r_regionkey | bigint | not null default nextval('region_r_regionkey_seq'::regclass) r_name | character(25) | r_comment | character varying(152) | Checksum: t Distributed by: (r_regionkey)
上面是查詢中涉及到的5個表。
可以看到Greenplum使用的是列存儲。
Append-Only意思是不斷追加的表,不能進行更新和刪除,壓縮表必須是Append-Only表。我理解Greenplum主要是處理OLAP,為了能夠有更大的吞吐量,使用列存儲的表結構,而列存儲就可以壓縮,而壓縮表又必須是Append-Only表,所以表的標題都使用Appen-Only Columnar Table XXX。
每個表最后都有Distributed by:(XXX),是表的分布鍵,即表是按照這個鍵值分布在不同的segment上的。
4.數據庫連接圖
5.查詢分析結果
QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Limit (cost=46881.43..46881.53 rows=5 width=198) -> Gather Motion 4:1 (slice10; segments: 4) (cost=46881.43..46881.53 rows=5 width=198) Merge Key: public.supplier.s_acctbal, public.nation.n_name, public.supplier.s_name, part.p_partkey -> Limit (cost=46881.43..46881.44 rows=2 width=198) -> Sort (cost=46881.43..46881.44 rows=2 width=198) Sort Key (Limit): public.supplier.s_acctbal, public.nation.n_name, public.supplier.s_name, part.p_partkey -> Hash Join (cost=42481.34..46881.39 rows=2 width=198) Hash Cond: "Expr_SUBQUERY".csq_c0 = part.p_partkey AND "Expr_SUBQUERY".csq_c1 = public.partsupp.ps_supplycost -> HashAggregate (cost=23828.08..25828.08 rows=40000 width=40) Group By: public.partsupp.ps_partkey -> Redistribute Motion 4:4 (slice4; segments: 4) (cost=18228.08..21428.08 rows=40000 width=40) Hash Key: public.partsupp.ps_partkey -> HashAggregate (cost=18228.08..18228.08 rows=40000 width=40) Group By: public.partsupp.ps_partkey -> Hash Join (cost=423.08..17428.08 rows=40001 width=16) Hash Cond: public.partsupp.ps_suppkey = public.supplier.s_suppkey -> Append-only Columnar Scan on partsupp (cost=0.00..11805.00 rows=200000 width=24) -> Hash (cost=323.08..323.08 rows=2001 width=8) -> Broadcast Motion 4:4 (slice3; segments: 4) (cost=9.08..323.08 rows=2001 width=8) -> Hash Join (cost=9.08..223.08 rows=501 width=8) Hash Cond: public.supplier.s_nationkey = public.nation.n_nationkey -> Append-only Columnar Scan on supplier (cost=0.00..149.00 rows=2500 width=16) -> Hash (cost=8.83..8.83 rows=6 width=8) -> Broadcast Motion 4:4 (slice2; segments: 4) (cost=4.16..8.83 rows=6 width=8) -> Hash Join (cost=4.16..8.58 rows=2 width=8) Hash Cond: public.nation.n_regionkey = public.region.r_regionkey -> Append-only Columnar Scan on nation (cost=0.00..4.25 rows=7 width=16) -> Hash (cost=4.11..4.11 rows=2 width=8) -> Broadcast Motion 4:4 (slice1; segments: 4) (cost=0.00..4.11 rows=2 width=8) -> Append-only Columnar Scan on region (cost=0.00..4.06 rows=1 width=8) Filter: r_name = 'AFRICA'::bpchar -> Hash (cost=18623.96..18623.96 rows=489 width=214) -> Redistribute Motion 4:4 (slice9; segments: 4) (cost=4340.29..18623.96 rows=489 width=214) Hash Key: part.p_partkey -> Hash Join (cost=4340.29..18584.88 rows=489 width=214) Hash Cond: public.partsupp.ps_suppkey = public.supplier.s_suppkey -> Redistribute Motion 4:4 (slice6; segments: 4) (cost=4092.22..18287.96 rows=2443 width=58) Hash Key: public.partsupp.ps_suppkey -> Hash Join (cost=4092.22..18092.59 rows=2443 width=58) Hash Cond: public.partsupp.ps_partkey = part.p_partkey -> Append-only Columnar Scan on partsupp (cost=0.00..11805.00 rows=200000 width=24) -> Hash (cost=3970.11..3970.11 rows=2443 width=34) -> Broadcast Motion 4:4 (slice5; segments: 4) (cost=0.00..3970.11 rows=2443 width=34) -> Append-only Columnar Scan on part (cost=0.00..3848.00 rows=611 width=34) Filter: p_size = 6 AND p_type::text ~~ '%COPPER'::text -> Hash (cost=223.08..223.08 rows=501 width=172) -> Hash Join (cost=9.08..223.08 rows=501 width=172) Hash Cond: public.supplier.s_nationkey = public.nation.n_nationkey -> Append-only Columnar Scan on supplier (cost=0.00..149.00 rows=2500 width=154) -> Hash (cost=8.83..8.83 rows=6 width=34) -> Broadcast Motion 4:4 (slice8; segments: 4) (cost=4.16..8.83 rows=6 width=34) -> Hash Join (cost=4.16..8.58 rows=2 width=34) Hash Cond: public.nation.n_regionkey = public.region.r_regionkey -> Append-only Columnar Scan on nation (cost=0.00..4.25 rows=7 width=42) -> Hash (cost=4.11..4.11 rows=2 width=8) -> Broadcast Motion 4:4 (slice7; segments: 4) (cost=0.00..4.11 rows=2 width=8) -> Append-only Columnar Scan on region (cost=0.00..4.06 rows=1 width=8) Filter: r_name = 'AFRICA'::bpchar Settings: enable_nestloop=off Optimizer status: legacy query optimizer (60 rows)
下面對於查詢分析中的一些參數做一些說明:
slice:Greenplum在實現分布式執行計划的時候,需要將SQL拆分成多個切片,每個slice是單褲執行的一部分SQL,每一個廣播或者重分布會產生一個切片,每一個切片在每一個數據結點上都會對應的發起一個進程來處理該slice負責的數據,上一層負責該slice的進程會讀取下級slice廣播或重分布的數據,之后進行相應的計算。
segment:這里使用的是1個mdw、2個sdw,每個sdw中設置兩個primary(greenplum安裝時gpinitsystem使用的文件中設置),所以看到的segment是4。
cost:數據庫自定義的消耗單位,通過統計信息來估計SQL消耗。(查詢分析是根據analyze的固執生成的,生成之后按照這個查詢計划執行,執行過程中analyze是不會變的。所以如果估值和真是情況差別較大,就會影響查詢計划的生成。)
rows:根據統計信息估計SQL返回結果集的行數。
width:返回結果集每一行的長度,這個長度值是根據pg_statistic表中的統計信息來計算的。
6.對查詢計划的結果進行分析
1.邏輯架構圖
根據這個查詢分析,畫出查詢的邏輯架構圖:
2.對上圖做一些補充
上圖中我只畫出了一個節點的邏輯架構,並沒有表現出廣播和重分布,下面用例子說明廣播和重分布
上面這個圖是兩個節點數據重分布的例子,這個圖要完成的任務是兩個表的Hash Join,由於某種原因(之后會講述到)要將其中一個表的數據重分布到其他結點上去,以完成連接的任務。橘色部分是slice1,綠色部分是slice2,slice1中的表重分布之后到了slice2中,在slice2中做Hash Join,對連接之后的結果收集到master上。下圖說明重分布時數據在slice之間傳輸的過程
所以這也是為什么在邏輯圖中,redistribution和broadcast的橢圓處於兩個slice的交界處,並且同時屬於兩個slice。
3.能夠產生slice的操作是redistribution、broadcast和gather
1.gather、broadcast和redistribution的介紹
gather:聚合,在master上講子節點所有的數據聚合起來。一般的聚合規則是哪一個子節點的數據先返回到master上就將該節點的數據先放在master上。
broadcast:廣播,發生在兩表關聯的時候。將每個節點上的某個表的數據全部發送到所有節點,這樣每個節點都相當於有全量數據。一般,小表的時候采用廣播的方法。(注:的是不論大表還是小表,最初都是分散在所有子節點上的)
redistribution:重分布,發生在兩表關聯的時候和group by的時候。當不滿足廣播的條件或者代價太大的時候,選擇重分布,即按照新的分布鍵將各個節點上的數據重新打散到各個節點。
下面着重介紹broadcast和redistribution
2. join的時候的廣播和重分布
兩表連接的時候可能廣播可能重分布那么什么時候使用廣播,什么時候使用重分布呢?在Query中使用到的都是Hash Join,所以我們暫時只討論這種情況下的廣播和重分布。
通俗的講,兩表連接,如果是其中一個是小表,則將其廣播,因為小表廣播代價不會很大;如果兩個表都是大表可能要重分布。分三種情況討論:
默認情況,我們認為主鍵id即為分布鍵
①select * from A,B where A.id = B.id 分布鍵就是關聯鍵,兩表可以在本結點直接連接
②select * from A,B where A.id = B.id2 A的分布鍵就是關聯鍵,B的分布鍵不是關聯鍵。所以不能將A重分布,有兩種解決方案:
a.將A廣播(如果A是小表)
b.將B重分布(如果A是大表)
最終權衡取代價最小的方案
③select * from A,B where A.id2 = B.id2 A和B的分布鍵都不是關聯鍵。
a.將A或B都按照id2重分布
b.將min(A,B)廣播(如果較小的表是小表)
最終權衡取代價最小的方案
這里只講述了Hash Join的情況,除此之外還有left join和full outer join,需要了解在書中135頁有詳解。
3.group by時候的重分布
在group by的時候也可能會產生重分布,下面介紹一下group by時重分布的原理:
group by時時先在本機上進行一個group by,然后重分布,用group by時使用的字段作為分布鍵重分布,重分布之后再做一次group by,所以group by操作在分布式的環境下其實是做兩次group by的。書上119頁圖有例子說明group by的數據重分布情況。
4.分析Query2的查詢計划
下面按照每個切片的方式分析Query2的查詢計划。
slice1~4,包括slice10左分支的部分,是Query中的子查詢部分;
slice5~9是父查詢中where子句中除了ps_supplycost = (子查詢)的部分;
slice10是ps_supplycost = (子查詢)和父查詢中的order by和limit。
①slice1和slice2:region表和nation表的hash join,數據庫連接圖可以看出應該屬於上述第二種情況region.id=nation.id2,這里采用將region廣播,說明相比重分布nation,廣播region的代價更小;hash join的時候將region表hash,說明region表是個小表,這與廣播region代價更小相吻合。
②slice3:hash join的原理同①
③slice4:hash join的原理同①,三次hash join實現了四個表的連接。之后做了聚合操作,對應子查詢中min的操作,min的時候需要使用到group by,根據上面的介紹,知道group by在分布式環境下其實是做兩次的,中間是一次重分布,這里可以在slice4和slice10的左子樹看到有兩次hashAggregate。
④slice5和slice6:hash join的原理同①
⑤slice7和slice8:hash join的同①
⑥slice9:左子樹的hash join與slice3中的一樣,不同的是連接之后,slice3進行了廣播,slice9是進行了hash映射,造成這種差別的原因是這個連接表將要連接的表不同:
slice3中hash join的中間表與partsupp連接,是hash join中的第二種情況,中間表.id=partsupp.id2,由於中間表是小表,所以將中間表廣播;
slice9中hash join的中間表1與slice6的中間表2連接,是hash join中的第二種情況,中間表1.id=中間表.id2,雖然中間表1是小表,但是沒有廣播中間表1,而是將中間表2重分布,因為中間表1廣播到4個節點的代價總和大於將中間表2重分布。
⑦slice10,hash join之后,對每個子節點的數據按照四個鍵值排序,每個節點舍棄掉一部分數據(排序比較靠后,不可能包含在最后的limit結果中),將排序靠前的數據輸出給master,這個過程叫做gather,使用排序時的四個鍵值作為merge key
取排序之后的一部分(這部分一定包含最終結果)輸出給master,另一部分舍棄掉,不輸出給master。master得到最終聚合的數據,再進行一次limit操作,這次limit得到的是query需要的100條數據。
以上就是對Query2的查詢計划結果的分析,還有一些問題尚未弄清楚:
1.關聯鍵是否默認是主鍵?是(書上201頁)
2.列存儲是否有主鍵,是否有完整性約束?應該是有的,因為列存儲取到相應屬性之后還要將他們組合成行表。
3.在查詢計划的結果中,為什么query中的min(ps_supplycost)體現在查詢計划中是group by(ps_partkey)而非group by(ps_supplycost)
4.書上講的hashAggregate和groupAggregate的原理不是很清楚
5.slice10中的hash join的hash key不明白
6.order by多個鍵值是怎么排序的
心得:
1.覺得畫圖寫東西有點浪費時間,但是發現不畫出來寫出來,其實有些東西理解的不夠,或者有偏差。記錄下來有助於深入理解知識
2.寫東西相當於對知識體系的一次整合,沒整理的時候,所有東西在大腦不是混沌的,真理之后知識變得邏輯、有序。