在 8 月 13 日的 TDengine 開發者大會上(shang),TDengine 計(ji)(ji)算引擎架(jia)構(gou)師廖(liao)浩(hao)均帶(dai)來題(ti)為《TDengine 3.0——全(quan)新計(ji)(ji)算查(cha)詢(xun)引擎的設計(ji)(ji)》的主題(ti)演(yan)講,詳細闡述了(le) TDengine 3.0 計(ji)(ji)算查(cha)詢(xun)引擎技術的優化與升級。本文根據此(ci)演(yan)講整理而成。
點擊【這里】查看完整演講視頻
3.0 中(zhong)的(de)(de)查詢引(yin)擎在 2.0 版(ban)本(ben)的(de)(de)基(ji)礎上(shang)進(jin)行(xing)了(le)重寫,在承襲 2.0 查詢引(yin)擎既有(you)優勢和(he)(he)技(ji)術特性的(de)(de)基(ji)礎上(shang),在工程實現和(he)(he)整(zheng)體(ti)架(jia)構設計上(shang)有(you)了(le)極大的(de)(de)改善(shan)和(he)(he)提升。
總(zong)體來講(jiang),TDengine 3.0 大幅(fu)增(zeng)強(qiang)了(le)對 SQL 語義的(de)(de)(de)支持、完(wan)善(shan)了(le) SQL 查(cha)詢(xun)語法;強(qiang)化了(le)整體執行框架(jia)及 SQL 查(cha)詢(xun)的(de)(de)(de)調度能(neng)力;提供(gong)更好的(de)(de)(de)執行任務隔離機制,對于錯誤具備更好的(de)(de)(de)容(rong)忍度;提供(gong)支持存算分離架(jia)構的(de)(de)(de)能(neng)力。它主要包含以下(xia)幾個方面的(de)(de)(de)特點(dian):
- 支持標準 SQL 查詢語法
- 全新的 SQL 解析器、計劃生成及優化器,引入偽列 (_wstart/_wend/_qstart/_qend/_rowts) 等語法概念和新的(分組)關鍵詞,全面增強查詢語法。
- 消除 2.x 版本中對時序數據查詢施加的諸多限制,可以像使用關系對象數據庫查詢引擎一樣使用 SQL 查詢語法。
- 支持標簽/普通列的運算,標量函數和矢量函數的嵌套、任意字段(列)的分組/排序/聚合、無限制的多級子查詢。
- 支持存算分離架構
- 支持存算分離的體系架構,支持集群內計算節點動態、快速、彈性部署。
- 查詢引擎能夠將查詢按需調度到計算節點,有效降低存儲節點(Vnode)計算資源的壓力。
- 原生的流批一體化查詢處理
- TDengine 3.0 原生支持流計算和交互式查詢。流計算引擎也使用 SQL 語言作為交互途徑,因此,流計算引擎直接使用查詢框架的 SQL 解析處理、執行計劃生成、查詢執行算子、用戶應用交互等諸多方面的能力。
- 更優秀的魯棒性和韌性設計
- 為避免用戶錯誤導致的主服務宕機,用戶定義函數的執行服務(UDFD)與 TDengine 的主進程進行隔離,UDFD 可以在崩潰以后自動被重新拉起。
- 對于 SQL 查詢轉化為調度器的查詢任務(Job),Job 在被分解以后會按層級調度執行,并且同層次任務相互獨立執行,單個任務失敗以后可以重新拉起進行處理。
- 對于不同分層的任務(Task),失敗任務可以從下游任務的結果緩存(Sink Node)中再次請求數據并嘗試重新執行。
- 更好的執行可觀測性
- 將查詢計劃、執行開銷、查詢中 Task 的動態執行狀態等相關信息通過命令行交互界面(CLI)呈現給用戶,助力用戶定位查詢執行的瓶頸,并進行針對性的優化處理。
- 提供 Explain 指令,用戶可以通過 Explain 獲得查詢執行的計劃、各算子執行開銷等信息,為用戶層面 SQL 改寫和優化性能提供支持。
- 通過心跳信息,將服務端執行狀態推送到調度器,并反饋給用戶,讓用戶更好確定查詢執行的狀態。
- 支持數據集快照查詢
- 全面內置版本化的查詢支持能力,可以為事件驅動流計算引擎提供指定版本和時間范圍的歷史數據追溯查詢。
- 為雙(多)活集群提供數據同步版本化讀取能力。
查詢引擎架構設計

