第五章 執行計劃詳解

gp 是基於 pgsql 開發的,其執行計劃大多是跟 pgsql 一樣的,但由於 gp 是分佈式並行數據庫,在 sql 執行上有很多 MPP 的痕跡,因此在理解 gp 的執行計劃時,一定要將其分佈式框架熟讀在心,從而能夠通過調整執行計劃給 sql 帶來很大的性能提升。

5.1 執行計劃入門

5.1.1 什麼是執行計劃

執行計劃就是數據庫運行 sql 的步驟,相當算法,讀懂 gp 的執行計劃,對理解 sql 的正確性即性能有很大的幫助。執行計劃時數據庫使用者瞭解數據庫內部結構的一個重要途徑。

5.1.2 查看執行計劃

跟 pgsql 一樣,gp 通過 explain 命令來查看執行計劃。具體語法如下:

EXPLAIN [ ANALYZE ] [ VERBOSE ] statement

各個參數的含義如下:

  • ANALYZE:執行命令並顯示實際運行時間。
  • VERBOSE:顯示規劃樹完整的內部表現形式,而不僅是一個摘要。通常,這個選項只是在特殊的調試過程中有用,VERBOSE 輸出是否打印工整的,具體取決於配置參數 explain_pretty_print 的值。
  • statement:查詢執行計劃的 SQL 語句,可以是任何 select、insert、update、delete、values、execute、declare 語句。

5.2 分佈式執行計劃概述

5.2.1 架構

圖5-1

圖5-1 很好地說明了 ShareNothing 的特點:

  • 底層的數據完全部共享。
  • 每個 Segment 只有一部分數據。
  • 每一個節點都通過網絡連接在一起。

5.2.2 重分佈於廣播

關聯數據在不同節點上,對於普通關係型數據庫來說,是無法進行連接的。關聯的數據需要通過網絡流入到一個節點中進行計算,這樣就需要發生數據遷移。數據遷移有廣播和重分佈兩種。

圖5-2

圖5-2 展示了 gp 中重分佈數據的實現。

在圖5-2中,兩個 Segment 分別進行計算,但由於其中一張表的關聯鍵與分佈鍵不一致,需要關聯的數據不在同一個節點上,所以在 SLICE1 上需要將其中一個表進行重分佈,可理解爲在每個節點之間互相交換數據。

關於廣播與重分佈,gp 有一個很重要的概念:Slice(切片)。每一個廣播或 重分佈會產生一個切片,每一個切片在每個數據節點上都會對應發起一個進行來處理該 Slice 負責的數據,上一層負責該 Slice 的進程會讀取下級 Slice 廣播或重分佈的數據,然後進行相應的計算。

由於在每個 Segment 上每一個 Slice 都會發起一個進程來處理,所以在 sql 中藥嚴格控制切片的個數,如果重分佈或者廣播太多,應適當將 sql 拆分,避免由於進程太多給數據庫或者是機器帶來太多的負擔。進程太多也比較容易導致 sql 失敗

圖5-3

Slice 之間如何交互可以從圖5-3中看出。

下面通過一個實際的數據形象地介紹數據在 Segment 中的切分。比方說,對一個成績表來說,分佈鍵是學號(sno),我們現在要按照成績(score)來執行 group by,那麼就需要將數據按照 score 字段進行重分佈,重分佈前會對每個 Segment 的數據進行局部彙總,重分佈後,同一個 score 的數據都在同一個 Segment 上,再進行一次彙總即可,數據的具體情況如圖5-4所示。

圖5-4

5.2.3 Greenplum Master 的工作

Master 在 sql 的執行過程中承擔着很多重要的工作,主要如下:

  • 執行計劃解析即分發。
  • 將子節點的數據彙集在一起。
  • 將所有 Segment 的有序數據進行歸併操作(歸併排序)。
  • 聚合函數在 Master 上進行最後的計算。
  • 需要有唯一的序列的功能(如開窗函數不帶 partition by 字句)。

