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

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

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

本文不會講什么:

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

本文會講什么:

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

一、查詢計劃如何被執(zhí)行

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

查詢計劃如何被分發(fā)

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

image.png

總結(jié)一下:
Root Server負責(zé):

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

Remote Server負責(zé):

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

二、解讀查詢計劃

1. Example — 簡單查詢計劃

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

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

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

執(zhí)行與返回階段

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

整個查詢計劃結(jié)束

2. Example — 復(fù)雜查詢計劃

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


摘自Spanner官方文檔的復(fù)雜查詢計劃
下發(fā)階段

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

執(zhí)行與返回階段

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

整個查詢計劃結(jié)束。

3. Example — Distributed Cross Apply查詢計劃

摘自Spanner官方文檔的Distributed Cross Apply

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

下發(fā)階段

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

執(zhí)行與返回階段

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

整個查詢計劃結(jié)束。

4. 從GCP Console解讀查詢計劃

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


Console查詢計劃

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

  • 垂直箭頭則表示上下關(guān)系。比如:


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


    Hash Join是二元操作符
  • 直角箭頭代表是父操作符的輸入?yún)?shù),比如圖中兩個Distributed Union不是上下級關(guān)系,而是兄弟關(guān)系,作為Hash Join的下級操作符,也就是Hash Join的輸入?yún)?shù),兩個Distributed Union應(yīng)該是左右排列。

  • 對于應(yīng)該左右排列的操作符,越上面出現(xiàn),越左邊,從左往右依次排放。


    左右關(guān)系

因此上圖應(yīng)該是如下:


查詢計劃圖

三、SQL優(yōu)化

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

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

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

Full Scan

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

2. Apply Join與Hash Join的選擇

Apply Join

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

Hash Join

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

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

3. 本機Join可減少開銷

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

4. 測試比Explaination更重要

查詢計劃不是萬能的,特別是僅僅使用Spanner Console的Explaination Only功能,是看不到最終掃描行數(shù)和執(zhí)行效率的,對于查詢計劃的分析僅僅限于理論,理論必須結(jié)合實踐,因此非常有必要在測試環(huán)境模擬足夠的數(shù)據(jù)量去進行測試、調(diào)優(yōu)、驗證。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容