摘要:同樣的,對于上圖表達式樹中的表達式,計算前需要將其參數分別轉化為類型對于表達式,計算前需要將其參數分別轉化為類型。成為獲贈限量版馬克杯,馬克杯獲取流程如下提交提交之后,請耐心等待維護者進行。當收到兩個及以上的后,該將會被合并。
這是十分鐘成為 TiDB Contributor 系列的第二篇文章,讓大家可以無門檻參與大型開源項目,感謝社區為 TiDB 帶來的貢獻,也希望參與 TiDB Community 能為你的生活帶來更多有意義的時刻。
為了加速表達式計算速度,最近我們對表達式的計算框架進行了重構,這篇教程為大家分享如何利用新的計算框架為 TiDB 重寫或新增 built-in 函數。對于部分背景知識請參考這篇文章,本文將首先介紹利用新的表達式計算框架重構 built-in 函數實現的流程,然后以一個函數作為示例進行詳細說明,最后介紹重構前后表達式計算框架的區別。
重構 built-in 函數整體流程在 TiDB 源碼 expression 目錄下選擇任一感興趣的函數,假設函數名為 XX
重寫 XXFunctionClass.getFunction() 方法
該方法參照 MySQL 規則,根據 built-in 函數的參數類型推導函數的返回值類型
根據參數的個數、類型、以及函數的返回值類型生成不同的函數簽名,關于函數簽名的詳細介紹見文末附錄
實現該 built-in 函數對應的所有函數簽名的 evalYY() 方法,此處 YY 表示該函數簽名的返回值類型
添加測試:
在 expression 目錄下,完善已有的 TestXX() 方法中關于該函數實現的測試
在 executor 目錄下,添加 SQL 層面的測試
運行 make dev,確保所有的 test cast 都能跑過
示例這里以重寫 LENGTH() 函數的 PR 為例,進行詳細說明
首先看 expression/builtin_string.go:(1)實現 lengthFunctionClass.getFunction() 方法
該方法主要完成兩方面工作:
參照 MySQL 規則推導 LEGNTH 的返回值類型
根據 LENGTH 函數的參數個數、類型及返回值類型生成函數簽名。由于 LENGTH 的參數個數、類型及返回值類型只存在確定的一種情況,因此此處沒有定義新的函數簽名類型,而是修改已有的 builtinLengthSig,使其組合了 baseIntBuiltinFunc(表示該函數簽名返回值類型為 int)
type builtinLengthSig struct { baseIntBuiltinFunc } func (c *lengthFunctionClass) getFunction(args []Expression, ctx context.Context) (builtinFunc, error) { // 參照 MySQL 規則,對 LENGTH 函數返回值類型進行推導 tp := types.NewFieldType(mysql.TypeLonglong) tp.Flen = 10 types.SetBinChsClnFlag(tp) // 根據參數個數、類型及返回值類型生成對應的函數簽名,注意此處與重構前不同,使用的是 newBaseBuiltinFuncWithTp 方法,而非 newBaseBuiltinFunc 方法 // newBaseBuiltinFuncWithTp 的函數聲明中,args 表示函數的參數,tp 表示函數的返回值類型,argsTp 表示該函數簽名中所有參數對應的正確類型 // 因為 LENGTH 的參數個數為1,參數類型為 string,返回值類型為 int,因此此處傳入 tp 表示函數的返回值類型,傳入 tpString 用來標識參數的正確類型。對于多個參數的函數,調用 newBaseBuiltinFuncWithTp 時,需要傳入所有參數的正確類型 bf, err := newBaseBuiltinFuncWithTp(args, tp, ctx, tpString) if err != nil { return nil, errors.Trace(err) } sig := &builtinLengthSig{baseIntBuiltinFunc{bf}} return sig.setSelf(sig), errors.Trace(c.verifyArgs(args)) }
(2) 實現 builtinLengthSig.evalInt() 方法
func (b *builtinLengthSig) evalInt(row []types.Datum) (int64, bool, error) { // 對于函數簽名 builtinLengthSig,其參數類型已確定為 string 類型,因此直接調用 b.args[0].EvalString() 方法計算參數 val, isNull, err := b.args[0].EvalString(row, b.ctx.GetSessionVars().StmtCtx) if isNull || err != nil { return 0, isNull, errors.Trace(err) } return int64(len([]byte(val))), false, nil }然后看 expression/builtin_string_test.go,對已有的 TestLength() 方法進行完善:
func (s *testEvaluatorSuite) TestLength(c *C) { defer testleak.AfterTest(c)() // 監測 goroutine 泄漏的工具,可以直接照搬 // cases 的測試用例對 length 方法實現進行測試 // 此處注意,除了正常 case 之外,最好能添加一些異常的 case,如輸入值為 nil,或者是多種類型的參數 cases := []struct { args interface{} expected int64 isNil bool getErr bool }{ {"abc", 3, false, false}, {"你好", 6, false, false}, {1, 1, false, false}, ... } for _, t := range cases { f, err := newFunctionForTest(s.ctx, ast.Length, primitiveValsToConstants([]interface{}{t.args})...) c.Assert(err, IsNil) // 以下對 LENGTH 函數的返回值類型進行測試 tp := f.GetType() c.Assert(tp.Tp, Equals, mysql.TypeLonglong) c.Assert(tp.Charset, Equals, charset.CharsetBin) c.Assert(tp.Collate, Equals, charset.CollationBin) c.Assert(tp.Flag, Equals, uint(mysql.BinaryFlag)) c.Assert(tp.Flen, Equals, 10) // 以下對 LENGTH 函數的計算結果進行測試 d, err := f.Eval(nil) if t.getErr { c.Assert(err, NotNil) } else { c.Assert(err, IsNil) if t.isNil { c.Assert(d.Kind(), Equals, types.KindNull) } else { c.Assert(d.GetInt64(), Equals, t.expected) } } } // 以下測試函數是否是具有確定性 f, err := funcs[ast.Length].getFunction([]Expression{Zero}, s.ctx) c.Assert(err, IsNil) c.Assert(f.isDeterministic(), IsTrue) }最后看 executor/executor_test.go,對 LENGTH 的實現進行 SQL 層面的測試:
// 關于 string built-in 函數的測試可以在這個方法中添加 func (s *testSuite) TestStringBuiltin(c *C) { defer func() { s.cleanEnv(c) testleak.AfterTest(c)() }() tk := testkit.NewTestKit(c, s.store) tk.MustExec("use test") // for length // 此處的測試最好也能覆蓋多種不同的情況 tk.MustExec("drop table if exists t") tk.MustExec("create table t(a int, b double, c datetime, d time, e char(20), f bit(10))") tk.MustExec(`insert into t values(1, 1.1, "2017-01-01 12:01:01", "12:01:01", "abcdef", 0b10101)`) result := tk.MustQuery("select length(a), length(b), length(c), length(d), length(e), length(f), length(null) from t") result.Check(testkit.Rows("1 3 19 8 6 2重構前的表達式計算框架")) }
TiDB 通過 Expression 接口(在 expression/expression.go 文件中定義)對表達式進行抽象,并定義 eval 方法對表達式進行計算:
type Expression interface{ ... eval(row []types.Datum) (types.Datum, error) ... }
實現 Expression 接口的表達式包括:
Scalar Function:標量函數表達式
Column:列表達式
Constant:常量表達式
下面以一個例子說明重構前的表達式計算框架。
例如:
create table t ( c1 int, c2 varchar(20), c3 double ) select * from t where c1 + CONCAT( c2, c3 < “1.1” )
對于上述 select 語句 where 條件中的表達式:
在編譯階段,TiDB 將構建出如下圖所示的表達式樹:
在執行階段,調用根節點的 eval 方法,通過后續遍歷表達式樹對表達式進行計算。
對于表達式 ‘<’,計算時需要考慮兩個參數的類型,并根據一定的規則,將兩個參數的值轉化為所需的數據類型后進行計算。上圖表達式樹中的 ‘<’,其參數類型分別為 double 和 varchar,根據 MySQL 的計算規則,此時需要使用浮點類型的計算規則對兩個參數進行比較,因此需要將參數 “1.1” 轉化為 double 類型,而后再進行計算。
同樣的,對于上圖表達式樹中的表達式 CONCAT,計算前需要將其參數分別轉化為 string 類型;對于表達式 ‘+’,計算前需要將其參數分別轉化為 double 類型。
因此,在重構前的表達式計算框架中,對于參與運算的每一組數據,計算時都需要大量的判斷分支重復地對參數的數據類型進行判斷,若參數類型不符合表達式的運算規則,則需要將其轉換為對應的數據類型。
此外,由 Expression.eval() 方法定義可知,在運算過程中,需要通過 Datum 結構不斷地對中間結果進行包裝和解包,由此也會帶來一定的時間和空間開銷。
為了解決這兩點問題,我們對表達式計算框架進行重構。
重構后的表達式計算框架重構后的表達式計算框架,一方面,在編譯階段利用已有的表達式類型信息,生成參數類型“符合運算規則”的表達式,從而保證在運算階段中無需再對類型增加分支判斷;另一方面,運算過程中只涉及原始類型數據,從而避免 Datum 帶來的時間和空間開銷。
繼續以上文提到的查詢為例,在編譯階段,生成的表達式樹如下圖所示,對于不符合函數參數類型的表達式,為其加上一層 cast 函數進行類型轉換;
這樣,在執行階段,對于每一個 ScalarFunction,可以保證其所有的參數類型一定是符合該表達式運算規則的數據類型,無需在執行過程中再對參數類型進行檢查和轉換。
附錄對于一個 built-in 函數,由于其參數個數、類型以及返回值類型的不同,可能會生成多個函數簽名分別用來處理不同的情況。對于大多數 built-in 函數,其每個參數類型及返回值類型均確定,此時只需要生成一個函數簽名。
對于較為復雜的返回值類型推導規則,可以參考 CONCAT 函數的實現和測試??梢岳?MySQLWorkbench 工具運行查詢語句 select funcName(arg0, arg1, ...) 觀察 MySQL 的 built-in 函數在傳入不同參數時的返回值數據類型。
在 TiDB 表達式的運算過程中,只涉及 6 種運算類型(目前正在實現對 JSON 類型的支持),分別是
int (int64)
real (float64)
decimal
string
Time
Duration
通過 WrapWithCastAsXX() 方法可以將一個表達式轉換為對應的類型。
對于一個函數簽名,其返回值類型已經確定,所以定義時需要組合與該類型對應的 baseXXBuiltinFunc,并實現 evalXX() 方法。(XX 不超過上述 6 種類型的范圍)
---------------------------- 我是 AI 的分割線 ----------------------------------------
回顧三月啟動的《十分鐘成為 TiDB Contributor 系列 | 添加內建函數》活動,在短短的時間內,我們收到了來自社區貢獻的超過 200 條新建內建函數,這之中有很多是來自大型互聯網公司的資深數據庫工程師,也不乏在學校或是剛畢業在刻苦鉆研分布式系統和分布式數據庫的學生。
TiDB Contributor Club 將大家聚集起來,我們互相分享、討論,一起成長。
感謝你的參與和貢獻,在開源的道路上我們將義無反顧地走下去,和你一起。
成為 New Contributor 贈送限量版馬克杯的活動還在繼續中,任何一個新加入集體的小伙伴都將收到我們充滿了誠意的禮物,很榮幸能夠認識你,也很高興能和你一起堅定地走得更遠。
成為 New Contributor 獲贈限量版馬克杯,馬克杯獲取流程如下:提交 PR
PR提交之后,請耐心等待維護者進行 Review。
目前一般在一到兩個工作日內都會進行 Review,如果當前的 PR 堆積數量較多可能回復會比較慢。代碼提交后 CI 會執行我們內部的測試,你需要保證所有的單元測試是可以通過的。期間可能有其它的提交會與當前 PR 沖突,這時需要修復沖突。維護者在 Review 過程中可能會提出一些修改意見。修改完成之后如果 reviewer 認為沒問題了,你會收到 LGTM(looks good to me) 的回復。當收到兩個及以上的 LGTM 后,該 PR 將會被合并。
合并 PR 后自動成為 Contributor,會收到來自 PingCAP Team 的感謝郵件,請查收郵件并填寫領取表單
表單填寫地址:http://cn.mikecrm.com/01wE8tX
后臺 AI 核查 GitHub ID 及資料信息,確認無誤后隨即便快遞寄出屬于你的限量版馬克杯
期待你分享自己參與開源項目的感想和經驗,TiDB Contributor Club 將和你一起分享開源的力量
了解更多關于 TiDB 的資料請登陸我們的官方網站:https://pingcap.com
加入 TiDB Contributor Club 請添加我們的 AI 微信:
TiDB Robot 微信二維碼
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/17613.html
閱讀 2928·2021-10-14 09:42
閱讀 3694·2021-08-11 11:19
閱讀 3542·2019-08-30 13:57
閱讀 3120·2019-08-30 13:49
閱讀 1534·2019-08-29 18:38
閱讀 898·2019-08-29 13:16
閱讀 1850·2019-08-26 13:25
閱讀 3230·2019-08-26 13:24