舉個簡單的例子,在計算學生的平均分數時,在每個節點上先計算好 sum 和 count 值,然後再由 Master 彙總,再次進行少量計算,算出平均值,如圖5-5所示。

圖5-5

5.3 Greenplum 執行計劃中的術語

5.3.1 數據掃描方式

gp 掃描數據的方式有很多種,每一種掃描方式都有其特點:

(1)、Seq Scan:順序掃描

順序掃描在數據庫中是最常見,也是最簡單的一種方式,就是講一個數據文件從頭到尾讀取一次,這種方式非常符合磁盤的讀寫特性,順序讀寫,吞吐很高。對於分析性的語句,順序掃描基本上是對全表的所有數據進行分析計算,因此這一個方式非常有效。在數據倉庫中,絕大部分都是這種掃描方式,在 gp 中結合壓縮表一起使用,可以減少磁盤 IO 的損耗。

(2)、Index Scan:索引掃描

索引掃描是通過索引來定位數據的,一般對數據進行特定的篩選,篩選後的數據量比較小(對於整個表而言)。使用索引進行篩選,必須事先在篩選的字段上建立索引,查詢時先通過索引文件定位到實際數據在數據文件中的位置,再返回數據。對於磁盤而言,索引掃描都是隨機 IO,對於查詢小數據量而言,速度很快。

(3)、Bitmap Heap Scan:位圖堆表掃描

當索引定位到的數據在整表中佔比較大的時候,通過索引定位到的數據會使用位圖的方式對索引字段進行位圖堆表掃描,以確定結果數據的準確。對於數據倉庫應用而言,很少用這種掃描方式。

(4)、Tid Scan:通過隱藏字段 ctid 掃描

ctid 是pgsql 中標記數據位置的字段,通過這個字段來查找數據,速度非常快,類似於 oracle 的 rowid。gp 是 一個分佈式數據庫,每一個子節點都是一個pgsql 數據庫,每一個子節點都單獨維護自己的一套 ctid 字段。

如果在 gp 中通過 ctid 來找數據,會有如下的提示:

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.

就是說,如果想確定到具體一行數據,還必須通過制定另外一個隱藏字段(gp_segment_id)來確定取哪一個數據庫的 ctid 值。

select * from test1 where ctid='(1,1)' and gp_segment_id=1;

(5)、Subquery Scan '*SELECT*':子查詢掃描

只要 sql 中有子查詢,需要對子查詢的結果做順序掃描,就會進行子查詢掃描。

(6)、Function Scan:函數掃描

數據庫中有一些函數的返回值是一個結果集,數據庫從這個結果集中取出數據的時候,就會用到這個 Function Scan,順序獲取函數返回的結果集(這是函數掃描方式,不屬於表掃描方式),如:

explain select * from generate_series(1,10);

5.3.2 分佈式執行

(1) Gather Motion(N:1)

聚合操作,在 Master 上講子節點所有的數據聚合起來。一般的聚合規則是:哪一個子節點的數據線返回到 Master 上就將該節點的數據先放在 Master 上。

(2) Broadcast Motion(N:N)

廣播,將每個 Segment 上某一個表的數據全部發送給所有 Segment。這樣每一個 Segment 都相當於有一份全量數據,廣播基本只會出現在兩邊關聯的時候,相關內容再選擇廣播或者重分佈,5.7節中有詳細的介紹。

(3) Redistribute Motion(N:N)

當需要做跨庫關聯或者聚合的時候,當數據不能滿足廣播的條件,或者廣播的消耗過大時,gp 就會選擇重分佈數據,即數據按照新的分佈鍵(關聯鍵)重新打散到每個 Segment 上,重分佈一般在以下三種情況下回發生:

  • 關聯:將每個 Segment 的數據根據關聯鍵重新計算 hash 值,並根據 gp 的路由算法路由到目標子節點中,使關聯時屬於同一個關聯鍵的數據都在同一個 Segment 上。
  • group by :當表需要 group by ,但是 group by 的字段不是分佈鍵時,爲了使 group by 的字段在同一個庫中,gp 會分兩個 group by 操作來執行,首先,在單庫上執行一個 group by 操作,從而減少需要重分佈的數據量;然後將結果數據按照 group by 字段重分佈,之後在做啊聚合獲得最終結果。
  • 開窗函數:跟group by 類似,開窗函數(Window Function)的實現也需要將數據重分佈到每個節點上進行計算,不過其實現比 group by 更復雜一些。

