Spanner會為每條SQL生成一個或多個查詢計(jì)劃吼拥,并選擇數(shù)據(jù)庫認(rèn)為最優(yōu)的那個查詢計(jì)劃去執(zhí)行,同一個SQL线衫,不同的查詢計(jì)劃最終的效率可能是千差萬別的凿可,理解查詢計(jì)劃是SQL優(yōu)化的基本必備技能。
Spanner本身有官方文檔幫助大家理解查詢計(jì)劃,但是講得比較精簡枯跑,如果對Spanner不熟悉惨驶,可能理解起來比較困難,本文是這篇文檔的擴(kuò)展敛助,但是會更淺顯易懂粗卜、詳細(xì),有一些總結(jié)與延伸纳击。
本文不會講什么:
- 查詢運(yùn)算符詳解(Query Operators)续扔,請自行參閱Spanner文檔
本文會講什么:
- 理解Spanner如何執(zhí)行一個查詢計(jì)劃
- 如何看懂GCP Console下獲取的Spanner的查詢計(jì)劃
- 如何基于查詢計(jì)劃作出優(yōu)化
一、查詢計(jì)劃如何被執(zhí)行
Spanner是分布式數(shù)據(jù)庫焕数,因此一個數(shù)據(jù)庫實(shí)例(Instance)是分布在多臺server的纱昧,因此一條SQL可能意味著需要多臺server配合才能產(chǎn)生最終結(jié)果。
client連接到Spanner堡赔,Spanner將SQL解析為查詢計(jì)劃(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é)果的行為辑甜,在查詢計(jì)劃中稱為Distributed Union
。
由于每臺server都負(fù)責(zé)保存多個splits袍冷,因此每臺remote server收到subplan后磷醋,會將subplan再次分割為一到多個splits的查詢計(jì)劃,下發(fā)給特定的split胡诗,每個split獨(dú)立執(zhí)行自己的計(jì)劃并返回結(jié)果給server邓线,這個過程在查詢計(jì)劃中稱為Local Distributed Union
。
總結(jié)一下:
Root Server負(fù)責(zé):
1. 下發(fā)subplan到其他參與的server
2. 等待所有server返回subplan結(jié)果給自己
3. 匯總各個server的執(zhí)行結(jié)果煌恢,如果需要的話骇陈,進(jìn)行進(jìn)一步處理
4. 將匯總后的執(zhí)行結(jié)果返回給client
Remote Server負(fù)責(zé):
1. 接收Root Server下發(fā)的subplan
2. 將subplan拆分成1個或多個分片的subplan并執(zhí)行
3. 匯總各個分片執(zhí)行的結(jié)果
4. 返回匯總的結(jié)果給Root Server
二、解讀查詢計(jì)劃
1. Example — 簡單查詢計(jì)劃
下圖是摘自Spanner官方文檔的查詢計(jì)劃
圖中箭頭由下往上瑰抵,表示的是結(jié)果返回順序你雌,而查詢計(jì)劃的分發(fā)順序恰恰相反,應(yīng)該由上往下二汛。
下發(fā)階段
圖中的查詢計(jì)劃表示SQL被解析為查詢計(jì)劃婿崭,發(fā)送給Root Server拨拓,Root Server進(jìn)行Distriubted Union
將subplan下發(fā)給Remote Server并等待最終結(jié)果。
Serialize Result
與Aggregate
都是對結(jié)果進(jìn)行處理的運(yùn)算符逛球,因此下發(fā)期間可以忽略千元。
Remote Server(s)收到Root Server的subplan后,將subplan拆分為特定split(s)的查詢計(jì)劃颤绕,交給特定的split(s)執(zhí)行幸海,也就是Local Distributed Union
。
在Local Distributed Union
下就是每個split會進(jìn)行的查詢計(jì)劃奥务,此時(shí)查詢計(jì)劃分發(fā)完畢物独,我們開始從下往上讀,解讀執(zhí)行與返回過程氯葬。
執(zhí)行與返回階段
每個split執(zhí)行Table Scan
挡篓,從Songs表讀取SingerId。
每一個被讀出的SingerId都會被Filter
操作符根據(jù)SingerId<100的條件過濾帚称,只有滿足條件的官研,才會往上返回。
被Filter
返回的數(shù)據(jù)會在Remote Server進(jìn)行Local Distributed Union
闯睹,也就是結(jié)果集的合并戏羽,并且再往上返回。
所有Remote Server都會將結(jié)果返回給Aggregator
操作符楼吃,進(jìn)行結(jié)果集的聚合始花。
聚合后的結(jié)果被Serialize Result
操作符組合為最終返回格式,這個操作符是每個查詢計(jì)劃都會有的孩锡,負(fù)責(zé)將查詢出的數(shù)據(jù)轉(zhuǎn)換為要發(fā)送回client的格式酷宵。
轉(zhuǎn)換為最終格式的數(shù)據(jù),進(jìn)行Distributed Union
躬窜,返回SQL執(zhí)行的最后結(jié)果浇垦。
整個查詢計(jì)劃結(jié)束
2. Example — 復(fù)雜查詢計(jì)劃
上面的簡單查詢計(jì)劃只包括一元操作符,下面講一下包含二元操作符的查詢計(jì)劃荣挨,比如進(jìn)行Join操作男韧。
注意:下圖生成的查詢計(jì)劃有個前提條件—— Albums表是Singers表的子表,兩者是Interleave關(guān)系垦沉。
下發(fā)階段
任何查詢計(jì)劃的分發(fā)都是差不多的煌抒,只有4個操作符涉及分發(fā),那就是Distributed Union
厕倍、Distributed Cross Apply
寡壮、Distributed Outer Apply
和Local Distributed Union
,因此這里不再講一遍。
執(zhí)行與返回階段
最底部是兩個并排的查詢計(jì)劃况既,應(yīng)該從左往右看这溅,左邊是input,右邊是對input進(jìn)行map處理棒仍,也就是說悲靴,查詢計(jì)劃是從下往上執(zhí)行,從左往右執(zhí)行莫其。
先對Albums表進(jìn)行Table Scan
查出SingerId癞尚、AlbumId、AlbumTitle三個字段乱陡。
Table Scan的結(jié)果會返回給Cross Apply
操作符浇揩,此操作符對結(jié)果進(jìn)行map,也就是為每個結(jié)果執(zhí)行一次Index Scan
憨颠。
Index Scan
查出SongName返回給Cross Apply
胳徽。
Cross Apply
將Table Scan與Index Scan的結(jié)果進(jìn)行Join,實(shí)際上Cross Apply
操作符就是進(jìn)行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進(jìn)行Distributed Union
將最終結(jié)果返回匙瘪。
整個查詢計(jì)劃結(jié)束铆铆。
3. Example — Distributed Cross Apply查詢計(jì)劃
上圖中的SQL需要讀2張表蝶缀,一張是索引SongsBySongName丹喻,一張是數(shù)據(jù)表Songs,索引無法Interleave翁都,所以索引和數(shù)據(jù)可以分別處于不同的分片碍论,那么要實(shí)現(xiàn)這個SQL,就不能使用
(Local) Cross Apply
柄慰,而需要使用Distributed Cross Apply
鳍悠,因此最頂層的操作符是Distributed Cross Apply
。
下發(fā)階段
Root Server的Distributed Cross Apply
會等待Distributed Union
后進(jìn)行Create Batch
的結(jié)果作為input坐搔,當(dāng)Distributed Cross Apply
收到Create Batch
的結(jié)果作為input后藏研,再下發(fā)plan給Remote Server,做map操作概行。
這里注意蠢挡,下發(fā)其實(shí)被分為了兩個階段,左邊先執(zhí)行完,Distributed Cross Apply才會進(jìn)行右邊的下發(fā)业踏。
執(zhí)行與返回階段
Remote Server將plan分配給多個splits進(jìn)行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)建中間表伐脖,因?yàn)樯婕暗娇鐂erver的join热幔,因此需要創(chuàng)建中間表。
將Create Batch創(chuàng)建的中間表作為input發(fā)給Distributed Cross Apply
操作符讼庇。
Distributed Cross Apply
下發(fā)查詢到Remote Server(進(jìn)行Map)断凶。
Batch Scan
讀取中間表并返回給Cross Apply
。
Cross Apply
根據(jù)Batch Scan結(jié)果進(jìn)行Join并通過Serialize Result
巫俺、Local Distributed Union
后返回給Root Server认烁。
Distributed Cross Apply
根據(jù)返回結(jié)果完成Join,返回SQL執(zhí)行結(jié)果介汹。
整個查詢計(jì)劃結(jié)束却嗡。
4. 從GCP Console解讀查詢計(jì)劃
在GCP Console中可以方便地獲得查詢計(jì)劃,但不是圖片形式嘹承,沒有左右關(guān)系窗价,因此我們需要將Console中的text展示的查詢計(jì)劃,在腦袋中轉(zhuǎn)換為圖片版的叹卷。
每行計(jì)劃的開頭都有一個小標(biāo)記撼港。
轉(zhuǎn)換原則是:
-
垂直箭頭則表示上下關(guān)系。比如:
-
人字型箭頭代表這是一個接收多個參數(shù)的操作符骤竹,比如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),越左邊个扰,從左往右依次排放瓷炮。
因此上圖應(yīng)該是如下:
三、SQL優(yōu)化
我們可以根據(jù)查詢計(jì)劃對SQL進(jìn)行優(yōu)化递宅,但是在優(yōu)化之前務(wù)必盡量讀懂查詢計(jì)劃娘香,因此需要了解每個操作符的意義冬筒,在進(jìn)行下面的閱讀前最好能夠先閱讀Spanner操作符文檔。
1. 為什么用了索引還是慢查詢茅主?
大家都知道全表掃描是嚴(yán)格禁止的(數(shù)據(jù)量特別小的表不在討論范圍)舞痰,導(dǎo)致慢查詢甚至拖垮數(shù)據(jù)庫,于是往查詢上面加索引诀姚,結(jié)果加了索引還是慢查詢响牛。
為什么會出現(xiàn)這種情況,是因?yàn)榇蠹液雎粤藢?dǎo)致慢查詢的根本原因——大量磁盤IO導(dǎo)致CPU和內(nèi)存被大量占用赫段,全表掃描不用說呀打,一定是大量的磁盤IO,把表依次讀一遍糯笙,實(shí)際上索引建得不好贬丛,也會有這種情況。
在Spanner中给涕,索引也是表豺憔,索引不過是只存儲部分字段的表而已,可以理解為一個比數(shù)據(jù)表更小的表够庙,如果查詢條件不能利用索引的最左前綴原則
恭应,那么這個索引就只能被全索引掃描,Spanner會將索引全部掃描一遍耘眨,利用Filter返回符合條件的行昼榛,對CPU和內(nèi)存的占用極大。
比如為users表建立一個 (user_name,email) 的索引剔难,卻使用這個索引進(jìn)行 SELECT user_name FROM users WHERE email = xxx 查詢胆屿,由于查詢條件不包括user_name,因此無法使用這個Index進(jìn)行Filter Scan
偶宫,也就是無法直接定位到索引所在數(shù)據(jù)頁非迹,而需要讀取整個索引,進(jìn)行Filter
操作读宙,也就是全表掃描
(索引也是表彻秆,因此對表和索引的全掃描都可以稱為全表掃描)楔绞。
從Spanner的查詢計(jì)劃中可以看到是否對一個索引或者一個表使用了全表掃描结闸,如下圖:
如果索引中有100萬條記錄,那么100萬條都會被讀入內(nèi)存進(jìn)行Filter酒朵,CPU和內(nèi)存壓力比較大桦锄,會出現(xiàn)慢查詢,因此
對于查詢計(jì)劃中的 Full Scan 需要根據(jù)SQL運(yùn)行頻率蔫耽、表大小進(jìn)行評估结耀,在必要的情況下建立更合適的索引避免 Full Scan
留夜。與Full Scan相對應(yīng)的是
Filter Scan
,也就是直接定位到索引數(shù)據(jù)所在數(shù)據(jù)頁图甜,只讀取符合條件的索引碍粥。注意,F(xiàn)ilter Scan與Filter是完全不一樣的黑毅,詳見官方文檔嚼摩。
2. Apply Join與Hash Join的選擇
Apply Join
也就是Nested Loop Join,接收一組記錄作為input矿瘦,然后分別對每條input進(jìn)行Join枕面,具體原理可以在網(wǎng)上搜到,這里就不多說缚去。
操作符Cross Apply
即代表Apply Join潮秘,它好處是,input越小易结,需要進(jìn)行的join記錄數(shù)越少枕荞,讀取越少,速度越快搞动。
可以說Apply Join是基于記錄的(record-based)买猖。
Hash Join
與Apply Join相反,Hash Join是基于集合(set-based)的滋尉,對于參與Join的兩張表玉控,會選擇更小的那一張,完全加載入內(nèi)存狮惜,建立一個Hash表高诺,再讀取另一張表,匹配Hash完成Join碾篡。
可以通過這篇文章理解Hash Join:《如何在分布式數(shù)據(jù)庫中實(shí)現(xiàn) Hash Join?》
綜上所述虱而,Hash Join適合需要整張表參與的大數(shù)據(jù)集的Join,而Apply Join適合記錄較少的Join开泽。
如果WHERE條件篩選后只有少量記錄牡拇,那么Apply Join是更好的選擇,此時(shí)如果選擇Hash Join穆律,即使某張表被篩選出少量記錄惠呼,另一張表還是會被全表讀取,效率非常低峦耘。
3. 本機(jī)Join可減少開銷
本機(jī)的Join比Distributed Join更快剔蹋、開銷更少,比如Cross Apply比Distributed Cross Apply更快辅髓,因此對于常用的Join泣崩,優(yōu)化思路是進(jìn)行本機(jī)Join避免Distributed Join少梁。
Interleave是記錄co-located的強(qiáng)保證,因此必要的情況下矫付,可以使用Interleave提升Join效率凯沪。
但是要注意Interleave的co-located保證也導(dǎo)致熱點(diǎn)不能被分散,因此需要綜合業(yè)務(wù)考慮后再決定是否使用Interleave买优。
4. 測試比Explaination更重要
查詢計(jì)劃不是萬能的著洼,特別是僅僅使用Spanner Console的Explaination Only
功能,是看不到最終掃描行數(shù)和執(zhí)行效率的而叼,對于查詢計(jì)劃的分析僅僅限于理論身笤,理論必須結(jié)合實(shí)踐,因此非常有必要在測試環(huán)境模擬足夠的數(shù)據(jù)量去進(jìn)行測試葵陵、調(diào)優(yōu)液荸、驗(yàn)證。