第五章 執(zhí)行計劃詳解
[TOC]
gp 是基于 pgsql 開發(fā)的,其執(zhí)行計劃大多是跟 pgsql 一樣的,但由于 gp 是分布式并行數(shù)據(jù)庫,在 sql 執(zhí)行上有很多 MPP 的痕跡,因此在理解 gp 的執(zhí)行計劃時,一定要將其分布式框架熟讀在心,從而能夠通過調(diào)整執(zhí)行計劃給 sql 帶來很大的性能提升。
5.1 執(zhí)行計劃入門
5.1.1 什么是執(zhí)行計劃
執(zhí)行計劃就是數(shù)據(jù)庫運(yùn)行 sql 的步驟,相當(dāng)算法,讀懂 gp 的執(zhí)行計劃,對理解 sql 的正確性即性能有很大的幫助。執(zhí)行計劃時數(shù)據(jù)庫使用者了解數(shù)據(jù)庫內(nèi)部結(jié)構(gòu)的一個重要途徑。
5.1.2 查看執(zhí)行計劃
跟 pgsql 一樣,gp 通過 explain 命令來查看執(zhí)行計劃。具體語法如下:
EXPLAIN [ ANALYZE ] [ VERBOSE ] statement
各個參數(shù)的含義如下:
- ANALYZE:執(zhí)行命令并顯示實際運(yùn)行時間。
- VERBOSE:顯示規(guī)劃樹完整的內(nèi)部表現(xiàn)形式,而不僅是一個摘要。通常,這個選項只是在特殊的調(diào)試過程中有用,VERBOSE 輸出是否打印工整的,具體取決于配置參數(shù) explain_pretty_print 的值。
- statement:查詢執(zhí)行計劃的 SQL 語句,可以是任何 select、insert、update、delete、values、execute、declare 語句。
5.2 分布式執(zhí)行計劃概述
5.2.1 架構(gòu)

圖5-1 很好地說明了 ShareNothing 的特點:
- 底層的數(shù)據(jù)完全部共享。
- 每個 Segment 只有一部分?jǐn)?shù)據(jù)。
- 每一個節(jié)點都通過網(wǎng)絡(luò)連接在一起。
5.2.2 重分布于廣播
關(guān)聯(lián)數(shù)據(jù)在不同節(jié)點上,對于普通關(guān)系型數(shù)據(jù)庫來說,是無法進(jìn)行連接的。關(guān)聯(lián)的數(shù)據(jù)需要通過網(wǎng)絡(luò)流入到一個節(jié)點中進(jìn)行計算,這樣就需要發(fā)生數(shù)據(jù)遷移。數(shù)據(jù)遷移有廣播和重分布兩種。

圖5-2 展示了 gp 中重分布數(shù)據(jù)的實現(xiàn)。
在圖5-2中,兩個 Segment 分別進(jìn)行計算,但由于其中一張表的關(guān)聯(lián)鍵與分布鍵不一致,需要關(guān)聯(lián)的數(shù)據(jù)不在同一個節(jié)點上,所以在 SLICE1 上需要將其中一個表進(jìn)行重分布,可理解為在每個節(jié)點之間互相交換數(shù)據(jù)。
關(guān)于廣播與重分布,gp 有一個很重要的概念:Slice(切片)。每一個廣播或 重分布會產(chǎn)生一個切片,每一個切片在每個數(shù)據(jù)節(jié)點上都會對應(yīng)發(fā)起一個進(jìn)行來處理該 Slice 負(fù)責(zé)的數(shù)據(jù),上一層負(fù)責(zé)該 Slice 的進(jìn)程會讀取下級 Slice 廣播或重分布的數(shù)據(jù),然后進(jìn)行相應(yīng)的計算。
由于在每個 Segment 上每一個 Slice 都會發(fā)起一個進(jìn)程來處理,所以在 sql 中藥嚴(yán)格控制切片的個數(shù),如果重分布或者廣播太多,應(yīng)適當(dāng)將 sql 拆分,避免由于進(jìn)程太多給數(shù)據(jù)庫或者是機(jī)器帶來太多的負(fù)擔(dān)。進(jìn)程太多也比較容易導(dǎo)致 sql 失敗

Slice 之間如何交互可以從圖5-3中看出。
下面通過一個實際的數(shù)據(jù)形象地介紹數(shù)據(jù)在 Segment 中的切分。比方說,對一個成績表來說,分布鍵是學(xué)號(sno),我們現(xiàn)在要按照成績(score)來執(zhí)行 group by,那么就需要將數(shù)據(jù)按照 score 字段進(jìn)行重分布,重分布前會對每個 Segment 的數(shù)據(jù)進(jìn)行局部匯總,重分布后,同一個 score 的數(shù)據(jù)都在同一個 Segment 上,再進(jìn)行一次匯總即可,數(shù)據(jù)的具體情況如圖5-4所示。

5.2.3 Greenplum Master 的工作
Master 在 sql 的執(zhí)行過程中承擔(dān)著很多重要的工作,主要如下:
- 執(zhí)行計劃解析即分發(fā)。
- 將子節(jié)點的數(shù)據(jù)匯集在一起。
- 將所有 Segment 的有序數(shù)據(jù)進(jìn)行歸并操作(歸并排序)。
- 聚合函數(shù)在 Master 上進(jìn)行最后的計算。
- 需要有唯一的序列的功能(如開窗函數(shù)不帶 partition by 字句)。
舉個簡單的例子,在計算學(xué)生的平均分?jǐn)?shù)時,在每個節(jié)點上先計算好 sum 和 count 值,然后再由 Master 匯總,再次進(jìn)行少量計算,算出平均值,如圖5-5所示。