(4) 切片(Slice)

gp 在實現分佈式執行計劃的時候,需要將 sql 拆分成多個切片(Slice),每一個 Slice 其實是單庫執行的一部分 sql,上面描述的每一個 motion 都會導致 gp 多一個 Slice 操作,而每一個 Slice 操作子節點都會發起一個進程來處理數據。

所以應該儘量控制 Slice 的個數,將太複雜的 sql 拆分,減少進程數,在執行計劃中,最常見的 Slice 關鍵字的地方就是廣播跟重分佈,如下:

Broadcast Motion 6:6 (slice1)
Gather Motion 6:1 (slice1)

5.3.3 兩種聚合方式

HashAggregate 和 GroupAggregate 這兩種聚合方式在 5.7 介紹執行原理時會給出詳細的講解,這裏主要從佔用內存方面簡單介紹:

(1) HashAggregate

對於 Hash 聚合來說,數據庫會根據 group by 字段後面的值計算 hash 值,並根據前面使用是的聚合函數在內存中維護對應的列表,然後數據庫會通過這個列表來實現聚合操作,效率相對較高。

(2) GroupAggregate

對於普通聚合函數,使用 group 聚合,其原理是先將表中的數據按照 group by 的字段排序,這樣同一個 group by 的值就在一起,只需要對排好序的數據進行一次全掃描就可以得到聚合的結果。

5.3.4 關聯

gp 中的關聯的實現比較多,有 Hash Join、NestLoop、Merge Join,實現方式跟普通的 pgsql 數據庫方式一樣。由於 gp 是分佈式的,所以關聯可能會涉及表的廣播或重分佈。下面通過實際的執行計劃來分析這 3 中關聯在 gp 上的簡單實現,首先建立兩張表以方便我們查看後面的執行計劃:

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 關聯) 是一種很搞笑的關聯方式,簡單地說,其實現原理就是講一張關聯表按照關聯鍵在內存中建立哈希表,在關聯的時候通過哈希的方式來處理。

下面是一個 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 的方式來實現左連接,在執行計劃中的體現就是 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 關聯是最簡單,也是最低效的關聯方式,但是在有些情況下,不得不使用 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 關聯。在 gp 中,如果採取 NestedLoop,關聯的兩張表中有一張表必須廣播,否則無法關聯,一般是數據量比較小的表會廣播。

4. Merge Join 和 Merge Left Join

Merge Join 也是量表關聯中比較常見的關聯方式,這種關聯方式需要將兩張表按照關聯鍵進行排序,然後按照歸併排序的方式將數據進行關聯,效率比 Hash Join 差。

下面的例子先通過設置兩個參數來強制執行計劃,採取的是 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 的肯定是兩張表關聯鍵的排序。

5. Merge Full Join

如果關聯使用的是 full outer join,則執行計劃使用的是 Merge Full Join。在 gp 中其他的關聯方式都無法進行全關聯。

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 的實現方式是對 a 和 b 做一個左外關聯,然後對 b 和 a 做一個反連接(在關聯時,匹配的剔除,不匹配的保留),再對兩個結果直接進行 union all 操作。但是在 gp 中沒有執行這個優化,所有隻能採取 Merge Join。Nest咯哦片只能用於內連接,對外連接無能爲力。

6. Hash EXISTS Join

關聯子查詢 exist 之類的 sql 會被改寫成 inner join,如果 sql 被改寫了,則會出現 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 的執行計劃中,每一步都會有(cost=0.01.。0.05 rows=3 width=150)折3項表示 sql 的消耗,這三個字段的含義:

