java执行查询postgresql得到中文乱码_Greenplum: 基于PostgreSQL的分布式数据库内核揭秘(上篇)...
關于作者
姚延棟,山東大學本科,中科院軟件所研究生。PostgreSQL中文社區委員,致力于Greenplum/PostgreSQL開源數據庫產品、社區和生態的發展。
一、數據庫內核揭秘
Greenplum 是最成熟的開源分布式分析型數據庫(今年6月份預計發布的 Greenplum 6 之OLTP性能大幅提升,將成為一款真正的HTAP數據庫,評測數據將于近期發布),Gartner 2019 最新評測顯示 Greenplum 在經典數據分析領域位列全球第三,在實時數據分析領域位列并列第四。兩個領域中前十名中唯一一款開源數據庫產品。這意味著如果選擇一款基于開源的產品,前十名中別無選擇,唯此一款。Gartner 報告原文。
那么 Greenplum 分布式數據庫是如何煉成?眾所周知 Greenplum 基于 PostgreSQL。PostgreSQL 是最先進的單節點數據庫,其相關內核文檔、論文資源很多。而有關如何將單節點 PostgreSQL 改造成分布式數據庫的資料相對較少。本文從6個方面介紹將單節點 PostgreSQL 數據庫發展成分布式 MPP 數據庫所涉及的主要工作。當然這些僅僅是極簡概述,做到企業級產品化耗資數億美元,百人規模的數據庫尖端人才團隊十幾年的研發投入結晶而成。
雖然不是必需,然而了解 PostgreSQL 基本內核知識對理解本文中的一些細節有幫助。Bruce Momjian 的PPT是極佳入門資料。
二、?Greenplum 集群化概述
PostgreSQL 是世界上最先進的單機開源數據庫。Greenplum 基于PostgreSQL,是世界上最先進的開源MPP數據庫 (有關Greenplum更多資訊請訪問Greenplum中文社區)。從用戶角度來看,Greenplum 是一個完備的關系數據庫管理系統(RDBMS)。從物理層面,它內含多個 PostgreSQL 實例,這些實例可以單獨訪問。為了實現多個獨立的 PostgreSQL 實例的分工和合作,呈現給用戶一個邏輯的數據庫,Greenplum 在不同層面對數據存儲、計算、通信和管理進行了分布式集群化處理。Greenplum 雖然是一個集群,然而對用戶而言,它封裝了所有分布式的細節,為用戶提供了單個邏輯數據庫。這種封裝極大的解放了開發人員和運維人員。
把單節點 PostgreSQL 轉化成集群涉及多個方面的工作,本文主要介紹數據分布、查詢計劃并行化、執行并行化、分布式事務、數據洗牌(shuffle)和管理并行化等6個方面。
Greenplum 在 PostgreSQL之上還添加了大量其他功能,例如 Append-Optimized 表、列存表、外部表、多級分區表、細粒度資源管理器、ORCA 查詢優化器、備份恢復、高可用、故障檢測和故障恢復、集群數據遷移、擴容、MADlib機器學習算法庫、容器化執行UDF、PostGIS擴展、GPText套件、監控管理、集成Kubernetes等。
下圖展示了一個 Greenplum 集群的俯瞰圖,其中一個master節點,兩個segment節點,每個segment節點上部署了4個segment實例以提高資源利用率。每個實例,不管是master實例還是segment實例都是一個物理上獨立的 PostgreSQL 數據庫。
三、分布式數據存儲
數據存儲分布化是分布式數據庫要解決的第一個問題。分布式數據存儲基本原理相對簡單,實現比較容易,很多數據庫中間件也可以做到基本的分布式數據存儲。Greenplum 在這方面不單單做到了基本的分布式數據存儲,還提供了很多更高級靈活的特性,譬如多級分區、多態存儲。Greenplum 6進一步增強了這一領域,實現了一致性哈希和復制表,并允許用戶根據應用干預數據分布方法。
如下圖所示,用戶看到的是一個邏輯數據庫,每個數據庫有系統表(例如pg_catalog下面的pg_class, pg_proc 等)和用戶表(下例中為sales表和customers表)。在物理層面,它有很多個獨立的數據庫組成。每個數據庫都有它自己的一份系統表和用戶表。master 數據庫僅僅包含元數據而不保存用戶數據。master 上仍然有用戶數據表,這些用戶數據表都是空表,沒有數據。優化器需要使用這些空表進行查詢優化和計劃生成。segment 數據庫上絕大多數系統表(除了少數表,例如統計信息相關表)和master上的系統表內容一樣,每個segment都保存用戶數據表的一部分。
在 Greenplum 中,用戶數據按照某種策略分散到不同節點的不同segment實例中。每個實例都有自己獨立的數據目錄,以磁盤文件的方式保存用戶數據。使用標準的 INSERT SQL 語句可以將數據自動按照用戶定義的策略分布到合適的節點,然而INSERT性能較低,僅適合插入少量數據。Greenplum 提供了專門的并行化數據加載工具以實現高效數據導入,詳情可以參考 gpfdist 和 gpload 的官方文檔。此外 Greenplum 還支持并行 COPY,如果數據已經保存在每個 segment 上,這是最快的數據加載方法。下圖形象的展示了用戶的 sales 表數據被分布到不同的segment實例上。
除了支持數據在不同的節點間水平分布,在單個節點上Greenplum 還支持按照不同的標準分區,且支持多級分區。Greenplum 支持的分區方法有:
范圍分區:根據某個列的時間范圍或者數值范圍對數據分區。譬如以下 SQL 將創建一個分區表,該表按天分區,從 2016-01-01 到 2017-01-01 把全部一年的數據按天分成了366個分區:
CREATE TABLE sales (id int, date date, amt decimal(10,2))DISTRIBUTED BY (id)PARTITION BY RANGE (date)( START (date '2016-01-01') INCLUSIVEEND (date '2017-01-01') EXCLUSIVE EVERY (INTERVAL '1 day') );
●?列表分區:按照某個列的數據值列表,將數據分不到不同的分區。譬如以下 SQL 根據性別創建一個分區表,該表有三個分區:一個分區存儲女士數據,一個分區存儲男士數據,對于其他值譬如NULL,則存儲在單獨 other 分區。
CREATE TABLE rank (id int, rank int, year int, gender char(1), count int ) DISTRIBUTED BY (id)PARTITION BY LIST (gender)( PARTITION girls VALUES ('F'), PARTITION boys VALUES ('M'), DEFAULT PARTITION other );
下圖展示了用戶的 sales 表首先被分布到兩個節點,然后每個節點又按照某個標準進行了分區。分區的主要目的是實現分區裁剪以通過降低數據訪問量來提高性能。分區裁剪指根據查詢條件,優化器自動把不需要訪問的分區過濾掉,以降低查詢執行時的數據掃描量。PostgreSQL 支持靜態條件分區裁剪,Greenplum 通過 ORCA 優化器實現了動態分區裁剪。動態分區裁剪可以提升十幾倍至數百倍性能。
Greenplum支持多態存儲,即單張用戶表,可以根據訪問模式的不同使用不同的存儲方式存儲不同的分區。通常不同年齡的數據具有不同的訪問模式,不同的訪問模式有不同的優化方案。多態存儲以用戶透明的方式為不同數據選擇最佳存儲方式,提供最佳性能。Greenplum 提供以下存儲方式:
●?堆表(Heap Table):堆表是 Greenplum 的默認存儲方式,也是 PostgreSQL 的存儲方式。支持高效的更新和刪除操作,訪問多列時速度快,通常用于 OLTP 型查詢。
●?Append-Optimized 表:為追加而專門優化的表存儲模式,通常用于存儲數據倉庫中的事實表。不適合頻繁的更新操作。
●?AOCO (Append-Optimized, Column Oriented) 表:AOCO 表為列表,具有較好的壓縮比,支持不同的壓縮算法,適合訪問較少的列的查詢場景。
●?外部表:外部表的數據存儲在外部(數據不被Greenplum管理),Greenplum 中只有外部表的元數據信息。Greenplum 支持很多外部數據源譬如 S3、HDFS、文件、Gemfire、各種關系數據庫等和多種數據格式譬如 Text、CSV、Avro、Parquet 等。
如下圖所示,假設前面提到的 sales 表按照月份分區,那么可以采用不同的存儲策略保存不同時間的數據,例如最近三個月的數據使用堆表(Heap)存儲,更老的數據使用列存儲,一年以前的數據使用外部表的方式存儲在 S3 或者HDFS中。
數據分布是任何 MPP 數據庫的基礎,也是 MPP 數據庫是否高效的關鍵之一。通過把海量數據分散到多個節點上,一方面大大降低了單個節點處理的數據量,另一方面也為處理并行化奠定了基礎,兩者結合起來可以極大的提高整個系統的性能。譬如在一百個節點的集群上,每個節點僅保存總數據量的百分之一,一百個節點同時并行處理,性能會是單個配置更強節點的幾十倍。如果數據分布不均勻出現數據傾斜,受短板效應制約,整個系統的性能將會和最慢的節點相同。因而數據分布是否合理對 Greenplum 整體性能影響很大。
Greenplum 6 提供了以下數據分布策略。
●?哈希分布
●?隨機分布
●?復制表(Replicated Table)
Hash 分布
哈希分布是 Greenlum 最常用的數據分布方式。根據預定義的分布鍵計算用戶數據的哈希值,然后把哈希值映射到某個 segment 上。 分布鍵可以包含多個字段。分布鍵選擇是否恰當是 Greenplum 能否發揮性能的主要因素。好的分布鍵將數據均勻分布到各個 segment 上,避免數據傾斜。
Greenplum 計算分布鍵哈希值的代碼在 cdbhash.c 中。結構體 CdbHash 是處理分布鍵哈希的主要數據結構。 計算分布鍵哈希值的邏輯為:
●?使用 makeCdbHash(int segnum) 創建一個 CdbHash 結構體
●?然后對每個 tuple 執行下面操作,計算該 tuple 對應的哈希值,并確定該tuple應該分布到哪個segment上:
○?cdbhashinit():執行初始化操作
○?cdbhash(), ?這個函數會調用 hashDatum() 針對不同類型做不同的預處理,最后 addToCdbHash() 將處理后的列值添加到哈希計算中
○?cdbhashreduce() 映射哈希值到某個 segment
CdbHash 結構體:
typedef struct CdbHash
{
????uint32 ???hash; ????????????????????????/* 哈希結果值 */
????int ?????????numsegs; ?????????????????/* segment 的個數 ?*/
????CdbHashReduce reducealg; ??/* ?用于減少桶的算法 ?*/
????uint32 ???rrindex; ?????????????????????/* 循環索引 */
} CdbHash;
主要的函數
●?makeCdbHash(int numsegs): 創建一個 CdbHash 結構體,它維護了以下信息:
○?Segment 的個數
○?Reduction 方法
■?如果segment 個數是2的冪,則使用 REDUCE_BITMASK,否則使用 REDUCE_LAZYMOD.
○?結構體內的 hash 值將會為每個 tuple 初始化,這個操作發生在 cdbhashinit() 中。
●?void cdbhashinit(CdbHash *h)
h->hash = FNV1_32_INIT; 重置hash值為初始偏移基礎量
●?void cdbhash(CdbHash *h, Datum datum, Oid type): ?添加一個屬性到 CdbHash 計算中,也就是添加計算hash時考慮的一個屬性。 這個函數會傳入函數指針: addToCdbHash。
●?void addToCdbHash(void *cdbHash, void *buf, size_t len); 實現了 datumHashFunction
h->hash = fnv1_32_buf(buf, len, h->hash); ???// 在緩沖區執行 32 位 FNV 1 哈希
通常調用路徑是: evalHashKey -> cdbhash -> hashDatum -> addToCdbHash
●?unsigned int cdbhashreduce(CdbHash *h): 映射哈希值到某個 segment,主要邏輯是取模,如下所示:
switch (h->reducealg){case REDUCE_BITMASK: result = FASTMOD(h->hash, (uint32) h->numsegs); /* fast mod (bitmask) */break;case REDUCE_LAZYMOD: result = (h->hash) % (h->numsegs); /* simple mod */break;}
對于每一個 tuple 要執行下面的flow:
●void cdbhashinit(CdbHash *h)●void cdbhash(CdbHash *h, Datum datum, Oid type)●void addToCdbHash(void *cdbHash, void *buf, size_t len)●unsigned int cdbhashreduce(CdbHash *h)
隨機分布
如果不能確定一張表的哈希分布鍵或者不存在合理的避免數據傾斜的分布鍵,則可以使用隨機分布。隨機分布會采用循環的方式將一次插入的數據存儲到不同的節點上。隨機性只在單個 SQL 中有效,不考慮跨 SQL 的情況。譬如如果每次插入一行數據到隨機分布表中,最終的數據會全部保存在第一個節點上。
test=# create table t1 (id int) DISTRIBUTED RANDOMLY;CREATE TABLEtest=# INSERT INTO t1 VALUES (1);INSERT 0 1test=# INSERT INTO t1 VALUES (2);INSERT 0 1test=# INSERT INTO t1 VALUES (3);INSERT 0 1test=# SELECT gp_segment_id, * from t1; gp_segment_id | id---------------+----1 | 11 | 21 | 3
有些工具使用隨機分布實現數據管理,譬如擴容工具 gpexpand 在增加節點后需要對數據進行重分布。在初始化的時候,gpexpand 會把所有表都標記為隨機分布,然后執行重新分布操作,這樣重分布操作不影響業務的正常運行。(Greenplum 6 重新設計了 gpexpand,不再需要修改分布策略為隨機分布)
復制表(Replicated Table)
Greenplum 6支持一種新的分布策略:復制表,即整張表在每個節點上都有一個完整的拷貝。
test=# CREATE TABLE t2 (id int) DISTRIBUTED REPLICATED;CREATE TABLEtest=# INSERT INTO t2 VALUES (1), (2), (3);INSERT 0 3test=# SELECT * FROM t2;id----123(3 rows)test=# SELECT gp_segment_id, * from t2; gp_segment_id | id---------------+----0 | 10 | 20 | 3
復制表解決了兩個問題:
●?UDF 在 segment 上不能訪問任何表。由于 MPP 的特性,任何 segment 僅僅包含部分數據,因而在 segment 執行的 UDF 不能訪問任何表,否則數據計算錯誤。
yydzero=# CREATE FUNCTION c() RETURNS bigint AS $$yydzero$# SELECT count(*) from t1 AS result;yydzero$# $$ LANGUAGE SQL;CREATE FUNCTIONyydzero=# SELECT c(); c---6(1 row)yydzero=# select c() from t2;ERROR: function cannot execute on a QE slice because it accesses relation "public.t1" (seg0 slice1 192.168.1.107:25435 pid=76589)
如果把上面的t1改成復制表,則不存在這個問題。
復制表有很多應用場景,譬如 PostGIS 的 spatial_ref_sys (PostGIS 有大量的 UDF 需要訪問這張表)和 PLR 中的 plr_modules 都可以采用復制表方式。在支持這個特性之前,Greenplum 只能通過一些小技巧來支持諸如 spatial_ref_sys 之類的表。
避免分布式查詢計劃:如果一張表的數據在各個segment上都有拷貝,那么就可以生成本地連接計劃,而避免數據在集群的不同節點間移動。如果用復制表存儲數據量比較小的表(譬如數千行),那么性能有明顯的提升。 數據量大的表不適合使用復制表模式。
四、查詢計劃并行化
PostgreSQL 生成的查詢計劃只能在單節點上執行,Greenplum 需要將查詢計劃并行化,以充分發揮集群的優勢。
Greenplum 引入 Motion 算子(操作符)實現查詢計劃的并行化。Motion 算子實現數據在不同節點間的傳輸,它為其他算子隱藏了 MPP 架構和單機的不同,使得其他大多數算子不用關心是在集群上執行還是在單機上執行。每個 Motion 算子都有發送方和接收方。此外 Greenplum 還對某些算子進行了分布式優化,譬如聚集。(本小節需要理解PostgreSQL 優化器基礎知識,可參閱 src/backend/optimizer/README)
優化實例
在介紹技術細節之前,先看幾個例子。
下面的例子中創建了2張表 t1 和 t2,它們都有兩個列 c1, c2,都是以 c1 為分布鍵。
CREATE table t1 AS SELECT g c1, g + 1 as c2 FROM generate_series(1, 10) g DISTRIBUTED BY (c1);CREATE table t2 AS SELECT g c1, g + 1 as c2 FROM generate_series(5, 15) g DISTRIBUTED BY (c1);SQL1: SELECT * from t1, t2 where t1.c1 = t2.c1; c1 | c2 | c1 | c2----+----+----+---- 5 | 6 | 5 | 6 6 | 7 | 6 | 7 7 | 8 | 7 | 8 8 | 9 | 8 | 9 9 | 10 | 9 | 10 10 | 11 | 10 | 11(6 rows)
SQL1 的查詢計劃為如下所示,因為關聯鍵是兩個表的分布鍵,所以關聯可以在本地執行,HashJoin 算子的子樹不需要數據移動,最后 GatherMotion 在 master 上做匯總即可。
QUERY PLAN------------------------------------------------------------------------------Gather Motion 3:1 (slice1; segments: 3) (cost=3.23..6.48 rows=10 width=16)-> Hash Join (cost=3.23..6.48 rows=4 width=16)Hash Cond: t2.c1 = t1.c1-> Seq Scan on t2 (cost=0.00..3.11 rows=4 width=8)-> Hash (cost=3.10..3.10 rows=4 width=8)-> Seq Scan on t1 (cost=0.00..3.10 rows=4 width=8)Optimizer: legacy query optimizer
SQL2: SELECT * from t1, t2 where t1.c1 = t2.c2; c1 | c2 | c1 | c2----+----+----+---- 9 | 10 | 8 | 910 | 11 | 9 | 10 8 | 9 | 7 | 86 | 7 | 5 | 6 7 | 8 | 6 | 7(5 rows)
SQL2 的查詢計劃如下所示,t1 表的關聯鍵c1也是其分布鍵,t2 表的關聯鍵c2不是分布鍵,所以數據需要根據 t2.c2 重分布,以便所有 t1.c1 = t2.c2 的行都在同一個 segment 上執行關聯操作。
QUERY PLAN----------------------------------------------------------------------------------------------Gather Motion 3:1 (slice2; segments: 3) (cost=3.23..6.70 rows=10 width=16)-> Hash Join (cost=3.23..6.70 rows=4 width=16)Hash Cond: t2.c2 = t1.c1-> Redistribute Motion 3:3 (slice1; segments: 3) (cost=0.00..3.33 rows=4 width=8)Hash Key: t2.c2-> Seq Scan on t2 (cost=0.00..3.11 rows=4 width=8)-> Hash (cost=3.10..3.10 rows=4 width=8)-> Seq Scan on t1 (cost=0.00..3.10 rows=4 width=8)Optimizer: legacy query optimizer
SQL3: SELECT * from t1, t2 where t1.c2 = t2.c2; c1 | c2 | c1 | c2----+----+----+---- 8 | 9 | 8 | 99 | 10 | 9 | 10 10 | 11 | 10 | 115 | 6 | 5 | 6 6 | 7 | 6 | 77 | 8 | 7 | 8(6 rows)
SQL3 的查詢計劃如下所示,t1的關聯鍵c2 不是分布鍵,t2的關聯鍵c2 也不是分布鍵,所以采用廣播Motion,使得其中一個表的數據可以廣播到所有節點上,以保證關聯的正確性。最新的 master 代碼對這個查詢生成的計劃會對兩個表選擇重分布,為何這么做可以作為一個思考題:)。
QUERY PLAN--------------------------------------------------------------------------------------------Gather Motion 3:1 (slice2; segments: 3) (cost=3.25..6.96 rows=10 width=16)-> Hash Join (cost=3.25..6.96 rows=4 width=16)Hash Cond: t1.c2 = t2.c2-> Broadcast Motion 3:3 (slice1; segments: 3) (cost=0.00..3.50 rows=10 width=8)-> Seq Scan on t1 (cost=0.00..3.10 rows=4 width=8)-> Hash (cost=3.11..3.11 rows=4 width=8)-> Seq Scan on t2 (cost=0.00..3.11 rows=4 width=8)Optimizer: legacy query optimizer
SQL4: SELECT * from t1 LEFT JOIN t2 on t1.c2 = t2.c2 ; c1 | c2 | c1 | c2----+----+----+---- 1 | 2 | |2 | 3 | | 3 | 4 | |4 | 5 | | 5 | 6 | 5 | 66 | 7 | 6 | 7 7 | 8 | 7 | 88 | 9 | 8 | 9 9 | 10 | 9 | 1010 | 11 | 10 | 11(10 rows)
SQL4 的查詢計劃如下所示,盡管關聯鍵和 SQL3 一樣,然而由于采用了 left join,所以不能使用廣播t1的方法,否則數據會有重復,因而這個查詢的計劃對兩張表都進行了重分布。根據路徑代價的不同,對于 SQL4 優化器也可能選擇廣播 t2 的方法。(如果數據量一樣,單表廣播代價要高于雙表重分布,對于雙表重分布,每個表的每個元組傳輸一次,相當于單表每個元組傳輸兩次,而廣播則需要單表的每個元組傳輸 nSegments 次。)
QUERY PLAN----------------------------------------------------------------------------------------------Gather Motion 3:1 (slice3; segments: 3) (cost=3.47..6.91 rows=10 width=16)-> Hash Left Join (cost=3.47..6.91 rows=4 width=16)Hash Cond: t1.c2 = t2.c2-> Redistribute Motion 3:3 (slice1; segments: 3) (cost=0.00..3.30 rows=4 width=8)Hash Key: t1.c2-> Seq Scan on t1 (cost=0.00..3.10 rows=4 width=8)-> Hash (cost=3.33..3.33 rows=4 width=8)-> Redistribute Motion 3:3 (slice2; segments: 3) (cost=0.00..3.33 ...Hash Key: t2.c2-> Seq Scan on t2 (cost=0.00..3.11 rows=4 width=8)Optimizer: legacy query optimizer
SQL5:SELECT c2, count(1) from t1 group by c2;c2 | count----+-------5 | 16 | 17 | 14 | 13 | 110 | 111 | 18 | 19 | 12 | 1(10 rows)
上面四個 SQL 顯示不同類型的 JOIN 對數據移動類型(Motion類型)的影響。SQL5 演示了 Greenplum 對聚集的優化:兩階段聚集。第一階段聚集在每個 Segment 上對本地數據執行,然后通過重分布到每個 segment 上執行第二階段聚集。最后由 Master 通過 Gather Motion 進行匯總。 Greenplum 對某些 SQL 譬如 DISTINCT GROUP BY也會采用三階段聚集。
QUERY PLAN-----------------------------------------------------------------------------------------------Gather Motion 3:1 (slice2; segments: 3) (cost=3.55..3.70 rows=10 width=12)-> HashAggregate (cost=3.55..3.70 rows=4 width=12)Group Key: t1.c2-> Redistribute Motion 3:3 (slice1; segments: 3) (cost=3.17..3.38 rows=4 width=12)Hash Key: t1.c2-> HashAggregate (cost=3.17..3.17 rows=4 width=12)Group Key: t1.c2-> Seq Scan on t1 (cost=0.00..3.10 rows=4 width=4)Optimizer: legacy query optimizer(9 rows)
Greenplum 為查詢優化引入的新數據結構和概念
前面幾個直觀的例子展示了Greenplum 對不同 SQL 生成的不同分布式查詢計劃。下面介紹其主要內部機制。
為了把單機查詢計劃變成并行計劃,Greenplum 引入了一些新的概念,分別對 PostgreSQL 的 Node、Path 和 Plan結構體進行了增強:
●?新增一種節點(Node)類型:Flow
●?新增一種路徑(Path)類型:CdbMotionPath
●?新增一個新的查詢計劃(Plan)算子:Motion(Motion 的第一個字段是 Plan, Plan 結構體的第一個字段是 NodeTag type。Flow 的第一個節點也是 NodeTag type,和 RangeVar、IntoClause、Expr、RangeTableRef 是一個級別的概念)
●?為 Path 結構體添加了 CdbPathLocus locus這個字段,以表示結果元組在這個路徑下的重分布策略
●?為 Plan 結構體增加 Flow 字段,以表示這個算子的元組流向;
新Node類型:Flow
新節點類型 Flow 描述了并行計劃中元組的流向。 每個查詢計劃節點(Plan 結構體)都有一個 Flow 字段,以表示當前節點的輸出元組的流向。 ?Flow 是一個新的節點類型,但不是一個查詢計劃節點。此外 Flow 結構體還包括一些用于計劃并行化的成員字段。
Flow 有三個主要字段:
●?FlowType,表示 Flow 的類型
●?UNDEFINED: 未定義 Flow
●?SINGLETON:表示的是 GatherMotion
●?REPLICATED:表示的是廣播 Motion
●?PARTITIONED: 表示的是重分布 Motion。
●?Movement,確定當前計劃節點的輸出,該使用什么樣的 motion。主要用于把子查詢的計劃進行處理以適應分布式環境。?
●?None:不需要motion
●?FOCUS:聚焦到單個 segment,相當于 GatherMotion
●?BROADCAST: 廣播 motion
●?REPARTITION: 哈希重分布
●?EXPLICIT:定向移動元組到 segid 字段標記的 segments
●?CdbLocusType: Locus 的類型,優化器使用這個信息以選擇最合適的節點進行最合適的數據流向處理,確定合適Motion。
●?CdbLocusType_Null:不用 Locus
●?CdbLocusType_Entry: 表示 entry db (即master) 上單個backend進程,可以是 QD (Query Dispatcher),也可以是 entrydb 上的 QE(Query Executor)
●?CdbLocusType_SingleQE:任何節點上的單個 backend進程,可以是 QD或者任意 QE 進程
●?CdbLocusType_General:和任何 locus 都兼容
●?CdbLocusType_Replicated:在所有 QEs 都有副本
●?CdbLocusType_Hashed:哈希分布到所有 QEs
●?CdbLocusType_Strewn:數據分布存儲,但是分布鍵未知
新Path類型:CdbMotionPath
Path 表示了一種可能的計算路徑(譬如順序掃描或者哈希關聯),更復雜的路徑會繼承 Path 結構體并記錄更多信息以用于優化。 Greenplum 為 Path 結構體新加 ?CdbPathLocus locus 這個字段,用于表示結果元組在當前路徑下的重分布和執行策略。
Greenplum 中表的分布鍵決定了元組存儲時的分布情況,影響元組在那個 segment 的磁盤上的存儲。CdbPathLocus 決定了在執行時一個元組在不同的進程間(不同segment的 QE)的重分布情況,即一個元組該被那個進程處理。元組可能來自于表,也可能來自于函數。
Greenplum 還引入了一個新的路徑: CdbMotionPath, 用以表示子路徑的結果如何從發送方進程傳送給接收方進程。
新 Plan 算子:Motion
如上面所述,Motion 是一種查詢計劃樹節點,它實現了數據的洗牌(Shuffle),使得其父算子可以從其子算子得到需要的數據。Motion 有三種類型:
●?MOTIONTYPE_HASH:使用哈希算法根據重分布鍵對數據進行重分布,把經過算子的每個元組發送到目標 segment,目標segment由重分布鍵的哈希值確定。
●?MOTIONTYPE_FIXED:發送元組給固定的segment集合,可以是廣播 Motion(發送給所有的 segments)或者 Gather Motion (發送給固定的某個segment)
●?MOTIONTYPE_EXPLICIT:發送元組給其 segid 字段指定的 segments,對應于顯式重分布 Motion。和 MOTIONTYPE_HASH 的區別是不需要計算哈希值。
前面提到,Greenplum 為 Plan 結構體引入了 Flow *flow 這個字段表示結果元組的流向。此外Plan結構體還引入了其他幾個與優化和執行相關的字段,譬如表示是否需要 MPP 調度的DispatchMethod dispatch 字段、是否可以直接調度的 directDispatch 字段(直接調度到某個segment,通常用于主鍵查詢)、方便MPP執行的分布式計劃的 sliceTable、用于記錄當前計劃節點的父motion 節點的 motionNode 等。
生成分布式查詢計劃
下圖展示了 Greenplum 中傳統優化器(ORCA 優化器于此不同)的優化流程,本節強調與 PostgreSQL 的單機優化器不同的部分。
standard_planner 是 PostgreSQL 缺省的優化器,它主要調用了 subquery_planner 和 set_plan_references。在 Greenplum 中,set_plan_references 之后又調用了 cdbparallelize 以對查詢樹做最后的并行化處理。
subquery_planner 如名字所示對某個子查詢進行優化,生成查詢計劃樹,它主要有兩個執行階段:
●?基本查詢特性(也稱為SPJ:Select/Projection/Join)的優化,由 query_planner() 實現
●?高級查詢特性(Non-SPJ)的優化,例如聚集等,由 grouping_planner() 實現,grouping_planner() 會調用 query_planner() 進行基本優化,然后對高級特性進行優化。
Greenplum 對單機計劃的分布式處理主要發生在兩個地方:
●?單個子查詢:Greenplum 的 subquery_planner() 返回的子查詢計劃樹已經進行了某些分布式處理,譬如為 HashJoin 添加 Motion 算子,二階段聚集等。
●?多個子查詢間:Greenplum 需要設置多個子查詢間恰當的數據流向,以使得某個子查詢的結果可以被上層查詢樹使用。這個操作是由函數 cdbparallelize 實現的。
單個子查詢的并行化
Greenplum 優化單個子查詢的流程和PostgreSQL 相似,主要區別在于:
●?關聯:根據關聯算子的左右子表的數據分布情況確定是否添加 Motion 節點、什么類型的Motion 等。
●?聚集等高級操作的優化,譬如前面提到的兩階段聚集。
下面簡要介紹下主要流程:
首先使用 build_simple_rel() 構建簡單表的信息。build_simple_rel 獲得表的基本信息,譬如表里面有多少元組,占用了多少個頁等。其中很重要的一個信息是數據分布信息:GpPolicy 描述了基本表的數據分布類型和分布鍵。
然后使用 set_base_rel_pathlists() 設置基本表的訪問路徑。set_base_rel_pathlists 根據表類型的不同,調用不同的函數:
●RTE_FUNCTION: create_functionscan_path()●RTE_RELATION: create_external_path()/create_aocs_path()/create_seqscan_path()/create_index_paths()●RTE_VALUES: create_valuesscan_path
這些函數會確定路徑節點的 locus 類型,表示數據分布處理相關的一種特性。 這個信息對于子查詢并行化非常重要,在后面把 path 轉換成 plan 的時候,被用于決定一個計劃的 FLOW 類型,而 FLOW 會決定執行器使用什么樣類型的 Gang 來執行。
如何確定 locus?
對于普通的堆表(Heap),順序掃描路徑 create_seqscan_path() 使用下面方式確定路徑的 locus 信息:
●?如果表是哈希分布,則 locus 類型為 CdbLocusType_Hashed
●?如果是隨機分布,則 locus 類型為 CdbLocusType_Strewn
●?如果是系統表,則 locus 類型為 CdbLocusType_Entry
對于函數,則 create_function_path() 使用下面方式確定路徑的 locus:
●?如果函數是 immutable 函數,則使用:CdbLocusType_General
●?如果函數是 mutable 函數,則使用:CdbLocusType_Entry
●?如果函數需要在 master 上執行,則使用: CdbLocusType_Entry
●?如果函數需要在所有 segments 上執行,則使用 CdbLocusType_Strewn
如果SQL語句中包含關聯,則使用 make_rel_from_joinlist() 為關聯樹生成訪問路徑。相應的函數有:create_nestloop_path/create_mergejoin_path/create_hashjoin_path。這個過程最重要的一點是確定是否需要添加 Motion 節點以及什么類型的 Motion 節點。 譬如前面 SQL1 關聯鍵是兩張表t1/t2 的分布鍵,因而不需要添加 Motion;而 SQL2 則需要對 t2 進行重分布,以使得對于任意t1的元組,滿足關聯條件 (t1.c1 = t2.c2) 的所有t2的元組都在同一個 segment 上。
如果 SQL 包含聚集、窗口函數等高級特性,則調用 cdb_grouping_planner() 進行優化處理,譬如將聚集轉換成兩階段聚集或者三階段聚集等。
最后一步是從所有可能的路徑中選擇最廉價的路徑,并調用 create_plan() 把最優路徑樹轉換成最優查詢樹。
在這個階段, Path 路徑的 Locus 影響生成的 Plan 計劃的 Flow 類型。Flow 和執行器一節中的 Gang 相關,Flow 使得執行器不用關心數據以什么形式分布、分布鍵是什么,而只關心數據是在多個 segment 上還是單個 segment 上。 Locus 和 Flow 之間的對應關系:
●FLOW_SINGLETON: Locus_Entry/Locus_SingleQE/Locus_General●FLOW_PARTITIONED: Locus_Hash/Locus_Strewn/Locus_Replicated
多個子查詢間的并行化
cdbparallelize() 主要目的是解決多個子查詢之間的數據流向,生成最終的并行化查詢計劃。它含有兩個主要步驟:prescan 和 apply_motion
●?prescan 有兩個目的,一個目的是對某些類型的計劃節點(譬如 Flow )做標記以備后面 apply_motion 處理;第二個目的是對子計劃節點 (SubPlan)進行標記或者變形。SubPlan 實際上不是查詢計劃節點,而是表達式節點,它包含一個計劃節點及其范圍表(Range Table)。 SubPlan 對應于查詢樹中的 SubLink(SQL 子查詢表達式),可能出現在表達式中。prescan 對 SubPlan 包含的計劃樹做以下處理:
●?如果 Subplan 是個 Initplan,則在查詢樹的根節點做一個標注,表示需要以后調用 apply_motion 添加一個 motion 節點。
●?如果 Subplan 是不相關的多行子查詢,則根據計劃節點中包含的 Flow 信息對子查詢執行 Gather 或者廣播操作。并在查詢樹之上添加一個新的 materialized (物化)節點,以防止對 Subplan 進行重新掃描。因為避免了每次重新執行子查詢,所以效率提高。
●?如果 Subplan 是相關子查詢,則轉換成可執行的形式。遞歸掃描直到遇到葉子掃描節點,然后使用下面的形式替換該掃描節點。經過這個轉換后,查詢樹可以并行執行,因為相關子查詢已經變成結果節點的一部分,和外層的查詢節點在同一個Slice中。
Result \ \_Material \ \_Broadcast (or Gather) \ \_SeqScan
●?apply_motion: 根據計劃中的 Flow 節點,為頂層查詢樹添加 motion 節點。根據 SubPlan 類型的不同(譬如InitPlan、不相關多行子查詢、相關子查詢)添加不同的Motion節點。
譬如 SELECT * FROM tbl WHERE id = 1,prescan() 遍歷到查詢樹的根節點時會在根節點上標注,apply_motion() 時在根節點之上添加一個 GatherMotion。
本篇主要介紹了Greenplum集群概述、分布式數據存儲和分布式查詢優化。下一篇將會繼續介紹分布式查詢執行、分布式事務、數據洗牌和集群管理等。
PostgreSQL中文社區歡迎廣大技術人員投稿
投稿郵箱:press@postgres.cn
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的java执行查询postgresql得到中文乱码_Greenplum: 基于PostgreSQL的分布式数据库内核揭秘(上篇)...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python结果导入excel_荐Pyt
- 下一篇: 服务器数据库2008怎么备份数据库文件,