5.3 Greenplum 執(zhí)行計劃中的術(shù)語
5.3.1 數(shù)據(jù)掃描方式
gp 掃描數(shù)據(jù)的方式有很多種,每一種掃描方式都有其特點:
(1)、Seq Scan:順序掃描
順序掃描在數(shù)據(jù)庫中是最常見,也是最簡單的一種方式,就是講一個數(shù)據(jù)文件從頭到尾讀取一次,這種方式非常符合磁盤的讀寫特性,順序讀寫,吞吐很高。對于分析性的語句,順序掃描基本上是對全表的所有數(shù)據(jù)進(jìn)行分析計算,因此這一個方式非常有效。在數(shù)據(jù)倉庫中,絕大部分都是這種掃描方式,在 gp 中結(jié)合壓縮表一起使用,可以減少磁盤 IO 的損耗。
(2)、Index Scan:索引掃描
索引掃描是通過索引來定位數(shù)據(jù)的,一般對數(shù)據(jù)進(jìn)行特定的篩選,篩選后的數(shù)據(jù)量比較?。▽τ谡麄€表而言)。使用索引進(jìn)行篩選,必須事先在篩選的字段上建立索引,查詢時先通過索引文件定位到實際數(shù)據(jù)在數(shù)據(jù)文件中的位置,再返回數(shù)據(jù)。對于磁盤而言,索引掃描都是隨機(jī) IO,對于查詢小數(shù)據(jù)量而言,速度很快。
(3)、Bitmap Heap Scan:位圖堆表掃描
當(dāng)索引定位到的數(shù)據(jù)在整表中占比較大的時候,通過索引定位到的數(shù)據(jù)會使用位圖的方式對索引字段進(jìn)行位圖堆表掃描,以確定結(jié)果數(shù)據(jù)的準(zhǔn)確。對于數(shù)據(jù)倉庫應(yīng)用而言,很少用這種掃描方式。
(4)、Tid Scan:通過隱藏字段 ctid 掃描
ctid 是pgsql 中標(biāo)記數(shù)據(jù)位置的字段,通過這個字段來查找數(shù)據(jù),速度非???,類似于 oracle 的 rowid。gp 是 一個分布式數(shù)據(jù)庫,每一個子節(jié)點都是一個pgsql 數(shù)據(jù)庫,每一個子節(jié)點都單獨維護(hù)自己的一套 ctid 字段。
如果在 gp 中通過 ctid 來找數(shù)據(jù),會有如下的提示:
Select * from test1 where ctid='(1,1)';
NOTICE: SELECT uses system-definedd column "test1.ctid" without the necessary companion column "test1.gp_segment_id"
HINT: TO uniquely identify a row within a distributer table, use the "gp_segment_id" column together with the "ctid" column.
就是說,如果想確定到具體一行數(shù)據(jù),還必須通過制定另外一個隱藏字段(gp_segment_id)來確定取哪一個數(shù)據(jù)庫的 ctid 值。
select * from test1 where ctid='(1,1)' and gp_segment_id=1;
(5)、Subquery Scan '*SELECT*':子查詢掃描
只要 sql 中有子查詢,需要對子查詢的結(jié)果做順序掃描,就會進(jìn)行子查詢掃描。
(6)、Function Scan:函數(shù)掃描
數(shù)據(jù)庫中有一些函數(shù)的返回值是一個結(jié)果集,數(shù)據(jù)庫從這個結(jié)果集中取出數(shù)據(jù)的時候,就會用到這個 Function Scan,順序獲取函數(shù)返回的結(jié)果集(這是函數(shù)掃描方式,不屬于表掃描方式),如:
explain select * from generate_series(1,10);
5.3.2 分布式執(zhí)行
(1) Gather Motion(N:1)
聚合操作,在 Master 上講子節(jié)點所有的數(shù)據(jù)聚合起來。一般的聚合規(guī)則是:哪一個子節(jié)點的數(shù)據(jù)線返回到 Master 上就將該節(jié)點的數(shù)據(jù)先放在 Master 上。
(2) Broadcast Motion(N:N)
廣播,將每個 Segment 上某一個表的數(shù)據(jù)全部發(fā)送給所有 Segment。這樣每一個 Segment 都相當(dāng)于有一份全量數(shù)據(jù),廣播基本只會出現(xiàn)在兩邊關(guān)聯(lián)的時候,相關(guān)內(nèi)容再選擇廣播或者重分布,5.7節(jié)中有詳細(xì)的介紹。
(3) Redistribute Motion(N:N)
當(dāng)需要做跨庫關(guān)聯(lián)或者聚合的時候,當(dāng)數(shù)據(jù)不能滿足廣播的條件,或者廣播的消耗過大時,gp 就會選擇重分布數(shù)據(jù),即數(shù)據(jù)按照新的分布鍵(關(guān)聯(lián)鍵)重新打散到每個 Segment 上,重分布一般在以下三種情況下回發(fā)生:
- 關(guān)聯(lián):將每個 Segment 的數(shù)據(jù)根據(jù)關(guān)聯(lián)鍵重新計算 hash 值,并根據(jù) gp 的路由算法路由到目標(biāo)子節(jié)點中,使關(guān)聯(lián)時屬于同一個關(guān)聯(lián)鍵的數(shù)據(jù)都在同一個 Segment 上。
- group by :當(dāng)表需要 group by ,但是 group by 的字段不是分布鍵時,為了使 group by 的字段在同一個庫中,gp 會分兩個 group by 操作來執(zhí)行,首先,在單庫上執(zhí)行一個 group by 操作,從而減少需要重分布的數(shù)據(jù)量;然后將結(jié)果數(shù)據(jù)按照 group by 字段重分布,之后在做啊聚合獲得最終結(jié)果。
- 開窗函數(shù):跟group by 類似,開窗函數(shù)(Window Function)的實現(xiàn)也需要將數(shù)據(jù)重分布到每個節(jié)點上進(jìn)行計算,不過其實現(xiàn)比 group by 更復(fù)雜一些。
(4) 切片(Slice)
gp 在實現(xiàn)分布式執(zhí)行計劃的時候,需要將 sql 拆分成多個切片(Slice),每一個 Slice 其實是單庫執(zhí)行的一部分 sql,上面描述的每一個 motion 都會導(dǎo)致 gp 多一個 Slice 操作,而每一個 Slice 操作子節(jié)點都會發(fā)起一個進(jìn)程來處理數(shù)據(jù)。
所以應(yīng)該盡量控制 Slice 的個數(shù),將太復(fù)雜的 sql 拆分,減少進(jìn)程數(shù),在執(zhí)行計劃中,最常見的 Slice 關(guān)鍵字的地方就是廣播跟重分布,如下:
Broadcast Motion 6:6 (slice1)
Gather Motion 6:1 (slice1)
5.3.3 兩種聚合方式
HashAggregate 和 GroupAggregate 這兩種聚合方式在 5.7 介紹執(zhí)行原理時會給出詳細(xì)的講解,這里主要從占用內(nèi)存方面簡單介紹:
(1) HashAggregate
對于 Hash 聚合來說,數(shù)據(jù)庫會根據(jù) group by 字段后面的值計算 hash 值,并根據(jù)前面使用是的聚合函數(shù)在內(nèi)存中維護(hù)對應(yīng)的列表,然后數(shù)據(jù)庫會通過這個列表來實現(xiàn)聚合操作,效率相對較高。
(2) GroupAggregate
對于普通聚合函數(shù),使用 group 聚合,其原理是先將表中的數(shù)據(jù)按照 group by 的字段排序,這樣同一個 group by 的值就在一起,只需要對排好序的數(shù)據(jù)進(jìn)行一次全掃描就可以得到聚合的結(jié)果。
5.3.4 關(guān)聯(lián)
gp 中的關(guān)聯(lián)的實現(xiàn)比較多,有 Hash Join、NestLoop、Merge Join,實現(xiàn)方式跟普通的 pgsql 數(shù)據(jù)庫方式一樣。由于 gp 是分布式的,所以關(guān)聯(lián)可能會涉及表的廣播或重分布。下面通過實際的執(zhí)行計劃來分析這 3 中關(guān)聯(lián)在 gp 上的簡單實現(xiàn),首先建立兩張表以方便我們查看后面的執(zhí)行計劃:
testDB=# create table test1 (id int,values varchar(256)) distributed by (id);
CREATE TABLE
testDB=# create table test2 (id int,values varchar(256)) distributed by (id);
CREATE TABLE
1. Hash Join
Hash Join(Hash 關(guān)聯(lián)) 是一種很搞笑的關(guān)聯(lián)方式,簡單地說,其實現(xiàn)原理就是講一張關(guān)聯(lián)表按照關(guān)聯(lián)鍵在內(nèi)存中建立哈希表,在關(guān)聯(lián)的時候通過哈希的方式來處理。
下面是一個 Hash Join 的例子:
testDB=# explain select * from test1 a,test2 b where a.id=b.id;
QUERY PLAN
-------------------------------------------------------------------
Gather Motion 6:1 (slicel) (cost=0.01..0.05 row=3 width=300)
-> Hash Join (cost=0.01..0.05 rows=3 width=300)
Hash Cond: a.id=b.id
-> Seq Scan on test1 a (cost=0.00..0.00 rows=1 width=150)
-> Hash (cost=0.00..0.00 rows=1 width=150)
-> Seq Scan on test2 b (cost=0.00..0.00 rows=1 width=150)
(6 rows)
2. Hash Left Join
通過 Hash Join 的方式來實現(xiàn)左連接,在執(zhí)行計劃中的體現(xiàn)就是 Hash Left Join:
testDB=# explain select * from test1 a left join testb on a.id=b.id;
QUERY PLAN
-------------------------------------------------------------------
Gather Motion 6:1 (slicel) (cost=0.01..0.05 row=3 width=300)
-> Hash Left Join (cost=0.01..0.05 rows=3 width=300)
Hash Cond: a.id=b.id
-> Seq Scan on test1 a (cost=0.00..0.00 rows=1 width=150)
-> Hash (cost=0.00..0.00 rows=1 width=150)
-> Seq Scan on test2 b (cost=0.00..0.00 rows=1 width=150)
(6 rows)
3. NestedLoop
NestedLoop 關(guān)聯(lián)是最簡單,也是最低效的關(guān)聯(lián)方式,但是在有些情況下,不得不使用 NestedLoop,例如笛卡爾積:
testDB=# explain select * from test1, test2;
QUERY PLAN
-------------------------------------------------------------------
Gather Motion 6:1 (slicel) (cost=0.08..0.20 row=3 width=300)
-> Nested Loop (cost=0..08..0.20 rows=3 width=300)
-> Seq Scan on test1 a (cost=0.00..0.00 rows=1 width=150)
-> Materialize (cost=0.08..0.14 rows=6 width=150)
-> Broadcast Motion 6:6 (slicel) (cost=0.00..0.07 rows=6 width=150)
-> Seq Scan on test2 b (cost=0.00..0.00 rows=1 width=150)
Settings: enable_seqscan=off
(7 rows)
由于是笛卡爾積,因此 sql 一定是采取 NestedLoop 關(guān)聯(lián)。在 gp 中,如果采取 NestedLoop,關(guān)聯(lián)的兩張表中有一張表必須廣播,否則無法關(guān)聯(lián),一般是數(shù)據(jù)量比較小的表會廣播。
4. Merge Join 和 Merge Left Join
Merge Join 也是量表關(guān)聯(lián)中比較常見的關(guān)聯(lián)方式,這種關(guān)聯(lián)方式需要將兩張表按照關(guān)聯(lián)鍵進(jìn)行排序,然后按照歸并排序的方式將數(shù)據(jù)進(jìn)行關(guān)聯(lián),效率比 Hash Join 差。
下面的例子先通過設(shè)置兩個參數(shù)來強(qiáng)制執(zhí)行計劃,采取的是 Merge Join 方式:
testDB=# set enable_hashjoin =off;
SET
testDB=# set enable_mergejion =on;
SET
testDB=# explain select * from test1 a join test2 b on a.id=b.id;
QUERY PLAN
-------------------------------------------------------------------
Gather Motion 6:1 (slicel) (cost=0.02..0.05 rows=3 width=300)
-> Merge Join (cosr=0.02..0.05 rows=3 with=300)
Merge Cond: a.id=b.id
-> Sort (cost=0.01..0.02 rows=1 width=150)
Sort Key: a.id
-> Seq Scan on test1 a (cost=0.00..0.00 rows=1 width=150)
->Sort (cost=0.01..0.02 rows=1 width=150)
Sort Key: b.id
-> Seq Sacn on test2 b (cost=0.00..0.00 rows=1 width=150)
Settings: enable_hashjoin=off; enable_mergejoin=on; enable_seqscan=off
(10 rows)
伴隨 Merge Join 的肯定是兩張表關(guān)聯(lián)鍵的排序。
5. Merge Full Join
如果關(guān)聯(lián)使用的是 full outer join,則執(zhí)行計劃使用的是 Merge Full Join。在 gp 中其他的關(guān)聯(lián)方式都無法進(jìn)行全關(guān)聯(lián)。
testDB=# explain select * from test1 a full outer join test2 b on a.id=b.id;
QUERY PLAN
-------------------------------------------------------------------
Gather Motion 6:1 (slicel) (cost=0.02..0.05 rows=3 width=300)
-> Merge Full Join (cost=0.02..0.05 rows=3 width=300)
Merge Cond: a.id=b.id
-> Sort (cost=0.01..0.02 rows=1 width=150)
Sort Key: a.id
-> Seq Scan on test1 a (cost=0.00..0.00 rows=1 width=150)
-> Sort (cost=0.01..0.02 rows=1 width=150)
Sort Key: b.id
-> Seq Scan on test2 b (cost=0.00..0.00 rows=1 width=150)
Settings: enable_seqscan=off
(1o rows)
在 oracle 10g 中,a full outer join b 的實現(xiàn)方式是對 a 和 b 做一個左外關(guān)聯(lián),然后對 b 和 a 做一個反連接(在關(guān)聯(lián)時,匹配的剔除,不匹配的保留),再對兩個結(jié)果直接進(jìn)行 union all 操作。但是在 gp 中沒有執(zhí)行這個優(yōu)化,所有只能采取 Merge Join。Nest咯哦片只能用于內(nèi)連接,對外連接無能為力。
6. Hash EXISTS Join
關(guān)聯(lián)子查詢 exist 之類的 sql 會被改寫成 inner join,如果 sql 被改寫了,則會出現(xiàn) Hash EXISTS Join。
testDB=# explain select * from test1 a where exists(select 1 from test2 b where a.id=b.id);
QUERY PLAN
-------------------------------------------------------------------
Gather Motion 6:1 (slicel) (cost=0.02..0.05 rows=3 width=300)
-> Hash EXISTS Join (cost=0.01..0.05 rows=3 width=150)
Hash Cond: a.id = b.id
-> Seq Scan on test1 a (cost=0.00..0.00 rows=1 width=150)
-> Hash (cost=0.00..0.00 rows=1 width=4)
-> Seq Scan on test2 b (cost=0.00..0.00 rows=1 width=4)
(6 rows)
5.3.5 SQL 消耗
在每個 sql 的執(zhí)行計劃中,每一步都會有(cost=0.01.。0.05 rows=3 width=150)折3項表示 sql 的消耗,這三個字段的含義:
(1) Cost
以數(shù)據(jù)庫自定義的消耗單位,通過統(tǒng)計信息來估計 sql 的消耗。具體消耗的單位可以參考 pgsql 的官方文檔: http://www.pgsqldb.org/pgsqldoc-8.1c/runtime-config-query.html
(2) Rows
根據(jù)統(tǒng)計信息估計 sql 返回結(jié)果集的行數(shù)。
(3) Width
返回結(jié)果集每一行的長度,這個長度值是根據(jù) pg_statistic 表中的統(tǒng)計信息來計算的。
5.3.6 其他術(shù)語
(1) Filter 過濾
where 條件中的篩選條件,在執(zhí)行計劃中就是 Filter 關(guān)鍵字。
Filter: relfilenode = 1249::oid
(2)Index Cond
如果在查詢的表中 where 篩選的字段中有阿銀,那么執(zhí)行計劃會通過索引定位,提高查詢的效率。Index Cond 就是定位索引的條件。
Index Scan using pg_class_oid_index on pg_class(cost=0.00..200.27 rows=1 width=205)
Index Cond: oid = 1259::oid
(3)Recheck Cond
在使用位圖掃描索引的時候, 由于 pgsql 里面使用的是 MVCC 協(xié)議,為了保證結(jié)果的正確性,要重新檢查一下過濾條件。
Bitmap Heap Scan on test1 (cost=100.37..103.48 rows=7 width=12)
Recheck Cond: a >= 1 AND a <= 50
-> Bitmap Index Scan on dix_test1 (cost=0.00..100.37 rows=7 width=0)
Index Cond: a>=1 AND a<=50
(4) Hash Cond
執(zhí)行 Hash Join 的時候的關(guān)聯(lián)條件:
-> Hash Join (cost=40.87..119.60 rows=1683 width=24)
Hash Cond: y.b = x.a
(5)Merge
在執(zhí)行排序操作時數(shù)據(jù)會在子節(jié)點上各自排好序然后在 Master 上做一個歸并操作:
Gather Motion 6:1 (slicel) (cost=0.01..0.02 rows=1 width=150)
Merge Key: id
-> Sort (cost=0.01..0.02 rows=1 width=150)
(6)Hash Key
在數(shù)據(jù)重分布時候指定的重算 hash 值的分布鍵:
-> Redistribute Motion 6:6 (slicel) (cost=0.00..53.49 rows=1683 width=12)
Hash Key: y.b
-> Seq Scan on test2 y (cost=0.00..19.83 rows=1683 width=12)
(7)Materialize
將數(shù)據(jù)保存在內(nèi)存中,避免多次掃描磁盤帶來的開銷。這個要重點注意,由于將數(shù)據(jù)保存在內(nèi)存中,會占用很大的內(nèi)存,而執(zhí)行計劃時按照統(tǒng)計信息來計算的,如果統(tǒng)計信息丟失或者錯誤,有可能會將一張很大的表保存在內(nèi)存中,直接導(dǎo)致內(nèi)存不足,進(jìn)而導(dǎo)致 sql 執(zhí)行失?。?/p>
-> Materialize (cost=147.74..248.72 rows=10098 width=12)
-> Broadcast Motion 6:6 (slicel) (cost=0.00..137.64 rows=1098 width=12)
-> Seq Scan on test2 y (cost=0.00..19.83 rows=1683 width=12)
(8)Join Filter
對數(shù)據(jù)關(guān)聯(lián)后再進(jìn)行篩選,如:
-> Nested Loop (cost=147.74..467528.25 rows=314721 width=24)
Join Filter: x.a < y.a AND x.a > (y.a + 1)
(9)Sort,Sort Key
如果執(zhí)行計劃中出現(xiàn)了 Sort 關(guān)鍵字,則說明有排序的操作,排序的字段為: Sort Key。
-> Sort (cost=0.01..0.02 rows=1 width=150)
Sort Key: id
-> Seq Scan on test1 (cost=0.00..0.00 rows=1 width=150)
(10)Window,Partition By,Order by
這個出使用開窗函數(shù)(Window Function)時,執(zhí)行計劃顯示了使用分析函數(shù)的標(biāo)識:
testDB=# explain select * from ( select row_number() over (partition by id order by values) rn from test1) t where rn=1;
QUERY PLAN
-------------------------------------------------------------------
Gather Motion 6:1 (slicel) (cost=0.01..0.03 rows=1 width=8)
-> Subquery Scan t (cost=0.01..0.03 rows=1 width=8)
FIlter: rn = 1
-> Window (cost=0.01..0.02 rows=1 width=150)
Partition By: id
Order By: "values"
-> Sort (cost=0.01..0.02 rows=1 width=150)
Sort Key: id, "values"
-> Seq Scan on test1 (cosr=0.00..0.00 rows=1 width=150)
Settings: enable_seqscan=off
(10 rows)
(11)Limit
當(dāng)在 sql 只取前幾行時,就使用 Limit 語句:
Limit (cost=0.00..0.85 rows=1 width=205)
(12)Append
將結(jié)果直接匯總起來:
-> Append (cost=0.00..2.02 rows=1 width=13998)
->Append-only Scan on offer_1_prt_p20100801 offer
->Append-only Scan on offer_1_prt_p20100802 offer
->Append-only Scan on offer_1_prt_p20100803 offer
5.4 數(shù)據(jù)庫統(tǒng)計信息收集
gp 與 oracle 等數(shù)據(jù)庫一樣,都是根據(jù) CBO 優(yōu)化器來選擇一個好的執(zhí)行計劃的,尤其是在識別廣播或者重分布的時候,統(tǒng)計信息十分重要,其準(zhǔn)確與否直接決定了執(zhí)行計劃的好壞。
5.4.1 Analyze 分析
統(tǒng)計信息的命令如下:
ANALYZE [ VERBOSE ] [ table [ (column [, ...] ) ] ]
如果沒有參數(shù),ANALYZE 檢查當(dāng)前數(shù)據(jù)庫中所有表。如果有參數(shù),則只檢查參數(shù)指定的那個表。還可以給出一列字段名字,則只收集給出的字段的統(tǒng)計信息。
ANALYZE收集表內(nèi)容的統(tǒng)計信息,表級別的信息(表的數(shù)據(jù)量及表大小)保存在 pg_class 中的 reltuples 和 relpages 字段中,然后把字段級別的結(jié)果保存在系統(tǒng)表 pg_statistic 中,字段級信息通常包括每個字段最常用數(shù)值的列表以及顯示每個字段中數(shù)據(jù)近似分布的包線圖。
在GP中會對表自動進(jìn)行統(tǒng)計信息收集??刂谱詣邮占膮?shù)是 gp_autostats_mode,這個參數(shù)有三個值:none、on_change、on_no_stats。