(1) Cost

以數據庫自定義的消耗單位,通過統計信息來估計 sql 的消耗。具體消耗的單位可以參考 pgsql 的官方文檔: http://www.pgsqldb.org/pgsqldoc-8.1c/runtime-config-query.html

(2) Rows

根據統計信息估計 sql 返回結果集的行數。

(3) Width

返回結果集每一行的長度,這個長度值是根據 pg_statistic 表中的統計信息來計算的。

5.3.6 其他術語

(1) Filter 過濾

where 條件中的篩選條件,在執行計劃中就是 Filter 關鍵字。

Filter: relfilenode = 1249::oid

(2)Index Cond

如果在查詢的表中 where 篩選的字段中有阿銀,那麼執行計劃會通過索引定位,提高查詢的效率。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 協議,爲了保證結果的正確性,要重新檢查一下過濾條件。

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

執行 Hash Join 的時候的關聯條件:

-> Hash Join (cost=40.87..119.60 rows=1683 width=24)
    Hash Cond: y.b = x.a

(5)Merge

在執行排序操作時數據會在子節點上各自排好序然後在 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

在數據重分佈時候指定的重算 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

將數據保存在內存中,避免多次掃描磁盤帶來的開銷。這個要重點注意,由於將數據保存在內存中,會佔用很大的內存,而執行計劃時按照統計信息來計算的,如果統計信息丟失或者錯誤,有可能會將一張很大的表保存在內存中,直接導致內存不足,進而導致 sql 執行失敗:

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

對數據關聯後再進行篩選,如:

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

如果執行計劃中出現了 Sort 關鍵字,則說明有排序的操作,排序的字段爲: 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

這個出使用開窗函數(Window Function)時,執行計劃顯示了使用分析函數的標識:

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

當在 sql 只取前幾行時,就使用 Limit 語句:

Limit (cost=0.00..0.85 rows=1 width=205)

(12)Append

將結果直接彙總起來:

-> 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 數據庫統計信息收集

gp 與 oracle 等數據庫一樣,都是根據 CBO 優化器來選擇一個好的執行計劃的,尤其是在識別廣播或者重分佈的時候,統計信息十分重要,其準確與否直接決定了執行計劃的好壞。

5.4.1 Analyze 分析

統計信息的命令如下:

ANALYZE [ VERBOSE ] [ table [ (column [, ...] ) ] ]

如果沒有參數,ANALYZE 檢查當前數據庫中所有表。如果有參數,則只檢查參數指定的那個表。還可以給出一列字段名字,則只收集給出的字段的統計信息。

ANALYZE收集表內容的統計信息,表級別的信息(表的數據量及表大小)保存在 pg_class 中的 reltuples 和 relpages 字段中,然後把字段級別的結果保存在系統表 pg_statistic 中,字段級信息通常包括每個字段最常用數值的列表以及顯示每個字段中數據近似分佈的包線圖。

在GP中會對錶自動進行統計信息收集。控制自動收集的參數是 gp_autostats_mode,這個參數有三個值:none、on_change、on_no_stats。

1

這個參數在Master上修改,然後通過 gpstop -u 重新加載 postgresql.conf 這個配置文件即可。默認on_no_stats。

5.4.2 固定執行計劃

greenplum是通過統計信息來生成執行計劃的。

一般對執行計劃影響最大的是 pg_class 的 relpages 和 reltuples 這兩個字段,reltuples是表的數據量,relpages則表示表大小除以32k,即:

select pg_relation_size(tablename)/32/1024;

5.5 控制執行計劃的參數介紹

在gp 中,控制執行計劃的參數都是在會話級別,對於同一會話的所有sql生效。

這些配置參數提供了查詢優化器選擇查詢規劃的原始方法。如果優化器爲特定的查詢選擇的默認規劃並不是最優,那麼我們就可以通過使用這些配置參數強制優化器選擇一個更好的規劃來臨時解決這個問題。這些參數一般是在某個會話級別對某個sql進行設置的,不建議在全局中修改,會影響其他正常sql的執行計劃。

