當 SQL 跑在 MPP 上:Greenplum 與 Cloudberry

· tech

#sql#data-engineering

📑 目錄

整個 SQL 系列的壓軸。前面 11 篇都跑在單機 PostgreSQL,這篇把同一批 SQL 放到 MPP(Massively Parallel Processing,大規模平行處理) 上——Greenplum、以及它的開源接班人 Apache Cloudberry(Apache 孵化中)。好消息:因為 Cloudberry 源自 Greenplum、Greenplum 又源自 PostgreSQL,語法幾乎不變,前面 11 篇你全部可以直接用;壞消息:你多了一個單機時代不用想的東西——資料放在哪個節點。而這個決定,會左右你所有查詢的快慢。

從單機到 MPP:一個 coordinator + 一堆 segment

MPP 的架構很直觀:一個 coordinator(大腦)負責收 query、規劃、分派;下面一堆 segment(每個其實就是一個獨立的 PostgreSQL 實例)各自存一份資料、平行處理自己那份。一張表怎麼分散到各 segment,由分佈鍵(distribution key)決定:

Coordinator 收 query、規劃、分派 Segment 1 Segment 2 Segment 3 DISTRIBUTED BY (分佈鍵):用 hash(分佈鍵) 決定每列落哪個 segment,查詢在每台平行跑 ⚠ 分佈鍵不均勻 → 某 segment 資料爆多(skew),平行失效、最慢那台拖垮全部
MPP = 一個 coordinator 指揮、一群 segment 平行幹活。速度來自「大家同時做自己那份」,所以只要有一台特別慢(資料傾斜),整體就被它拖住——分佈鍵的第一個責任,就是讓資料分得均勻

平行的威力,建立在「每台的工作量差不多」。所以分佈鍵最忌資料傾斜(skew)——如果你拿一個值很集中的欄(例如「國家」而八成資料都是同一國)當分佈鍵,那一國全擠進同一個 segment,其他 segment 閒著、它累死,平行度直接報銷。

分佈鍵選錯,就會「跨節點搬資料」

傾斜之外,分佈鍵還有第二個、更隱形的責任:決定 join / group by 要不要跨節點搬資料。當你 join 兩張表,如果「要配對的資料」剛好在同一個 segment,就能各台本地 join;如果散在不同 segment,就得先把資料透過網路搬到一起——這個搬移,MPP 叫 Motion,它就是 Spark shuffle 的 MPP 版:

分佈鍵 = join key ✓ Segment 1 A k=1 B k=1 本地 Segment 2 A k=2 B k=2 本地 同 key 天生同一台 → 免搬(no motion) 分佈鍵 ≠ join key ✗ Segment 1 A k=1 缺 B k=1 Segment 2 B k=1 要配對的散在不同台 → 跨網路搬(Motion)= shuffle
左:兩表都按 join key 分佈,同 key 天生同 segment,本地 join、零搬移。右:分佈鍵沒對上,要 join 的 B 在別台,得跨網路搬過來(Redistribute Motion)——這就是 MPP 的 shuffle。另有把小表複製到每台的 Broadcast Motion(對應 Spark 的 broadcast join)

Motion 有幾種,對應的正是你在 Spark 學過的招式:Redistribute Motion(兩表都依 join key 重新 hash 分散,= shuffle join)、Broadcast Motion(小表複製到每個 segment,= broadcast join)、Gather Motion(把各 segment 的結果收回 coordinator)。而在 執行計畫裡,這些 Motion 會明明白白列出來——看到大表被 Redistribute,就跟在 PG 看到 Seq Scan、在 Spark 看到 Exchange 一樣,是該停下來想「這搬移省得掉嗎」的訊號。

選分佈鍵的原則

綜合起來,選分佈鍵就兩個目標,而它們有時會打架:

  • 分得均勻(避免 skew):選高基數、分佈平均的欄(例如 user_idorder_id),別選值很集中的欄(國家、狀態、布林)。
  • 配對在本地(避免 motion):選最常拿來 join 的欄當分佈鍵。尤其大表 join 大表時,讓兩張表用同一個 join key 當分佈鍵——這樣同 key 的資料天生就在同一 segment,join 完全不用搬,這是 MPP 效能最關鍵的一招。

真的兩者兼顧不了時(join key 剛好很傾斜),就得取捨,或用 Broadcast 把小表複製掉。建表 DISTRIBUTED BY 那一刻,你其實就決定了未來一票查詢的命運——這是 MPP 跟單機最不一樣的地方。

反思

MPP 的一切難題,都回到同一句話:少搬資料

Distribution key、motion、skew,名詞一堆,但骨子裡只有一件事:資料在哪、要不要搬。這跟 Spark 的 shuffle 根本是同一套物理——把「會一起被 join / group 的資料」放在一起,就免搬;放不對,就得跨網路搬,而網路搬移永遠是分散式運算最貴的一步。學過 Spark shuffle 再看 MPP,幾乎是無痛切換:換了名字(Exchange → Motion、broadcast join → Broadcast Motion),道理一模一樣。跨引擎的效能物理是共通的——這也是為什麼我一直說,學透一個分散式引擎,其他的就通了大半。

分散式的難,不在語法,在「位置」

這篇最大的體會:從單機到 MPP,SQL 幾乎沒變,但你多了一整層要想的事——資料的「位置」。單機你只煩惱「查詢怎麼寫」;MPP 你得先煩惱「資料怎麼分佈、運算在哪發生、要不要搬」。而且這個決定在建表時就定死了,選錯後面全部慢。這讓我更確定一件事:分散式系統真正的難點,從來不是 API,是位置與移動——資料放哪、算在哪、什麼時候得跨越邊界。這正好是我在寫的 運算與儲存分離、以及 DDIA 分片那章的核心命題,MPP 只是它一個很具體的縮影。

整個系列,其實在講同一件事:別被「能跑」騙了

寫到最後一篇,回頭看這十二篇的共同主題其實只有一句:SQL 很容易「跑得出來」,但「跑對、跑得快」需要你看穿底下的機制。 執行順序、JOIN 怎麼配對、NULL 的三值邏輯、window 怎麼算、索引怎麼查、EXPLAIN 怎麼讀、MVCC 怎麼隔離、MPP 怎麼分佈——每一篇都在拆穿一個「你以為你懂、其實只是會用」的東西。看穿機制之後,你才從「會寫 SQL」變成「懂 SQL」。這就是這個系列名字的意思——SQL,我以為我懂;而讀完這一輪,希望你跟我一樣,真的更懂了一點。