上圖是(shi)(shi) TDengine 3.0 的(de)(de)查詢引擎架構,左邊的(de)(de) Query Wrapper 運行(xing)在(zai)(zai) Vnode 和(he) Qnode 上,它里面包(bao)含了一個(ge)執(zhi)行(xing)器,其(qi)中的(de)(de) Index 模塊(kuai)是(shi)(shi)處(chu)(chu)理 Vnode 中存儲(chu)的(de)(de)標簽數據(ju)的(de)(de)索(suo)引。執(zhi)行(xing)器本身(shen)并不(bu)處(chu)(chu)理與(yu)調度器的(de)(de)交(jiao)互(hu)行(xing)為,Query Wrapper 負責處(chu)(chu)理執(zhi)行(xing) Task 動作并將結果緩(huan)存在(zai)(zai) Sink Node;此外,執(zhi)行(xing)器會調用 function 模塊(kuai)和(he) ScalarFunc 模塊(kuai)進行(xing)計算處(chu)(chu)理,并在(zai)(zai)必要(yao)的(de)(de)情況下通(tong)過(guo) function 模塊(kuai)調用 UDF 服(fu)務。
中間的(de)(de)模塊(kuai)(kuai)是由 Query Wrapper 和(he) libtaos.so 所共享的(de)(de),里面包含的(de)(de)是 function(系統中其他所有函(han)數的(de)(de)定(ding)義模塊(kuai)(kuai))和(he) ScalarFunc(標(biao)量函(han)數以及(ji)過濾的(de)(de)模塊(kuai)(kuai))。需(xu)要注(zhu)意的(de)(de)是,在 function 模塊(kuai)(kuai)中還包含了一個 UDFD 的(de)(de)計算模塊(kuai)(kuai),它是用戶定(ding)義程序的(de)(de)執行服務端(duan),運行在一個獨立(li)的(de)(de)進程空(kong)間。
最右邊的模塊是運(yun)行(xing)在用(yong)(yong)戶進(jin)程空間中的驅動 libtaos.so,Driver 是 libtaos.so 里的一個驅動,它(ta)負(fu)責(ze)串聯(lian)各個模塊來(lai)完成查(cha)詢(xun)的操作(zuo),Parser 用(yong)(yong)來(lai)調用(yong)(yong)分詞(ci)器(qi)與語法分析器(qi)進(jin)行(xing)語法分析,生成語法樹(AST),Catalog 負(fu)責(ze)從 Mnode 和 Vnode 中獲(huo)取各種元(yuan)數據,Planner 負(fu)責(ze)將 AST 和 元(yuan)數據信息轉(zhuan)化為邏輯執行(xing)計劃(hua)、物理執行(xing)計劃(hua)、并(bing)進(jin)行(xing)執行(xing)計劃(hua)重寫,最后(hou)生成分層查(cha)詢(xun)計劃(hua),Command 負(fu)責(ze)執行(xing)本地化查(cha)詢(xun)操作(zuo)。
相比于 2.0,3.0 在模塊化(hua)(hua)和工程(cheng)化(hua)(hua)上做了(le)較多的優化(hua)(hua),主(zhu)要可以歸結為以下幾方面:
- 客戶端不參與計算,計算過程在服務端完成
- UDF 在獨立的進程空間中運行,與主服務進程隔離
- Qnode-aware 的自適應查詢任務調度策略
- Index-aware 的標簽過濾機制
- 節點間/算子間采用標準的列格式(columnar data)數據進行數據傳遞
- 進行計算的節點會通過心跳將查詢執行信息返回給調度器
- 重寫查詢框架,形成了高內聚、低耦合的框架模塊
時序數據查詢處理流程

首先(xian),Parser 會(hui)把圖上(shang)的 SQL 語(yu)句轉成(cheng)一個(ge)抽象(xiang)語(yu)法樹,之后會(hui)依托(tuo)于(yu) Catalog 節點拉(la)取相應的元數據(ju),然后將信息(xi)傳遞到(dao) planner,生(sheng)成(cheng)如圖上(shang)所示的邏輯計劃(hua)。之后在(zai)邏輯計劃(hua)層面會(hui)生(sheng)成(cheng)四個(ge) node,自底向上(shang)分別是 Scan、Partition、Window、Project,它們分別對應 SQL 語(yu)句里(li)不同的幾(ji)個(ge)關鍵詞。