2

3

5.6 規劃期開銷的計算方法

在選擇合理的執行計劃的時候,gp 會遍歷所有的執行計劃,計算其開銷,即cost值,並選擇最小的執行路徑執行sql。

一般,gp/pgsql 以抓取順序頁的開銷作爲基準單位,也就是說將 seq_page_cost 設爲1.0,同時其他開銷參數對照它來設置。

表5-3 是gp中衡量數據庫消耗的各個變量及默認參數。

4

減小 random_page_cost 值(相對於 seq_page_cost)將導致更傾向於使用索引掃描,而增加這個值導致更傾向於使用順序掃描。可以通過同時增加或減少這兩個值來調整磁盤 I/O 相對於 CPU的開銷。

5.7 各種執行計劃原理分析

5.7.1 詳解關聯的廣播與重分佈

分佈式的關聯有兩種:

  • 單庫關聯:關聯鍵與分佈鍵一致,只需要在單個庫關聯後得到結果即可
  • 跨庫關聯:關聯鍵與分佈鍵不一致,數據需要重新分佈,轉換成單庫關聯,從而實現表的關聯。

5

1、內連接

情況1

select * from A,B where A.id=B.id;

分佈鍵與關聯鍵相同,屬於單庫關聯,不會造成廣播或者重分佈。

情況2

select * from  A,B where A.id=B.id2;

表A的關聯鍵是分佈鍵,表B的關聯鍵不是分佈鍵,那麼可以通過兩種方法來實現表的關聯:

  • 將表B按照id2字段將數據重分佈到每一個節點上,然後再與表A進行關聯。重分佈的數據量是N
  • 將表A廣播,每一個節點都放一份全量數據,然後再與表B關聯得到結果。廣播的數據量是 M * 節點數

所以當N>M * 節點數 的時候,選擇表A廣播,否則選擇表B重分佈。

情況3

select * from A,B where A.id2=B.id2;

對於這種情況,兩個表的關聯鍵及分佈鍵都不一樣,那麼還有兩種做法:

  • 將表A與表B都按照id2字段,將數據重分佈到每個節點,重分佈的代價是M+N
  • 將其中一個表廣播後再關聯,當然選取小表廣播,代價小,廣播的代價是 min(M,N) * 節點數

所以,當 N + M > min(M,N) * 節點數 的時候,選擇小表廣播,否則選擇兩個表都重分佈。

2、左連接

情況1:

select * from A left join B on A.id=B.id;

單庫關聯,不涉及數據跨庫關聯

情況2

select * from A left join B on A.id=B.id2;

由於左表的分佈鍵是關聯鍵,鑑於左連接的性質,無論表B數據量多大,都必須將表B按照字段id2重分佈數據。

情況3

select * from A left join B on A.id2=B.id;

左表的關聯鍵不是分佈鍵,由於左連接A表肯定是不能被廣播的,所以有兩種方式:

  • 將表A按照id2重分佈數據,轉換成情況A,代價爲M
  • 將表B廣播,代價爲 N * 節點數

情況4

select * from A left join B on A.id2=B.id2;
  • 將表A與表B都哦據考id2字段將數據重分佈一遍,轉換成情況1,代價是M+N
  • 表A不能被廣播,只能將表B廣播,代價是 N * 節點數

對於有多種情況,gp總是選擇代價小的方式來執行sql

3、全連接:

情況1

select * from A full outer join B on A.id=B.id;

對於關聯鍵都是分佈鍵的情況,在gp中全連接只能採用Merger Join來實現

情況2

select * from A full outer join B on A.id = B.id2;

將不是關聯鍵不是分佈鍵的才重分佈數據,轉換成情況1來解決。無論A、B大小分別爲多少,爲了實現全連接,不能將表廣播,只能是重分佈。

情況3

select * from A full outer join B on A.id2=B.id2;

將兩張表都重分佈,轉換成情況1進行處理。

5.7.2 HashAggregate 與 GroupAggregate