這個參數(shù)在Master上修改,然后通過 gpstop -u 重新加載 postgresql.conf 這個配置文件即可。默認(rèn)on_no_stats。
5.4.2 固定執(zhí)行計劃
greenplum是通過統(tǒng)計信息來生成執(zhí)行計劃的。
一般對執(zhí)行計劃影響最大的是 pg_class 的 relpages 和 reltuples 這兩個字段,reltuples是表的數(shù)據(jù)量,relpages則表示表大小除以32k,即:
select pg_relation_size(tablename)/32/1024;
5.5 控制執(zhí)行計劃的參數(shù)介紹
在gp 中,控制執(zhí)行計劃的參數(shù)都是在會話級別,對于同一會話的所有sql生效。
這些配置參數(shù)提供了查詢優(yōu)化器選擇查詢規(guī)劃的原始方法。如果優(yōu)化器為特定的查詢選擇的默認(rèn)規(guī)劃并不是最優(yōu),那么我們就可以通過使用這些配置參數(shù)強(qiáng)制優(yōu)化器選擇一個更好的規(guī)劃來臨時解決這個問題。這些參數(shù)一般是在某個會話級別對某個sql進(jìn)行設(shè)置的,不建議在全局中修改,會影響其他正常sql的執(zhí)行計劃。


