理解Google Spanner(4): 看懂查詢計劃

Spanner會為每條SQL生成一個或多個查詢計劃,并選擇數據庫認為最優的那個查詢計劃去執行,同一個SQL,不同的查詢計劃最終的效率可能是千差萬別的,理解查詢計劃是SQL優化的基本必備技能。

Spanner本身有官方文檔幫助大家理解查詢計劃,但是講得比較精簡,如果對Spanner不熟悉,可能理解起來比較困難,本文是這篇文檔的擴展,但是會更淺顯易懂、詳細,有一些總結與延伸。

本文不會講什么:

  1. 查詢運算符詳解(Query Operators),請自行參閱Spanner文檔

本文會講什么:

  1. 理解Spanner如何執行一個查詢計劃
  2. 如何看懂GCP Console下獲取的Spanner的查詢計劃
  3. 如何基于查詢計劃作出優化

一、查詢計劃如何被執行

Spanner是分布式數據庫,因此一個數據庫實例(Instance)是分布在多臺server的,因此一條SQL可能意味著需要多臺server配合才能產生最終結果。
client連接到Spanner,Spanner將SQL解析為查詢計劃(Query Plan),并選擇一臺server作為root server,Spanner將plan發送到root server,其他需要參與query的server稱為remote server,均被root server協調,它們接收root server下發的subplan,然后將查詢結果返回給root server,最終由root server返回給client。
Root server本身也參與query,因此理論上有一部分subplan會下發給自己,也就是說root server本身也可以扮演remote server。
Root Server下發subplan到各個Remote Server并從Remote Server收集結果的行為,在查詢計劃中稱為Distributed Union

查詢計劃如何被分發

由于每臺server都負責保存多個splits,因此每臺remote server收到subplan后,會將subplan再次分割為一到多個splits的查詢計劃,下發給特定的split,每個split獨立執行自己的計劃并返回結果給server,這個過程在查詢計劃中稱為Local Distributed Union

image.png

總結一下:
Root Server負責:

1. 下發subplan到其他參與的server
2. 等待所有server返回subplan結果給自己
3. 匯總各個server的執行結果,如果需要的話,進行進一步處理
4. 將匯總后的執行結果返回給client

Remote Server負責:

1. 接收Root Server下發的subplan
2. 將subplan拆分成1個或多個分片的subplan并執行
3. 匯總各個分片執行的結果
4. 返回匯總的結果給Root Server

二、解讀查詢計劃

1. Example — 簡單查詢計劃

下圖是摘自Spanner官方文檔的查詢計劃
圖中箭頭由下往上,表示的是結果返回順序,而查詢計劃的分發順序恰恰相反,應該由上往下。

摘自Spanner官方文檔的查詢計劃
下發階段

圖中的查詢計劃表示SQL被解析為查詢計劃,發送給Root Server,Root Server進行Distriubted Union將subplan下發給Remote Server并等待最終結果。
Serialize ResultAggregate都是對結果進行處理的運算符,因此下發期間可以忽略。
Remote Server(s)收到Root Server的subplan后,將subplan拆分為特定split(s)的查詢計劃,交給特定的split(s)執行,也就是Local Distributed Union
Local Distributed Union下就是每個split會進行的查詢計劃,此時查詢計劃分發完畢,我們開始從下往上讀,解讀執行與返回過程。

執行與返回階段

每個split執行Table Scan,從Songs表讀取SingerId。
每一個被讀出的SingerId都會被Filter操作符根據SingerId<100的條件過濾,只有滿足條件的,才會往上返回。
Filter返回的數據會在Remote Server進行Local Distributed Union,也就是結果集的合并,并且再往上返回。
所有Remote Server都會將結果返回給Aggregator操作符,進行結果集的聚合。
聚合后的結果被Serialize Result操作符組合為最終返回格式,這個操作符是每個查詢計劃都會有的,負責將查詢出的數據轉換為要發送回client的格式。
轉換為最終格式的數據,進行Distributed Union,返回SQL執行的最后結果。

整個查詢計劃結束

2. Example — 復雜查詢計劃