在pgsql/gp 數據庫中,聚合函數有兩種實現方式:HashAggregate 與 GroupAggregate。

案例:

select count(1) from pg_class group by oid;

1、兩種實現算法的比較

(1)HashAggregate

對於hash聚合來說,數據庫會根據group by字段後面的值算出hash值,並根據前面使用的聚合函數在內存中維對應的列表。如果select後面有兩種聚合函數,那麼在內存中就會維護兩個對應的數據。同樣的,有n個聚合函數就會維護n個同樣的數組。對於hash聚合來說,數組的長度肯定是大於group by 的字段的distinct值的個數的,且與這個值應該呈線性關係,group by 後面的字段重複值越少,使用的內存也就越大。

執行計劃如下:

6

(2)GroupAggregate

對於普通聚合函數,使用GroupAggregate,其原理是先將表中的數據按照group by 的字段排序,這樣同一個group by 的值就在一起,只需要對排好序的數據進行一次全掃描,並進行對應的聚合函數的計算,就可以得到聚合的結果

7

從上面兩個執行計劃的消耗來說,GroupAggregate由於需要排序,效率很差,消耗是HashAggregate的7倍,所以在gp中,對於聚合函數的使用,採用的都是HashAggregate。

2、兩種實現的內存消耗

結論:

HashAggregate 在少數聚合函數時表現優異,但是對於很多聚合函數的情況,性能和消耗的內存差異很明顯。尤其是受group by 字段唯一性的影響,字段count(district)值越大,HashAggregate消耗的內存越多,性能下降越明顯。

所以在SQL中有大量聚合函數group by的字段重複值比較少的時候,應該用GroupAggregate,而不能用HashAggregate。

5.7.3 Nestloop Join、Hash Join與Merge Join

(1)Nestloop Join:笛卡爾積

儘量杜絕Nesloop

(2)Hash Join

這是在關聯時候採用的一種很高效的方法,它先對其中一張關聯的表計算Hash值,在內存中用一個散列表保存,然後對另外一張表進行全表掃描,之後將每一行與這個散列表進行關聯。對於散列表來說,在理想情況下,每一行的關聯都只有 O(1) 常數的消耗,從而使得表關聯達到很高的性能。在一般情況,gp都是使用這個關聯方式進行等值連接的。

(3)Merge Join

這種方法是對兩張表都按照關聯字段進行排序,然後按照排序好的內容順序遍歷一遍,將相同的值連接起來,從而實現了連接。使用這種方法,最大的消耗是對兩張表進行排序,快速排序至少也要 O(nlogn) 的時間複雜度。gp默認將 MergeJoin 給關閉掉了。

5.7.4 分析函數:開窗函數和grouping sets

1、開窗函數

對於如下的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;

執行計劃的圖示如圖5-6

8

這段sql代碼中有兩個開窗函數。開窗函數的實現與group by 相似,需要把分組(partition by) 的字段分不到一個節點上計算,這個表的分佈鍵是offer_id,而offer_id不是開窗函數的分區字段,故都要將數據進行重分佈才能計算,步驟如下:

  1. 順序掃描appenonly的offer表
  2. 按照掃描member_id字段進行重分佈
  3. 對數據重分佈之後按照member_id和gmt_create對其進行排序,然後將排好順序的數據進行編號,即完成這個row_number的開窗函數
  4. 再按照offer_type對數據進行重分佈,用同樣的方法計算另外一個開窗函數的值

由於分區字段不是分佈鍵,所以數據全部都要重分佈一遍,如果開窗函數太多,會導致數據重分佈的次數非常多,每一次重分佈每一個Segment都要發起一個進程來處理,這會給操作系統和網絡都帶來一定的壓力,所以開窗函數儘量少用或者用分區鍵作爲分佈鍵,這樣也可以減少數據庫的消耗。

如果開窗函數是對整個數據進行排序,沒有partition字段,那麼爲了維護一個全局的序列,所有數據都必須彙總到Master上進行計算,然後再重新分發到每一個節點上,這個性能瓶頸會出現在Master上,效率會很差。