5.6 規(guī)劃期開銷的計算方法
在選擇合理的執(zhí)行計劃的時候,gp 會遍歷所有的執(zhí)行計劃,計算其開銷,即cost值,并選擇最小的執(zhí)行路徑執(zhí)行sql。
一般,gp/pgsql 以抓取順序頁的開銷作為基準(zhǔn)單位,也就是說將 seq_page_cost 設(shè)為1.0,同時其他開銷參數(shù)對照它來設(shè)置。
表5-3 是gp中衡量數(shù)據(jù)庫消耗的各個變量及默認(rèn)參數(shù)。

減小 random_page_cost 值(相對于 seq_page_cost)將導(dǎo)致更傾向于使用索引掃描,而增加這個值導(dǎo)致更傾向于使用順序掃描??梢酝ㄟ^同時增加或減少這兩個值來調(diào)整磁盤 I/O 相對于 CPU的開銷。
5.7 各種執(zhí)行計劃原理分析
5.7.1 詳解關(guān)聯(lián)的廣播與重分布
分布式的關(guān)聯(lián)有兩種:
- 單庫關(guān)聯(lián):關(guān)聯(lián)鍵與分布鍵一致,只需要在單個庫關(guān)聯(lián)后得到結(jié)果即可
- 跨庫關(guān)聯(lián):關(guān)聯(lián)鍵與分布鍵不一致,數(shù)據(jù)需要重新分布,轉(zhuǎn)換成單庫關(guān)聯(lián),從而實現(xiàn)表的關(guān)聯(lián)。