上面的簡單查詢計劃只包括一元操作符,下面講一下包含二元操作符的查詢計劃,比如進行Join操作。
注意:下圖生成的查詢計劃有個前提條件—— Albums表是Singers表的子表,兩者是Interleave關系。


摘自Spanner官方文檔的復雜查詢計劃
下發階段

任何查詢計劃的分發都是差不多的,只有4個操作符涉及分發,那就是Distributed UnionDistributed Cross ApplyDistributed Outer ApplyLocal Distributed Union,因此這里不再講一遍。

執行與返回階段

最底部是兩個并排的查詢計劃,應該從左往右看,左邊是input,右邊是對input進行map處理,也就是說,查詢計劃是從下往上執行,從左往右執行。
先對Albums表進行Table Scan查出SingerId、AlbumId、AlbumTitle三個字段。
Table Scan的結果會返回給Cross Apply操作符,此操作符對結果進行map,也就是為每個結果執行一次Index Scan
Index Scan查出SongName返回給Cross Apply
Cross Apply 將Table Scan與Index Scan的結果進行Join,實際上Cross Apply操作符就是進行nested loop join,由于兩個參與Join的表是Interleave的,所以此Cross Apply只需要在本Remote Server上執行,否則應使用Distributed Cross Apply(將在下一個例子中說明)。
Join后的結果被Serialize Result轉換為返回格式。
Local Distributed Union整合此Remote Server上的所有Results返回給Root Server。
Root Server進行Distributed Union將最終結果返回。

整個查詢計劃結束。

3. Example — Distributed Cross Apply查詢計劃

摘自Spanner官方文檔的Distributed Cross Apply

上圖中的SQL需要讀2張表,一張是索引SongsBySongName,一張是數據表Songs,索引無法Interleave,所以索引和數據可以分別處于不同的分片,那么要實現這個SQL,就不能使用(Local) Cross Apply,而需要使用Distributed Cross Apply,因此最頂層的操作符是Distributed Cross Apply

下發階段

Root Server的Distributed Cross Apply會等待Distributed Union后進行Create Batch的結果作為input,當Distributed Cross Apply收到Create Batch的結果作為input后,再下發plan給Remote Server,做map操作。
這里注意,下發其實被分為了兩個階段,左邊先執行完,Distributed Cross Apply才會進行右邊的下發

執行與返回階段

Remote Server將plan分配給多個splits進行Index Scan
Index Scan的結果被Filter過濾符合條件的返回。
Local Distributed Union匯總本臺server上的數據發回給Root Server。
Root Server使用Distributed Union匯總Remote Server發來的數據。
Serialize Result格式化數據。
Create Batch操作符代表創建中間表,因為涉及到跨server的join,因此需要創建中間表。
將Create Batch創建的中間表作為input發給Distributed Cross Apply操作符。
Distributed Cross Apply下發查詢到Remote Server(進行Map)。
Batch Scan讀取中間表并返回給Cross Apply
Cross Apply根據Batch Scan結果進行Join并通過Serialize ResultLocal Distributed Union后返回給Root Server。
Distributed Cross Apply根據返回結果完成Join,返回SQL執行結果。

整個查詢計劃結束。

4. 從GCP Console解讀查詢計劃

在GCP Console中可以方便地獲得查詢計劃,但不是圖片形式,沒有左右關系,因此我們需要將Console中的text展示的查詢計劃,在腦袋中轉換為圖片版的。


Console查詢計劃

每行計劃的開頭都有一個小標記。
轉換原則是:

  • 垂直箭頭則表示上下關系。比如:


    上下關系
  • 人字型箭頭代表這是一個接收多個參數的操作符,比如Hash Join


    Hash Join是二元操作符
  • 直角箭頭代表是父操作符的輸入參數,比如圖中兩個Distributed Union不是上下級關系,而是兄弟關系,作為Hash Join的下級操作符,也就是Hash Join的輸入參數,兩個Distributed Union應該是左右排列。

  • 對于應該左右排列的操作符,越上面出現,越左邊,從左往右依次排放。


    左右關系

因此上圖應該是如下:


查詢計劃圖

三、SQL優化

我們可以根據查詢計劃對SQL進行優化,但是在優化之前務必盡量讀懂查詢計劃,因此需要了解每個操作符的意義,在進行下面的閱讀前最好能夠先閱讀Spanner操作符文檔

1. 為什么用了索引還是慢查詢?

大家都知道全表掃描是嚴格禁止的(數據量特別小的表不在討論范圍),導致慢查詢甚至拖垮數據庫,于是往查詢上面加索引,結果加了索引還是慢查詢。
為什么會出現這種情況,是因為大家忽略了導致慢查詢的根本原因——大量磁盤IO導致CPU和內存被大量占用,全表掃描不用說,一定是大量的磁盤IO,把表依次讀一遍,實際上索引建得不好,也會有這種情況。
在Spanner中,索引也是表,索引不過是只存儲部分字段的表而已,可以理解為一個比數據表更小的表,如果查詢條件不能利用索引的最左前綴原則,那么這個索引就只能被全索引掃描,Spanner會將索引全部掃描一遍,利用Filter返回符合條件的行,對CPU和內存的占用極大。
比如為users表建立一個 (user_name,email) 的索引,卻使用這個索引進行 SELECT user_name FROM users WHERE email = xxx 查詢,由于查詢條件不包括user_name,因此無法使用這個Index進行Filter Scan,也就是無法直接定位到索引所在數據頁,而需要讀取整個索引,進行Filter操作,也就是全表掃描(索引也是表,因此對表和索引的全掃描都可以稱為全表掃描)。
從Spanner的查詢計劃中可以看到是否對一個索引或者一個表使用了全表掃描,如下圖:

Full Scan

如果索引中有100萬條記錄,那么100萬條都會被讀入內存進行Filter,CPU和內存壓力比較大,會出現慢查詢,因此對于查詢計劃中的 Full Scan 需要根據SQL運行頻率、表大小進行評估,在必要的情況下建立更合適的索引避免 Full Scan
與Full Scan相對應的是Filter Scan,也就是直接定位到索引數據所在數據頁,只讀取符合條件的索引。
注意,Filter Scan與Filter是完全不一樣的,詳見官方文檔。
Filter Scan

2. Apply Join與Hash Join的選擇

Apply Join

也就是Nested Loop Join,接收一組記錄作為input,然后分別對每條input進行Join,具體原理可以在網上搜到,這里就不多說。
操作符Cross Apply即代表Apply Join,它好處是,input越小,需要進行的join記錄數越少,讀取越少,速度越快。
可以說Apply Join是基于記錄的(record-based)。

Hash Join

與Apply Join相反,Hash Join是基于集合(set-based)的,對于參與Join的兩張表,會選擇更小的那一張,完全加載入內存,建立一個Hash表,再讀取另一張表,匹配Hash完成Join。
可以通過這篇文章理解Hash Join:《如何在分布式數據庫中實現 Hash Join?》

綜上所述,Hash Join適合需要整張表參與的大數據集的Join,而Apply Join適合記錄較少的Join
如果WHERE條件篩選后只有少量記錄,那么Apply Join是更好的選擇,此時如果選擇Hash Join,即使某張表被篩選出少量記錄,另一張表還是會被全表讀取,效率非常低。

3. 本機Join可減少開銷

本機的Join比Distributed Join更快、開銷更少,比如Cross Apply比Distributed Cross Apply更快,因此對于常用的Join,優化思路是進行本機Join避免Distributed Join。
Interleave是記錄co-located的強保證,因此必要的情況下,可以使用Interleave提升Join效率。
但是要注意Interleave的co-located保證也導致熱點不能被分散,因此需要綜合業務考慮后再決定是否使用Interleave。

4. 測試比Explaination更重要

查詢計劃不是萬能的,特別是僅僅使用Spanner Console的Explaination Only功能,是看不到最終掃描行數和執行效率的,對于查詢計劃的分析僅僅限于理論,理論必須結合實踐,因此非常有必要在測試環境模擬足夠的數據量去進行測試、調優、驗證。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374