邏輯計(ji)(ji)(ji)劃傳遞下去(qu),最(zui)后(hou)會(hui)在 planner 中生成(cheng)一個(ge)(ge)物理(li)計(ji)(ji)(ji)劃。上圖(tu)所示的紅色(se)虛(xu)線以(yi)下部分(Partial Agg)是(shi)對物理(li)計(ji)(ji)(ji)劃的執行,包括 VgID:#1 和VgTD:#2 兩個(ge)(ge)子任務(wu);虛(xu)線上層是(shi)一個(ge)(ge)Global Merge,是(shi)最(zui)后(hou)的一個(ge)(ge)聚合(he)階段,Partial 聚合(he)結果經過(guo) merge sort 后(hou)再次進(jin)行全局聚合(he),形(xing)成(cheng)結果以(yi)后(hou)送到(dao)計(ji)(ji)(ji)算節點本地的 sink node 進(jin)行緩存,等(deng)待(dai) scheduler 來拉(la)取最(zui)后(hou)的計(ji)(ji)(ji)算結果。
在(zai)之前邏輯計劃(hua)中(zhong)(zhong)的 Logic partition 在(zai)物理計劃(hua)中(zhong)(zhong)卻看不到了(le),其(qi)實(shi) Partition 分(fen)(fen)(fen)組(zu)邏輯是被下沉(chen)到了(le) TableScan 完成(cheng),這是一個優化(hua)操(cao)作(zuo)(zuo)。按照(zhao)標簽分(fen)(fen)(fen)組(zu)機制,同一個表中(zhong)(zhong)數(shu)(shu)據(ju)(ju)一定同一分(fen)(fen)(fen)組(zu),我們在(zai) TableScan Node 中(zhong)(zhong)建立(li)表分(fen)(fen)(fen)組(zu)的映射關系即可,再將(jiang)數(shu)(shu)據(ju)(ju)塊打上 GroupID 就完成(cheng)了(le)分(fen)(fen)(fen)組(zu)操(cao)作(zuo)(zuo),不需要對每一條(tiao)數(shu)(shu)據(ju)(ju)進行掃描。
物理計(ji)劃(hua)的(de)(de)(de)具體實現在(zai) TDengine 系(xi)統執行的(de)(de)(de)日(ri)志(zhi)里面可以看到,感興趣的(de)(de)(de)同學可以去看看,系(xi)統中的(de)(de)(de)物理計(ji)劃(hua)采用 JSON 形式(shi)的(de)(de)(de)文本呈(cheng)現。
在實際操作中(zhong),物理計劃最終(zhong)會(hui)傳遞到調度器(qi)中(zhong)執(zhi)行,調度器(qi)工作的元素就是(shi) Task 和 Job,每(mei)個SQL 查詢是(shi)一個 Job,每(mei)個 Job 由若干(gan)個 Task 構成,每(mei)個 Task 包含了其執(zhi)行的節(jie)點信息、任務(wu) ID、需要執(zhi)行的物理計劃等信息,不同的 Task 會(hui)形(xing)成一個層(ceng)級化(hua)的樹形(xing)結構。
物理(li)計劃傳遞到調度器(qi)后,虛線(xian)下(xia)面的(de)(de)兩個(ge)(ge) Partial Agg 會被轉(zhuan)化成 Subplan #1(level 1) 和 Subplan #2(level 1),這兩個(ge)(ge)執(zhi)(zhi)行(xing)(xing)計劃會分別(bie)發送到不(bu)同的(de)(de) Vnode 進行(xing)(xing)執(zhi)(zhi)行(xing)(xing)。虛線(xian)上層(ceng)也(ye)是(shi)一個(ge)(ge) Task [Subplan #0(level 0)],等下(xia)層(ceng)兩個(ge)(ge) Task 執(zhi)(zhi)行(xing)(xing)完成或至少返回(hui)第一條記錄時(shi),調度器(qi)就會拉(la)起(qi)上一層(ceng) level 0 的(de)(de) Task 進行(xing)(xing)執(zhi)(zhi)行(xing)(xing),它(ta)是(shi)層(ceng)級化的(de)(de)執(zhi)(zhi)行(xing)(xing)方式。

