談談in常量查詢的設計與優化

介紹

如標題所示,這是一篇介紹in常量查詢的源碼解讀文章,但又不限於in常量查詢,因爲其中涉及的很多設計與優化對於大多數查詢都是普適的。 一如往常一樣,我們首先會過一遍整體的執行流程,梳理一個大致的框架。緊接着,同時也是更重要的,我們會通過一系列在真實場景中遇到的問題(說白了就是性能優化),來對各種細節處理進行增強。

溫馨提醒:建議有條件有興趣的同學可以對照着本篇文章邊調試(我基本上把重要的斷點位置都截了圖)邊學習邊思考,這樣印象和理解應該會更加深刻。

希望大家在讀完之後,可以嘗試着回答以下一些問題來進行某種測驗:

  • 什麼是分片裁剪?爲什麼要進行分片裁剪?
  • 爲什麼要對物理SQL中值進行裁剪?
  • 什麼是plan cache?爲什麼需要?
  • 爲什麼需要post planner ?
  • XPlan是什麼?爲什麼Xplan比物理SQL更優?
  • 爲什麼要有一個ToDrdsRelVisitor?
  • 什麼是全局二級索引?如何利用?
  • 以及其他散落於文章中或者閱讀時的問題

從大致的流程說起

注:詳細的執行流程請參考文章,PolarDB-X 源碼解讀(四):SQL 的一生 - 知乎,我們這裏只介紹其中幾個比較重要的環節。 我們拿一個非常簡單的場景來看一下吧,一個簡單的表如下,create table t(c1 int, c2 int, c3 int) dbpartition by hash(c1) tbpartition by hash(c1) tbpartitions 2,一條最簡單的SQL如下:select c3 from t where c1 in (1,2)。挑了五個階段進行了並不太詳盡的說明,如果你感覺比較抽象時,也可以動手調試一下,一些概念應該就會更加清晰了。

階段一

我們需要將SQL文本解析爲語法樹,如果不合法,則報錯,關鍵斷點如下圖,其中sql爲輸入的查詢語句,statement爲經過解析後的語法樹。

需要注意的是,在這個地方,我們是隻進行語法解析,而不進行語義解析。什麼意思呢,比如你現在輸入的SQL爲select c1 from tt,此時雖然我們沒有tt這張表,但是斷點處還是會正常解析出一個SQLSelectStatement,有興趣的同學可以打個斷點試一下。

階段二

如上分析,我們現在要進行語義的校驗了,比如我怎麼知道這張表存不存在,以及是否含有這個列呢?

階段三

構建執行計劃,在toRel時將由 SqlNode 構成的 AST 轉換爲由 RelNode 組成的邏輯計劃。

埋一個坑把,有興趣的同學可以結合代碼思考一下,既然我們已經拿到了邏輯執行計劃,那麼ToDrdsRelVisitor的作用是什麼呢?

階段四

對執行計劃進行優化,以期獲得較爲優異的執行效果。

階段五

拿到執行計劃之後,緊接着我們來看一下是在哪裏執行的,以及是如何執行的

我們可以簡單看一下這個plan,這是一個非常簡單的plan,最上層是一個Gather用來聚合下層多個logicalView的結果,而logicalView中包含了如何與存儲節點進行交互的信息。 根據plan拿到相應的handler,然後進行調用就可以了。

在這個場景中,我們會遞歸調用logicalView的handler。


OK,以上就是一個大概的執行流程,接下來我們來真正深入到一些細節看一下,我們如何將這個大致的流程進行豐富以使其能夠滿足工業生產的需求。

現實中的使用場景

In查詢列表中的值不固定,個數亦不固定。

優化思路

單條SQL的優化,比如分片裁剪,物理SQL中in值的裁剪,使用XPlan代替物理SQL。
大量執行相似的SQL時,避免重複性且不必要的工作,如避免每次重新生成plan。
對其中一些特殊場景進行更加極致的優化,比如單分片直接下推。
通過添加索引進行優化,在這裏我們主要討論全局二級索引。

具體的優化

單條SQL的優化

分片裁剪:只訪問必須訪問的分片

Q:select from t where c1 in (1,2) 會向所有分片下發物理SQL麼? A:不會的。通過上面的分析,我們下發的物理SQL爲select from t_physical_table where c1 in (1,2),t_physical_table爲邏輯表t所對應的物理表。而由於表t的分庫鍵和分表鍵均爲c1,因此顯然我們只需要向兩張可能存在匹配記錄的物理表下發物理SQL即可,獲取裁剪後的分表信息如下圖。

分片裁剪是一定需要調用分片計算,分片計算的邏輯在這裏。

物理SQL中in值的裁剪:只留下有用的in值

Q:下發的物理SQL中,是否會對in的列表進行裁剪呢? A:會的,而且對下發的物理SQL中的in列表中的值進行裁剪,主要有兩個好處,一是儘可能避免下發的物理SQL導致不必要的全表掃描,二是減少下發物理SQL的長度。

上圖中PruneRaw即代表裁剪後的in查詢列表。

使用XPlan代替物理SQL:避免DN節點進行物理SQL的解析優化