9

2、grouping sets

使用分析函數grouping sets、cube、rollup可進行多維度分析,如下:

explain select a,b,count(1) from cxfa group by grouping sets((a),(b),(a,b));

其執行計劃圖如圖5-7:

10

grouping sets的執行步驟如下:

  1. 順序掃描cxfa表,然後將其保存在內存中,之後分兩個分支進行。
  2. 分支1:讀取在內存中的數據,按照a執行GroupAggregate,計算出a字段彙總結果(a是分佈鍵)
  3. 分支2:讀取內存中的數據,按照a、b執行GroupAggregate,計算出這兩個字段的彙總結果,然後按照b字段重分佈再計算出b字段的彙總結果。
  4. 將分支1與分支2的結果都進行重分佈,然後分別執行HashAggregate1
  5. 將結果在Master上彙總起來。

5.8 案例

5.8.1 關聯鍵強制類型轉換,導致重分佈

量表的關聯鍵id的類型都是一樣的,都是integer類型。如果強制將兩個integer類型轉換成其他類型,會導致兩個表都要重分佈。

正常關聯的執行計劃如下:

11

12

強制將兩個表的執行計劃轉換成numeric之後的執行計劃:

13

可以看出,由於兩個表剛開始的時候都是按照integer的類型進行分佈的,但是關聯的時候強制將類型轉換成numeric類型,由於integer與numeric的hash值是不一樣的,所以數據需要重分佈到新的節點進行關聯。

5.8.2 統計信息過期

當統計信息過期的時候,會導致執行計劃出錯。選擇一個糟糕的執行計劃,會導致很大的數據庫開銷。

一般的解決辦法就是將表重新使用analyze分析一下,重新收集統計信息。或者使用 vacuum full analyze 對錶中的空洞進行回收,從而提高性能。

5.8.3 執行計劃出錯

有時統計信息是正確的,但是由於信息不夠全面,或者執行的優化器還不夠精準,可能會是對結果集大小的估計有很大的偏差。

例如,在sql中加入一個無用的條件:id::integer&0=0;

14

15

以上兩個sql的數據量都是一樣的,但是執行計劃看起來有很大的區別:

16

...

對於這種執行計劃出錯,沒有很好的辦法,只能將sql拆分,物化一張新的表來實現。

create table offer_tmp as select * from offer_2 where id::integer&0=0;

通過物化,讓gp重新收集offer_tmp的信息,然後再與member表進行關聯,才能得到正確的執行計劃。

5.8.4 分佈鍵選擇不恰當

分佈鍵選擇不當一般有兩種情況:

  • 隨便選擇一個字段作爲分佈鍵,導致關聯的時候需要重分佈一個表關聯
  • 分佈鍵導致數據分佈不均,sql都卡在一個Segment上進行計算

對於第一種情況,可以通過查詢執行計劃來得知。當執行計劃出現Redistribute Motion或Broadcast Motion時,就知道重新分佈了數據,這個時候就要留意分佈鍵選擇是否有誤,進而導致多餘的重分佈,比如一個表用了字段id來分佈,另外一個表通過id和name兩個字段來分佈,然後通過id來進行關聯,測過時候也會導致數據重分佈。

第二種情況,因爲在執行計劃中,看不出sql有什麼問題,往往要到sql執行非常慢的時候才意識到有問題。在數據分佈不均中,有一個特例,就是空值,這是一個比較常見的問題(在第10章中詳細介紹如何排除這種問題)。

下面介紹幾個方法判斷表是否分佈不均:

1、gp_segment_id

每個表都有一個隱藏字段gp_segment_id,表示數據是在哪個Segment上的,我們可以對這個字段進行group by 來查看每個節點的數據量

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函數來獲取數據分佈的信息

select * from get_ao_distribution('test01') order by 1;

segmentid | tupcount
---------------------
0 | 3948
1 | 3576
2 | 5448

3、all_seg_sql

通過這個是同,可以查看子節點上正在運行的所有sql。