時序數據查詢處理的(de)整體流程如上(shang)圖所示,其中計算節點(dian)的(de)選(xuan)擇(ze)主(zhu)要有以(yi)下幾點(dian)策略(lve):
- 部分聚合計算的計算節點由于聚合計算下推,要求在保存有數據的 Vnode 上進行,以減小數據在集群中傳輸帶來的開銷;
- 對于全局聚合,可以在集群中所有可用節點中任意選取,為了減小數據在集群中傳輸的開銷,一般會選擇進行局部聚合(Partial Agg) 的節點進行全局聚合;
- 如果在集群中存在 Qnode(計算節點),調度器會優先將后續階段的計算調度到計算節點上進行計算。
時序數據查詢優化策略
排序消除
排序(xu)是查(cha)詢過(guo)程中,I/O 和 CPU 開(kai)銷都(dou)非常高的操(cao)作(zuo)。如(ru)果查(cha)詢結果要(yao)求按照 timestamp 排序(xu)(升序(xu)或降序(xu))輸出,由于時(shi)序(xu)數據在(zai) TDengine 中就是按照時(shi)間(jian)序(xu)列(lie)存儲,所以排序(xu)的操(cao)作(zuo)可用直(zhi)接消(xiao)除,只需要(yao)將 TableScan 指定為升序(xu)/降序(xu)掃描返回結果即可。
排序優化

操作(zuo)邏輯(ji)如上圖所示。對(dui)于分布式的結果排序(xu)(xu)輸出,排序(xu)(xu)操作(zuo)充分利用(yong)時(shi)序(xu)(xu)數據有序(xu)(xu)性,如果我們使(shi)用(yong)歸(gui)并排序(xu)(xu)替(ti)代完全排序(xu)(xu),就(jiu)能避(bi)免在(zai)標準外存排序(xu)(xu)過程(cheng)中觸發的 IO 操作(zuo)。這一(yi)操作(zuo)主要在(zai)超級(ji)表查詢(xun)的場景中使(shi)用(yong)較多。
數據替代
從這(zhe)一(yi)條優化策略開始,需要給大家補充一(yi)下關于(yu) SMA 的背景知識。SMA 是(shi) Small Materialized Aggregates 的簡稱。對(dui)于(yu)每個落盤的數據(ju)塊(Block)都會生成(cheng)一(yi)個對(dui)應的 SMA 信息(xi),其中包含了每列(lie)數據(ju)的最大值、最小(xiao)值、NULL 數量信息(xi)。這(zhe)些(xie)信息(xi)針對(dui)數值型列(lie)都存在,必要時(shi)可以替代數據(ju)參與計算。相(xiang)對(dui)于(yu)具體的數據(ju)來說,SMA 占據(ju)的磁(ci)盤空(kong)間(jian)非常(chang)小(xiao),因此(ci)能(neng)夠極大地(di)提升(sheng)某些(xie)種類(lei)的查詢性能(neng)。