注:詳情可參考鏈接文章中的執行計劃傳輸部分,https://zhuanlan.zhihu.com/p/308173106#:~:text=PolarDB-,%E8%BF%9B%E8%A1%8C%E4%BA%86%E7%89%B9%E6%AE%8A%E4%BC%98%E5%8C%96%E3%80%82 Notice:in查詢其實暫時是不支持傳輸執行計劃的。 但我覺得可能沒什麼特別特殊的地方,像傳輸其他的plan一樣,我們需要在計算層指定數據的訪問方式(即指定索引),然後進行適配和對接。

避免每次重新構建plan

避免參數值不同而反覆構建plan

Q:每次都進行plan的構建,看起來並不是非常有必要,比如select from t where id in (1,2) 和select from t where id in (2,3)。
A: 是的,所以我們對plan進行了緩存,這就是PlanCache組件,可以理解爲Map。很自然的,我們需要對上述兩條SQL進行參數化以便從map中進行查找,即參數化爲select * from t where id in (?,?)的形式,代碼在

避免參數個數不同而反覆構建plan

Q:細心的同學可能感覺有點奇怪,上面的select c1,c3 from t where c1 in (1,2) 參數化後爲 select c1,c3 from t where c1 in (?),而非select c1,c3 from t where c1 in (?,?),這是爲什麼?
A:這樣做是爲了避免plan cache的膨脹,因爲這樣參數化之後,select c1,c3 from t where c1 in (1,2) 和select c1,c3 from t where c1 in (1,2,3,4)就是共用一個plan cache了;此外,這樣還可以減少參數化SQL佔用的內存,想象一下,有些SQL中in列表中的值多達幾十萬個呢。

單分片場景優化

Q:對於某些場景,是否有更近一步的優化,畢竟TP是需要儘可能的高性能的。
A:有的,比如單分片的場景,in列表中的值會落在同一個物理分表上。 我們可以思考下此時下面的執行計劃是否可以簡化?

答案是顯然的,在單分片場景下,上層的Gather是完全不需要的,否則我們在執行時會有額外的執行開銷。 引申:我們可以再結合前面的參數化與plan cache來理解這個問題,即參數不同的SQL的最優執行計劃其實並非總是相同的,但我們爲了避免每次重複生成plan,又會緩存一個plan,於是我們需要一個能夠對plan進行優化的能力。 我們大概可以把這種情況分成兩種,一種是參數不同導致選擇的join算法不同,比如是選擇bka join還是hash join,爲了解決這個問題,我們引入了執行計劃管理模塊(SPM);另一種則跟我們的架構有非常大的關係,因爲我們下層的DN(可以簡單理解爲mysql)顯然是具備執行各種SQL的能力的,而如果在某些參數下,經過裁剪後只剩下一個分片了,則該SQL經過物理表名的替換後可直接下發到DN執行,計算層只需要等待結果返回即可,無需做任何其他的操作。爲了實現第二種效果,我們在planner階段增加了一個階段,叫做post planner,在post planner中會判斷是否能夠下推到某個分片,默認爲打開,上圖中爲了演示需要,特意使用hint進行了關閉。

添加全局二級索引

注:索引,本質是一種修改與查詢的權衡,需要用戶謹慎考慮,尤其寫入全局索引會帶來較大的分佈式事務開銷。

Q:分片建已經確定了,in查詢的字段沒有跟分片對齊,是不是無法做分片裁剪了,還能優化麼?
A:可以考慮增加全局二級索引。 我們來舉個例子吧,比如table: t3(c1 int, c2 int, c3 int) dbpartition by hash(c1); SQL爲select c3 from t3 where c2 in (1,2),由執行計劃可知我們無法進行分片裁剪,因此需要訪問所有8個分片,如下:

現在讓我們來考慮一下如何優化? 我們的目的是希望減少訪問的分片數,而之所以無法進行分片的裁剪,是因爲in查詢的字段和分片鍵沒有對齊。於是解決方案也很簡單,我們增加一個拆分鍵與in查詢字段對齊的全局的二級索引即可,有關全局二級索引的介紹,可參考鏈接,https://help.aliyun.com/document_detail/182179.html。 比如,我們執行如下添加全局二級索引的SQL,alter table t3 add global index g_c2(c2) covering(c1, c3) dbpartition by hash(c2),然後我們再來看下此時的執行計劃,發現此時已經如我們所料進行了基於全局二級索引的分片裁剪,現在只需要掃描兩個分片即可。

一個小練習

In列表中包含大量重複值時,可以如何優化?(我們現在的版本沒有考慮這種情況) 比如,有一個很簡單的做法,在參數化時加一個去重,如下圖。

然後大家可以思考一下,需要注意什麼,以及有什麼問題麼?

One More:橫向對比與思考

大家有興趣,有時間的,可以對比其他友商數據庫進行比較與分析。

總結

其實我在這篇文章裏面,拋了挺多問題,有些給了一種便於敘述卻未必全面的答案,有些則完全沒有回答。最後的這個總結我覺得也留給大家來寫了。

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章