1、內(nèi)連接
情況1:
select * from A,B where A.id=B.id;
分布鍵與關(guān)聯(lián)鍵相同,屬于單庫關(guān)聯(lián),不會造成廣播或者重分布。
情況2:
select * from A,B where A.id=B.id2;
表A的關(guān)聯(lián)鍵是分布鍵,表B的關(guān)聯(lián)鍵不是分布鍵,那么可以通過兩種方法來實現(xiàn)表的關(guān)聯(lián):
- 將表B按照id2字段將數(shù)據(jù)重分布到每一個節(jié)點上,然后再與表A進(jìn)行關(guān)聯(lián)。重分布的數(shù)據(jù)量是N
- 將表A廣播,每一個節(jié)點都放一份全量數(shù)據(jù),然后再與表B關(guān)聯(lián)得到結(jié)果。廣播的數(shù)據(jù)量是 M * 節(jié)點數(shù)
所以當(dāng)N>M * 節(jié)點數(shù) 的時候,選擇表A廣播,否則選擇表B重分布。
情況3:
select * from A,B where A.id2=B.id2;
對于這種情況,兩個表的關(guān)聯(lián)鍵及分布鍵都不一樣,那么還有兩種做法:
- 將表A與表B都按照id2字段,將數(shù)據(jù)重分布到每個節(jié)點,重分布的代價是M+N
- 將其中一個表廣播后再關(guān)聯(lián),當(dāng)然選取小表廣播,代價小,廣播的代價是 min(M,N) * 節(jié)點數(shù)
所以,當(dāng) N + M > min(M,N) * 節(jié)點數(shù) 的時候,選擇小表廣播,否則選擇兩個表都重分布。
2、左連接:
情況1:
select * from A left join B on A.id=B.id;
單庫關(guān)聯(lián),不涉及數(shù)據(jù)跨庫關(guān)聯(lián)
情況2:
select * from A left join B on A.id=B.id2;
由于左表的分布鍵是關(guān)聯(lián)鍵,鑒于左連接的性質(zhì),無論表B數(shù)據(jù)量多大,都必須將表B按照字段id2重分布數(shù)據(jù)。
情況3:
select * from A left join B on A.id2=B.id;
左表的關(guān)聯(lián)鍵不是分布鍵,由于左連接A表肯定是不能被廣播的,所以有兩種方式:
- 將表A按照id2重分布數(shù)據(jù),轉(zhuǎn)換成情況A,代價為M
- 將表B廣播,代價為 N * 節(jié)點數(shù)
情況4:
select * from A left join B on A.id2=B.id2;
- 將表A與表B都哦據(jù)考id2字段將數(shù)據(jù)重分布一遍,轉(zhuǎn)換成情況1,代價是M+N
- 表A不能被廣播,只能將表B廣播,代價是 N * 節(jié)點數(shù)
對于有多種情況,gp總是選擇代價小的方式來執(zhí)行sql
3、全連接:
情況1:
select * from A full outer join B on A.id=B.id;
對于關(guān)聯(lián)鍵都是分布鍵的情況,在gp中全連接只能采用Merger Join來實現(xiàn)
情況2:
select * from A full outer join B on A.id = B.id2;
將不是關(guān)聯(lián)鍵不是分布鍵的才重分布數(shù)據(jù),轉(zhuǎn)換成情況1來解決。無論A、B大小分別為多少,為了實現(xiàn)全連接,不能將表廣播,只能是重分布。
情況3:
select * from A full outer join B on A.id2=B.id2;
將兩張表都重分布,轉(zhuǎn)換成情況1進(jìn)行處理。
5.7.2 HashAggregate 與 GroupAggregate
在pgsql/gp 數(shù)據(jù)庫中,聚合函數(shù)有兩種實現(xiàn)方式:HashAggregate 與 GroupAggregate。
案例:
select count(1) from pg_class group by oid;
1、兩種實現(xiàn)算法的比較:
(1)HashAggregate
對于hash聚合來說,數(shù)據(jù)庫會根據(jù)group by字段后面的值算出hash值,并根據(jù)前面使用的聚合函數(shù)在內(nèi)存中維對應(yīng)的列表。如果select后面有兩種聚合函數(shù),那么在內(nèi)存中就會維護(hù)兩個對應(yīng)的數(shù)據(jù)。同樣的,有n個聚合函數(shù)就會維護(hù)n個同樣的數(shù)組。對于hash聚合來說,數(shù)組的長度肯定是大于group by 的字段的distinct值的個數(shù)的,且與這個值應(yīng)該呈線性關(guān)系,group by 后面的字段重復(fù)值越少,使用的內(nèi)存也就越大。
執(zhí)行計劃如下:

(2)GroupAggregate
對于普通聚合函數(shù),使用GroupAggregate,其原理是先將表中的數(shù)據(jù)按照group by 的字段排序,這樣同一個group by 的值就在一起,只需要對排好序的數(shù)據(jù)進(jìn)行一次全掃描,并進(jìn)行對應(yīng)的聚合函數(shù)的計算,就可以得到聚合的結(jié)果

從上面兩個執(zhí)行計劃的消耗來說,GroupAggregate由于需要排序,效率很差,消耗是HashAggregate的7倍,所以在gp中,對于聚合函數(shù)的使用,采用的都是HashAggregate。
2、兩種實現(xiàn)的內(nèi)存消耗
結(jié)論:
HashAggregate 在少數(shù)聚合函數(shù)時表現(xiàn)優(yōu)異,但是對于很多聚合函數(shù)的情況,性能和消耗的內(nèi)存差異很明顯。尤其是受group by 字段唯一性的影響,字段count(district)值越大,HashAggregate消耗的內(nèi)存越多,性能下降越明顯。
所以在SQL中有大量聚合函數(shù)group by的字段重復(fù)值比較少的時候,應(yīng)該用GroupAggregate,而不能用HashAggregate。
5.7.3 Nestloop Join、Hash Join與Merge Join
(1)Nestloop Join:笛卡爾積
盡量杜絕Nesloop
(2)Hash Join
這是在關(guān)聯(lián)時候采用的一種很高效的方法,它先對其中一張關(guān)聯(lián)的表計算Hash值,在內(nèi)存中用一個散列表保存,然后對另外一張表進(jìn)行全表掃描,之后將每一行與這個散列表進(jìn)行關(guān)聯(lián)。對于散列表來說,在理想情況下,每一行的關(guān)聯(lián)都只有 O(1) 常數(shù)的消耗,從而使得表關(guān)聯(lián)達(dá)到很高的性能。在一般情況,gp都是使用這個關(guān)聯(lián)方式進(jìn)行等值連接的。
(3)Merge Join
這種方法是對兩張表都按照關(guān)聯(lián)字段進(jìn)行排序,然后按照排序好的內(nèi)容順序遍歷一遍,將相同的值連接起來,從而實現(xiàn)了連接。使用這種方法,最大的消耗是對兩張表進(jìn)行排序,快速排序至少也要 O(nlogn) 的時間復(fù)雜度。gp默認(rèn)將 MergeJoin 給關(guān)閉掉了。
5.7.4 分析函數(shù):開窗函數(shù)和grouping sets
1、開窗函數(shù)
對于如下的sql:
explain select
row_number()over(partition by offer_type order by join_from),
row_number()over(partition by member_id order by gmt_create)
from offer;
執(zhí)行計劃的圖示如圖5-6