基(ji)于 SMA 的(de)優化策略之一(yi)(yi)就是(shi)(shi)(shi)數(shu)(shu)據(ju)替代。針對(dui)每(mei)個函(han)數(shu)(shu),TDengine 定義(yi)了其(qi)需(xu)要的(de)數(shu)(shu)據(ju)范圍,上表(biao)是(shi)(shi)(shi)一(yi)(yi)些聚合函(han)數(shu)(shu)的(de)數(shu)(shu)據(ju)計(ji)算(suan)需(xu)求列表(biao)。在(zai) SQL 解析階段,如果(guo)確(que)認其(qi)最終的(de)數(shu)(shu)據(ju)需(xu)求是(shi)(shi)(shi) Small Materialized Aggregates (SMA),最終只會讀取 SMA 進行計(ji)算(suan)。但并非任何情況下都可以使(shi)用 SMA,對(dui)于所有標量(liang)函(han)數(shu)(shu),一(yi)(yi)旦在(zai)查(cha)詢請求里面(mian)出(chu)現(xian),SMA 是(shi)(shi)(shi)不參(can)與到計(ji)算(suan)里面(mian)來的(de)。
在使用 SMA 時,查詢優化(hua)器會告訴執(zhi)行節點,只(zhi)需要去讀取(qu)每個 block 的(de) SMA 信息就行,在整(zheng)個查詢過程中,執(zhi)行器不會再去讀取(qu)任何一條具體的(de)數據,所(suo)以它的(de)流程非常快。
數據掃描優化
TDengine 中定義了數據順序敏感型函數,在 SQL 語句中使用該類型的函數會觸發優化器使用特定的數據文件掃描策略。在 SQL 語句 SELECT last(ts),count(*) FROM foo_table_name 中,Last 函(han)數返回最后一個非 NULL 值(zhi),所以逆序(xu)(xu)掃描(miao)能夠更(geng)快獲得結果。而(er) count 函(han)數對于掃描(miao)的順序(xu)(xu)并(bing)不敏感(gan),因(yin)此,優(you)化器(qi)會(hui)指令 TableScanNode 采用逆序(xu)(xu)掃描(miao)策略(lve)。
動態裁剪 Block