如果在數據庫中發現一條sql執行了很長時間,但是在執行計劃中看不出有什麼問題,這時可以查看這條sql的sess_id,卷號通過這個sess_id,用下面的sql查詢所有節點sql的運行情況。如果只發現其中小部分節點還在運行,則表示大多數都是數據分佈不均導致的。

select * from all_seg_sql where sess_id=xxxx;

還有很多中數據分佈不均的情況很難發現,如果sql比較複雜,可以查詢表是否分佈均勻,但是由於有重分佈,而對於gp來說,重分佈並不會考慮數據是否均衡,因此會導致原表可能是分佈均勻的,中間卻發生了重分佈(關聯或者是聚合引起的)。這樣就更難定位到問題了,如果通過all_seg_sql觀察到有數據不均,那就要根據sql業務邏輯的理解或者將sql拆分成小的sql來進行分析,看看到底是哪一步導致的數據分佈不均的。

5.8.5 計算distinct

在sql中使用distinct一般有兩種辦法。

1、將全部數據按照使用distinct那個字段排序,然後執行一個unique操作去掉重複的數據,這樣的效率是比較差的

2、按照使用distinct哪個字段來計算hash值,然後放到一個hash數組中,同樣的值會得到相同的hash值,從而實現去重的功能。

從開銷來看,只使用了不到第一種執行計劃1/10的開銷。

5.8.6. union 與union all

如果使用union,會進行去重。在gp中,如果不是分佈鍵,去重的就要涉及數據的重分佈,而在gp中則更加特殊,因爲這個去重是以鄭航數據爲分佈鍵的,這樣分佈鍵很長,一般union的結果會插入到另外一張表中, 又會造成一次數據重分佈,效率會較差。

從執行計劃可以看到,gp會按照所有的字段作爲key去重分佈數據,然後按照全部的字段去排序,再去重,從而實現 union 的操作。

使用 union all 可能會造成不必要的數據重分佈。在使用union all時,可以將前後查詢的數據都插入到一個臨時表中,以避免不必要的數據重分佈。

5.8.7 子查詢 not in

not in在執行計劃中都會使用笛卡爾積來執行,效率極差,爲了避免這種極差的執行計劃,只能改寫sql來實現這種not in 的語法——使用 left join 去重後的表關聯來實現一樣的效果

5.8.8 聚合函數太多導致內存不足

在gp 4.1 數據庫中,sql進行很多的聚合運算時,有時候會報如下的錯誤:

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其實就是佔用內存太多,進程被操作系統發出信號干擾導致的報錯。

查看執行計劃,發現是HashAggregate搞的鬼。一般來說,數據庫會根據統計信息來了選擇HashAggregate或GroupAggregate,但是有可能統計信息不夠詳細或sql太複雜而選錯執行計劃。

一般遇到這種問題,有兩種方法:

  1. 拆分成多個sql來執行,減少HashAggregate使用的內存
  2. 在執行sql之前,先執行 enable_hashagg=off,將HashAggregate參數關掉,強制不採用HashAggregate這種聚合方式,則數據庫會採用GroupAggregate,雖然增加了排序的代價但是內存使用量是可控的,建議用這種方法,比較簡單。

5.9 小結

對於所有數據庫來說,學會閱讀執行計劃,可以讓我們瞭解整個數據庫的運行方式。對於sql調優來說,執行計劃是一個強有力的利器。

  • 閱讀執行計劃
  • 統計信息對執行計劃的影響
  • 各種執行計劃的原理
  • 執行計劃案例分析

gp與其他數據庫在執行計劃上的最大區別就是廣播與重分佈,而這兩個過程又嚴重依賴於統計信息的完整性,對於因統計信息不完善而導致的執行計劃出錯,就需要將sql拆分來實現。gp的執行計劃相對其他數據庫的執行計劃更容易出錯,而且廣播大表有可能耗盡數據庫所有的資源,因此在分析執行時間過長的sql時,應當首先從執行計劃入手。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章