這段sql代碼中有兩個開窗函數(shù)。開窗函數(shù)的實現(xiàn)與group by 相似,需要把分組(partition by) 的字段分不到一個節(jié)點上計算,這個表的分布鍵是offer_id,而offer_id不是開窗函數(shù)的分區(qū)字段,故都要將數(shù)據(jù)進(jìn)行重分布才能計算,步驟如下:
- 順序掃描appenonly的offer表
- 按照掃描member_id字段進(jìn)行重分布
- 對數(shù)據(jù)重分布之后按照member_id和gmt_create對其進(jìn)行排序,然后將排好順序的數(shù)據(jù)進(jìn)行編號,即完成這個row_number的開窗函數(shù)
- 再按照offer_type對數(shù)據(jù)進(jìn)行重分布,用同樣的方法計算另外一個開窗函數(shù)的值
由于分區(qū)字段不是分布鍵,所以數(shù)據(jù)全部都要重分布一遍,如果開窗函數(shù)太多,會導(dǎo)致數(shù)據(jù)重分布的次數(shù)非常多,每一次重分布每一個Segment都要發(fā)起一個進(jìn)程來處理,這會給操作系統(tǒng)和網(wǎng)絡(luò)都帶來一定的壓力,所以開窗函數(shù)盡量少用或者用分區(qū)鍵作為分布鍵,這樣也可以減少數(shù)據(jù)庫的消耗。
如果開窗函數(shù)是對整個數(shù)據(jù)進(jìn)行排序,沒有partition字段,那么為了維護(hù)一個全局的序列,所有數(shù)據(jù)都必須匯總到Master上進(jìn)行計算,然后再重新分發(fā)到每一個節(jié)點上,這個性能瓶頸會出現(xiàn)在Master上,效率會很差。

2、grouping sets
使用分析函數(shù)grouping sets、cube、rollup可進(jìn)行多維度分析,如下:
explain select a,b,count(1) from cxfa group by grouping sets((a),(b),(a,b));
其執(zhí)行計劃圖如圖5-7:

grouping sets的執(zhí)行步驟如下:
- 順序掃描cxfa表,然后將其保存在內(nèi)存中,之后分兩個分支進(jìn)行。
- 分支1:讀取在內(nèi)存中的數(shù)據(jù),按照a執(zhí)行GroupAggregate,計算出a字段匯總結(jié)果(a是分布鍵)
- 分支2:讀取內(nèi)存中的數(shù)據(jù),按照a、b執(zhí)行GroupAggregate,計算出這兩個字段的匯總結(jié)果,然后按照b字段重分布再計算出b字段的匯總結(jié)果。
- 將分支1與分支2的結(jié)果都進(jìn)行重分布,然后分別執(zhí)行HashAggregate1
- 將結(jié)果在Master上匯總起來。
5.8 案例
5.8.1 關(guān)聯(lián)鍵強(qiáng)制類型轉(zhuǎn)換,導(dǎo)致重分布
量表的關(guān)聯(lián)鍵id的類型都是一樣的,都是integer類型。如果強(qiáng)制將兩個integer類型轉(zhuǎn)換成其他類型,會導(dǎo)致兩個表都要重分布。
正常關(guān)聯(lián)的執(zhí)行計劃如下:


強(qiáng)制將兩個表的執(zhí)行計劃轉(zhuǎn)換成numeric之后的執(zhí)行計劃:

可以看出,由于兩個表剛開始的時候都是按照integer的類型進(jìn)行分布的,但是關(guān)聯(lián)的時候強(qiáng)制將類型轉(zhuǎn)換成numeric類型,由于integer與numeric的hash值是不一樣的,所以數(shù)據(jù)需要重分布到新的節(jié)點進(jìn)行關(guān)聯(lián)。
5.8.2 統(tǒng)計信息過期
當(dāng)統(tǒng)計信息過期的時候,會導(dǎo)致執(zhí)行計劃出錯。選擇一個糟糕的執(zhí)行計劃,會導(dǎo)致很大的數(shù)據(jù)庫開銷。
一般的解決辦法就是將表重新使用analyze分析一下,重新收集統(tǒng)計信息。或者使用 vacuum full analyze 對表中的空洞進(jìn)行回收,從而提高性能。
5.8.3 執(zhí)行計劃出錯
有時統(tǒng)計信息是正確的,但是由于信息不夠全面,或者執(zhí)行的優(yōu)化器還不夠精準(zhǔn),可能會是對結(jié)果集大小的估計有很大的偏差。
例如,在sql中加入一個無用的條件:id::integer&0=0;


以上兩個sql的數(shù)據(jù)量都是一樣的,但是執(zhí)行計劃看起來有很大的區(qū)別:

...
對于這種執(zhí)行計劃出錯,沒有很好的辦法,只能將sql拆分,物化一張新的表來實現(xiàn)。
create table offer_tmp as select * from offer_2 where id::integer&0=0;
通過物化,讓gp重新收集offer_tmp的信息,然后再與member表進(jìn)行關(guān)聯(lián),才能得到正確的執(zhí)行計劃。
5.8.4 分布鍵選擇不恰當(dāng)
分布鍵選擇不當(dāng)一般有兩種情況:
- 隨便選擇一個字段作為分布鍵,導(dǎo)致關(guān)聯(lián)的時候需要重分布一個表關(guān)聯(lián)
- 分布鍵導(dǎo)致數(shù)據(jù)分布不均,sql都卡在一個Segment上進(jìn)行計算
對于第一種情況,可以通過查詢執(zhí)行計劃來得知。當(dāng)執(zhí)行計劃出現(xiàn)Redistribute Motion或Broadcast Motion時,就知道重新分布了數(shù)據(jù),這個時候就要留意分布鍵選擇是否有誤,進(jìn)而導(dǎo)致多余的重分布,比如一個表用了字段id來分布,另外一個表通過id和name兩個字段來分布,然后通過id來進(jìn)行關(guān)聯(lián),測過時候也會導(dǎo)致數(shù)據(jù)重分布。
第二種情況,因為在執(zhí)行計劃中,看不出sql有什么問題,往往要到sql執(zhí)行非常慢的時候才意識到有問題。在數(shù)據(jù)分布不均中,有一個特例,就是空值,這是一個比較常見的問題(在第10章中詳細(xì)介紹如何排除這種問題)。
下面介紹幾個方法判斷表是否分布不均:
1、gp_segment_id
每個表都有一個隱藏字段gp_segment_id,表示數(shù)據(jù)是在哪個Segment上的,我們可以對這個字段進(jìn)行g(shù)roup by 來查看每個節(jié)點的數(shù)據(jù)量
select gp_segment_id ,count(1) from test01 group by 1 order by 1;
gp_segment_id | count
------------------------
0 | 3948
1 | 3576
2 | 5448
2、get_ao_distribution
對于appendonly表,還可以通過get_ao_distribution函數(shù)來獲取數(shù)據(jù)分布的信息
select * from get_ao_distribution('test01') order by 1;
segmentid | tupcount
---------------------
0 | 3948
1 | 3576
2 | 5448
3、all_seg_sql
通過這個是同,可以查看子節(jié)點上正在運(yùn)行的所有sql。
如果在數(shù)據(jù)庫中發(fā)現(xiàn)一條sql執(zhí)行了很長時間,但是在執(zhí)行計劃中看不出有什么問題,這時可以查看這條sql的sess_id,卷號通過這個sess_id,用下面的sql查詢所有節(jié)點sql的運(yùn)行情況。如果只發(fā)現(xiàn)其中小部分節(jié)點還在運(yùn)行,則表示大多數(shù)都是數(shù)據(jù)分布不均導(dǎo)致的。
select * from all_seg_sql where sess_id=xxxx;
還有很多中數(shù)據(jù)分布不均的情況很難發(fā)現(xiàn),如果sql比較復(fù)雜,可以查詢表是否分布均勻,但是由于有重分布,而對于gp來說,重分布并不會考慮數(shù)據(jù)是否均衡,因此會導(dǎo)致原表可能是分布均勻的,中間卻發(fā)生了重分布(關(guān)聯(lián)或者是聚合引起的)。這樣就更難定位到問題了,如果通過all_seg_sql觀察到有數(shù)據(jù)不均,那就要根據(jù)sql業(yè)務(wù)邏輯的理解或者將sql拆分成小的sql來進(jìn)行分析,看看到底是哪一步導(dǎo)致的數(shù)據(jù)分布不均的。
5.8.5 計算distinct
在sql中使用distinct一般有兩種辦法。
1、將全部數(shù)據(jù)按照使用distinct那個字段排序,然后執(zhí)行一個unique操作去掉重復(fù)的數(shù)據(jù),這樣的效率是比較差的
2、按照使用distinct哪個字段來計算hash值,然后放到一個hash數(shù)組中,同樣的值會得到相同的hash值,從而實現(xiàn)去重的功能。
從開銷來看,只使用了不到第一種執(zhí)行計劃1/10的開銷。
5.8.6. union 與union all
如果使用union,會進(jìn)行去重。在gp中,如果不是分布鍵,去重的就要涉及數(shù)據(jù)的重分布,而在gp中則更加特殊,因為這個去重是以鄭航數(shù)據(jù)為分布鍵的,這樣分布鍵很長,一般union的結(jié)果會插入到另外一張表中, 又會造成一次數(shù)據(jù)重分布,效率會較差。
從執(zhí)行計劃可以看到,gp會按照所有的字段作為key去重分布數(shù)據(jù),然后按照全部的字段去排序,再去重,從而實現(xiàn) union 的操作。
使用 union all 可能會造成不必要的數(shù)據(jù)重分布。在使用union all時,可以將前后查詢的數(shù)據(jù)都插入到一個臨時表中,以避免不必要的數(shù)據(jù)重分布。
5.8.7 子查詢 not in
not in在執(zhí)行計劃中都會使用笛卡爾積來執(zhí)行,效率極差,為了避免這種極差的執(zhí)行計劃,只能改寫sql來實現(xiàn)這種not in 的語法——使用 left join 去重后的表關(guān)聯(lián)來實現(xiàn)一樣的效果
5.8.8 聚合函數(shù)太多導(dǎo)致內(nèi)存不足
在gp 4.1 數(shù)據(jù)庫中,sql進(jìn)行很多的聚合運(yùn)算時,有時候會報如下的錯誤:
Error 7 (ERROR: Unexpected internal error:Segment process received signal SIGSEGV (postgre.c:3360) (seg43 slicel sdw19-4:30003 pid=26345) (cdbdisp.c:1457))
這段sql其實就是占用內(nèi)存太多,進(jìn)程被操作系統(tǒng)發(fā)出信號干擾導(dǎo)致的報錯。
查看執(zhí)行計劃,發(fā)現(xiàn)是HashAggregate搞的鬼。一般來說,數(shù)據(jù)庫會根據(jù)統(tǒng)計信息來了選擇HashAggregate或GroupAggregate,但是有可能統(tǒng)計信息不夠詳細(xì)或sql太復(fù)雜而選錯執(zhí)行計劃。
一般遇到這種問題,有兩種方法:
- 拆分成多個sql來執(zhí)行,減少HashAggregate使用的內(nèi)存
- 在執(zhí)行sql之前,先執(zhí)行 enable_hashagg=off,將HashAggregate參數(shù)關(guān)掉,強(qiáng)制不采用HashAggregate這種聚合方式,則數(shù)據(jù)庫會采用GroupAggregate,雖然增加了排序的代價但是內(nèi)存使用量是可控的,建議用這種方法,比較簡單。
5.9 小結(jié)
對于所有數(shù)據(jù)庫來說,學(xué)會閱讀執(zhí)行計劃,可以讓我們了解整個數(shù)據(jù)庫的運(yùn)行方式。對于sql調(diào)優(yōu)來說,執(zhí)行計劃是一個強(qiáng)有力的利器。
- 閱讀執(zhí)行計劃
- 統(tǒng)計信息對執(zhí)行計劃的影響
- 各種執(zhí)行計劃的原理
- 執(zhí)行計劃案例分析
gp與其他數(shù)據(jù)庫在執(zhí)行計劃上的最大區(qū)別就是廣播與重分布,而這兩個過程又嚴(yán)重依賴于統(tǒng)計信息的完整性,對于因統(tǒng)計信息不完善而導(dǎo)致的執(zhí)行計劃出錯,就需要將sql拆分來實現(xiàn)。gp的執(zhí)行計劃相對其他數(shù)據(jù)庫的執(zhí)行計劃更容易出錯,而且廣播大表有可能耗盡數(shù)據(jù)庫所有的資源,因此在分析執(zhí)行時間過長的sql時,應(yīng)當(dāng)首先從執(zhí)行計劃入手。