對(dui)于某些查(cha)詢函數(shu),例(li)如:last/first/top/bottom 查(cha)詢,可以使(shi)用中間結果來動(dong)態裁剪讀取的 block 的信息(xi)。
在執行過程之中,根據中間計算的結果及當前結果可以確定,下一個需要掃描的 block 是否需要真正地讀取。依然以 last 為例 SELECT last(ts) FROM foo_table_name,在(zai)每次需要讀(du)取下一個 Database 時,首先使用 ts=100 過濾每個數(shu)據塊,如果數(shu)據塊包(bao)含(han)的(de)時間范圍晚(wan)于 ts=100, 才會讀(du)取該數(shu)據塊數(shu)據并進行(xing)計算,大于 ts=100 的(de)就(jiu)自動跳過,這就(jiu)實現了動態裁剪 Block 的(de)功(gong)能。
基于 SMA 的預過濾
在進行 Last 查詢時(shi),SMA 會被用來做預(yu)過(guo)(guo)(guo)濾,其中保(bao)存了與之對應的(de)(de)數據塊的(de)(de)四(si)項統(tong)計信息:最大值(zhi)、最小值(zhi)、和 NULL 的(de)(de)數量。所(suo)有的(de)(de)針(zhen)對數據的(de)(de)過(guo)(guo)(guo)濾條(tiao)件首先應用在 SMA 之上,SMA 滿足(zu)過(guo)(guo)(guo)濾條(tiao)件以后,再(zai)讀取(qu)數據塊,然后再(zai)次進行針(zhen)對數據的(de)(de)過(guo)(guo)(guo)濾。
以 SELECT last(ts),count(*) FROM foo_table_name WHERE k > 20 為例,這里面有一個過(guo)濾(lv)條件(jian)是(shi) K 大(da)于(yu)(yu)(yu) 20,如果其中存儲著關(guan)于(yu)(yu)(yu) K 那(nei)一列(lie)的最大(da)值(zhi)、最小值(zhi)信息,就可(ke)以(yi)(yi)以(yi)(yi)此為條件(jian)進行過(guo)濾(lv),如果其中的 MAX 值(zhi)都小于(yu)(yu)(yu) 20,那(nei)么這個 block 也(ye)會在 TableScanNode 里面讀取真實數據之前就丟棄。
Interval SMA
時序數據庫(Time Series Database,TSDB)一個重(zhong)要的(de)應(ying)用(yong)場景就(jiu)是(shi)為看板(ban)(ban)(Dash Board)提供定(ding)時的(de)查詢(xun)(Standing Query, 應(ying)用(yong)或程(cheng)序按(an)照固(gu)(gu)定(ding)頻率(lv)發(fa)(fa)出的(de)查詢(xun))支持,看板(ban)(ban)應(ying)用(yong)以(yi)固(gu)(gu)定(ding)的(de)頻率(lv)向應(ying)用(yong)及時序數據(ju)庫發(fa)(fa)出查詢(xun)執行,并在可接受的(de)時間范圍(wei)內要求獲得查詢(xun)結(jie)果。
針(zhen)(zhen)對 Standing Query 的(de)執行需求,為(wei)了提(ti)升響應速(su)度,節約重復計算(suan)帶(dai)來(lai)的(de)資(zi)源開銷。在 TDengine 3.0 中(zhong)提(ti)供(gong)了使用流計算(suan)引(yin)擎的(de)異步 interval SMA(將針(zhen)(zhen)對時(shi)間窗(chuang)口的(de)聚合查(cha)詢(xun)轉化為(wei)投(tou)影查(cha)詢(xun),因為(wei)計算(suan)結果已經通過(guo)流計算(suan)引(yin)擎計算(suan)完成(cheng)并(bing)寫回到 TDengine)。
事實(shi)上,上述優化(hua)(hua)并(bing)不是 3.0 查(cha)(cha)詢計(ji)算引(yin)擎(qing)里包含(han)的所(suo)有(you)優化(hua)(hua)策(ce)略,只是涉(she)及到了(le)幾個較(jiao)為經典的查(cha)(cha)詢場(chang)景(jing)。關于查(cha)(cha)詢引(yin)擎(qing)的優化(hua)(hua)是非常瑣(suo)碎(sui)的,有(you)些優化(hua)(hua)策(ce)略只針對一(yi)種(zhong)場(chang)景(jing),甚(shen)至如果改寫一(yi)下(xia)語句(ju)順序可能就不生(sheng)效(xiao)了(le)。如果大家還(huan)想了(le)解更多的查(cha)(cha)詢引(yin)擎(qing)優化(hua)(hua)策(ce)略,可以(yi)去 GitHub 上查(cha)(cha)閱 3.0 的代碼,或者直(zhi)接下(xia)載進行體驗。
結語
接下(xia)來,關于 TDengine 查詢引擎(qing)的優化,大(da)致(zhi)分為以下(xia)幾(ji)點:
- 標量計算庫采用 SIMD 進行優化:采用 SIMD 指令加速標量計算的執行速度
- 支持多種腳本語言的 UDF/UDAF:3.0 現在支持 C 語言定義的 UDF/UDAF, 后續將支持腳本語言定義的 UDF/UDAF,提供更便捷的使用體驗
- 支持高精度數值類型和二進制類型數據:高精度 Decimal 類型和 binary(1MB)類型數據的存儲、讀取和查詢
- 完善查詢優化器:查詢優化器針對更多的查詢場景和用例提供高效率的優化,降低 SQL 執行的復雜程度
- 支持多源數據的聯邦查詢:支持使用外部數據源的聯邦查詢,通過定義連接器,將其他的數據平臺作為查詢的基礎數據源
- 提供更豐富的時序相關計算及分析函數和 SQL 查詢語法:部分聚合函數將支持 CASE/WHEN、CTE 語法、SQL Hint,以及針對數據分析應用的需求,并提供更多的內置 SQL 函數
除此之外,后續我們還將完善查詢內存控制和任務執行/調度策略,在現有的(de)(de)(de)隨機(ji)分(fen)配任務(wu)(wu)的(de)(de)(de)基(ji)礎上,基(ji)于 Qnode 的(de)(de)(de)運行(xing)負載狀況(kuang),調(diao)度查詢(xun)(xun)任務(wu)(wu)的(de)(de)(de)執行(xing);提供更好的(de)(de)(de)內存控制策略(lve),降低百萬(wan)/千萬(wan)級別表查詢(xun)(xun)過程中元(yuan)數據追蹤帶(dai)來的(de)(de)(de)內存抖動;針對查詢(xun)(xun)范圍進行(xing)橫向的(de)(de)(de)數據查詢(xun)(xun)范圍切分(fen),在同一(yi)個 vnode 中并(bing)行(xing)拉起多個查詢(xun)(xun)執行(xing)同一(yi)個查詢(xun)(xun)處理。
總而言之,后面我(wo)們還(huan)有更多(duo)的(de)工作要(yao)進(jin)行,會繼續優化 TDengine 3.0 查(cha)詢引擎的(de)性(xing)能,也歡迎(ying)更多(duo)的(de) TDengine 關注者(zhe)和支持者(zhe)交流應用體(ti)驗,幫(bang)助(zhu)我(wo)們進(jin)步。


























