這是十分鐘成爲 TiDB Contributor 系列的第二篇文章,讓你們能夠無門檻參與大型開源項目,感謝社區爲 TiDB 帶來的貢獻,也但願參與 TiDB Community 能爲你的生活帶來更多有意義的時刻。mysql
爲了加速表達式計算速度,最近咱們對錶達式的計算框架進行了重構,這篇教程爲你們分享如何利用新的計算框架爲 TiDB 重寫或新增 built-in 函數。對於部分背景知識請參考這篇文章,本文將首先介紹利用新的表達式計算框架重構 built-in 函數實現的流程,而後以一個函數做爲示例進行詳細說明,最後介紹重構先後表達式計算框架的區別。sql
在 TiDB 源碼 expression 目錄下選擇任一感興趣的函數,假設函數名爲 XX數據庫
重寫 XXFunctionClass.getFunction() 方法express
實現該 built-in 函數對應的全部函數簽名的 evalYY() 方法,此處 YY 表示該函數簽名的返回值類型微信
添加測試:框架
運行 make dev,確保全部的 test cast 都能跑過分佈式
這裏以重寫 LENGTH() 函數的 PR 爲例,進行詳細說明函數
首先看 expression/builtin_string.go:工具
(1)實現 lengthFunctionClass.getFunction() 方法單元測試
該方法主要完成兩方面工做:
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 <nil>"))
}複製代碼
TiDB 經過 Expression 接口(在 expression/expression.go 文件中定義)對錶達式進行抽象,並定義 eval 方法對錶達式進行計算:
type Expression interface{
...
eval(row []types.Datum) (types.Datum, error)
...
}複製代碼
實現 Expression 接口的表達式包括:
下面以一個例子說明重構前的表達式計算框架。
例如:
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,能夠保證其全部的參數類型必定是符合該表達式運算規則的數據類型,無需在執行過程當中再對參數類型進行檢查和轉換。
select funcName(arg0, arg1, ...)
觀察 MySQL 的 built-in 函數在傳入不一樣參數時的返回值數據類型。Duration
經過 WrapWithCastAsXX() 方法能夠將一個表達式轉換爲對應的類型。
---------------------------- 我是 AI 的分割線 ----------------------------------------
回顧三月啓動的《十分鐘成爲 TiDB Contributor 系列 | 添加內建函數》活動,在短短的時間內,咱們收到了來自社區貢獻的超過 200 條新建內建函數,這之中有不少是來自大型互聯網公司的資深數據庫工程師,也不乏在學校或是剛畢業在刻苦鑽研分佈式系統和分佈式數據庫的學生。
TiDB Contributor Club 將你們彙集起來,咱們互相分享、討論,一塊兒成長。
感謝你的參與和貢獻,在開源的道路上咱們將義無反顧地走下去,和你一塊兒。
成爲 New Contributor 贈送限量版馬克杯的活動還在繼續中,任何一個新加入集體的小夥伴都將收到咱們充滿了誠意的禮物,很榮幸可以認識你,也很高興能和你一塊兒堅決地走得更遠。
瞭解更多關於 TiDB 的資料請登錄咱們的官方網站:pingcap.com
加入 TiDB Contributor Club 請添加咱們的 AI 微信:tidbai