三十分钟成为 Contributor | 为 TiKV 添加 built-in 函数

2024-04-08 03:18

本文主要是介绍三十分钟成为 Contributor | 为 TiKV 添加 built-in 函数,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

作者:吴雪莲

背景知识

SQL 语句发送到 TiDB 后经过 parser 生成 AST(抽象语法树),再经过 Query Optimizer 生成执行计划,执行计划切分成很多子任务,这些子任务以表达式的方式最后下推到底层的各个 TiKV 来执行。

图 1

图 1

如图 1,当 TiDB 收到来自客户端的查询请求

select count(*) from t where a + b > 5

时,执行顺序如下:

  1. TiDB 对 SQL 进行解析,组织成对应的表达式,下推给 TiKV

  2. TiKV 收到请求后,循环以下过程

    • 获取下一行完整数据,并按列解析

    • 使用参数中的 where 表达式对数据进行过滤

    • 若上一条件符合,进行聚合计算

  3. TiKV 向 TiDB 返回聚合计算结果

  4. TiDB 对所有涉及的结果进行二次聚合,返回给客户端

这里的 where 条件便是以表达式树的形式下推给 TiKV。在此之前 TiDB 只会向 TiKV 下推一小部分简单的表达式,比如取出某一个列的某个数据类型的值,简单数据类型的比较操作,算术运算等。为了充分利用分布式集群的资源,进一步提升 SQL 在整个集群的执行速度,我们需要将更多种类的表达式下推到 TiKV 来运行,其中的一大类就是 MySQL built-in 函数。

目前,由于 TiKV 的 built-in 函数尚未全部实现,对于无法下推的表达式,TiDB 只能自行解决。这无疑将成为提升 TiDB 速度的最大绊脚石。好消息是,TiKV 在实现 built-in 函数时,可以直接参考 TiDB 的对应函数逻辑(顺便可以帮 TiDB 找找 Bug),为我们减少了不少工作量。

Built-in 函数无疑是 TiDB 和 TiKV 成长道路上不可替代的一步,如此艰巨又庞大的任务,我们需要广大社区朋友们的支持与鼓励。亲爱的朋友们,想玩 Rust 吗?想给 TiKV 提 PR 吗?想帮助 TiDB 跑得更快吗?动动您的小手指,拿 PR 来砸我们吧。您的 PR 一旦被采用,将会有小惊喜哦。

手把手教你实现 built-in 函数

Step 1:准备下推函数

在 TiKV 的 https://github.com/pingcap/tikv/issues/3275 issue 中,找到未实现的函数签名列表,选一个您想要实现的函数。

Step 2:获取 TiDB 中可参考的逻辑实现

在 TiDB 的 expression 目录下查找相关 builtinXXXSig 对象,这里 XXX 为您要实现的函数签名,本例中以 MultiplyIntUnsigned 为例,可以在 TiDB 中找到其对应的函数签名(builtinArithmeticMultiplyIntUnsignedSig)及 实现。

Step 3:确定函数定义

  1. built-in 函数所在的文件名要求与 TiDB 的名称对应,如 TiDB 中,expression 目录下的下推文件统一以 builtin_XXX 命名,对应到 TiKV 这边,就是 builtin_XXX.rs。若同名对应的文件不存在,则需要自行在同级目录下新建。对于本例,当前函数存放于 TiDB 的 builtin_arithmetic.go 文件里,对应到 TiKV 便是存放在 builtin_arithmetic.rs 中。

  2. 函数名称:函数签名转为 Rust 的函数名称规范,这里 MultiplyIntUnsigned 将会被定义为 multiply_int_unsigned

  3. 函数返回值,可以参考 TiDB 中实现的 Eval 函数,对应关系如下:

    TiDB 对应实现的 Eval 函数TiKV 对应函数的返回值类型
    evalIntResult<Option<i64>>
    evalRealResult<Option<f64>>
    evalStringResult<Option<Cow<'a, [u8]>>>
    evalDecimalResult<Option<Cow<'a, Decimal>>>
    evalTimeResult<Option<Cow<'a, Time>>>
    evalDurationResult<Option<Cow<'a, Duration>>>
    evalJSONResult<Option<Cow<'a, Json>>>

    可以看到 TiDB 的 builtinArithmeticMultiplyIntUnsignedSig  对象实现了 evalInt 方法,故当前函数(multiply_int_unsigned)的返回类型应该为 Result<Option<i64>>

  4. 函数的参数, 所有 builtin-in 的参数都与 Expression 的 eval 函数一致,即:

    • 环境配置量 (ctx:&StatementContext)

    • 该行数据每列具体值 (row:&[Datum])

综上,multiply_int_unsigned 的下推函数定义为:

    pub fn multiply_int_unsigned(&self,ctx: &mut EvalContext,row: &[Datum],) -> Result<Option<i64>>

Step 4:实现函数逻辑

这一块相对简单,直接对照 TiDB 的相关逻辑实现即可。这里,我们可以看到 TiDB 的 builtinArithmeticMultiplyIntUnsignedSig 的具体实现如下:

func (s *builtinArithmeticMultiplyIntUnsignedSig) evalInt(row types.Row) (val int64, isNull bool, err error) {a, isNull, err := s.args[0].EvalInt(s.ctx, row)if isNull || err != nil {return 0, isNull, errors.Trace(err)}unsignedA := uint64(a)b, isNull, err := s.args[1].EvalInt(s.ctx, row)if isNull || err != nil {return 0, isNull, errors.Trace(err)}unsignedB := uint64(b)result := unsignedA * unsignedBif unsignedA != 0 && result/unsignedA != unsignedB {return 0, true, types.ErrOverflow.GenByArgs("BIGINT UNSIGNED", fmt.Sprintf("(%s * %s)", s.args[0].String(), s.args[1].String()))}return int64(result), false, nil
}

参考以上代码,翻译到 TiKV 即可,如下:

 pub fn multiply_int_unsigned(&self,ctx: &mut EvalContext,row: &[Datum],) -> Result<Option<i64>> {let lhs = try_opt!(self.children[0].eval_int(ctx, row));let rhs = try_opt!(self.children[1].eval_int(ctx, row));let res = (lhs as u64).checked_mul(rhs as u64).map(|t| t as i64);// TODO: output expression in error when column's name pushed down.res.ok_or_else(|| Error::overflow("BIGINT UNSIGNED", &format!("({} * {})", lhs, rhs))).map(Some)}

Step 5:添加参数检查

TiKV 在收到下推请求时,首先会对所有的表达式进行检查,表达式的参数个数检查就在这一步进行。

TiDB 中对每个 built-in 函数的参数个数有严格的限制,这一部分检查可参考 TiDB 同目录下 builtin.go 相关代码。

在 TiKV 同级目录的 scalar_function.rs 文件里,找到 ScalarFunc 的 check_args 函数,按照现有的模式,加入参数个数的检查即可。

Step 6:添加下推支持

TiKV 在对一行数据执行具体的 expression 时,会调用 eval 函数,eval 函数又会根据具体的返回类型,执行具体的子函数。这一部分工作在 scalar_function.rs 中以宏(dispatch_call)的形式完成。

对于 MultiplyIntUnsigned, 我们最终返回的数据类型为 Int,所以可以在 dispatch_call 中找到 INT_CALLS,然后照着加入 MultiplyIntUnsigned => multiply_int_unsigned , 表示当解析到函数签名 MultiplyIntUnsigned 时,调用上述已实现的函数 multiply_int_unsigned

至此 MultiplyIntUnsigned 下推逻辑已完全实现。

Step 7:添加测试

在函数 multiply_int_unsigned 所在文件 builtin_arithmetic.rs 底部的 test 模块中加入对该函数签名的单元测试,要求覆盖到上述添加的所有代码,这一部分也可以参考 TiDB 中相关的测试代码。本例在 TiKV 中实现的测试代码如下:

    #[test]fn test_multiply_int_unsigned() {let cases = vec![(Datum::I64(1), Datum::I64(2), Datum::U64(2)),(Datum::I64(i64::MIN),Datum::I64(1),Datum::U64(i64::MIN as u64),),(Datum::I64(i64::MAX),Datum::I64(1),Datum::U64(i64::MAX as u64),),(Datum::U64(u64::MAX), Datum::I64(1), Datum::U64(u64::MAX)),];let mut ctx = EvalContext::default();for (left, right, exp) in cases {let lhs = datum_expr(left);let rhs = datum_expr(right);let mut op = Expression::build(&mut ctx,scalar_func_expr(ScalarFuncSig::MultiplyIntUnsigned, &[lhs, rhs]),).unwrap();op.mut_tp().set_flag(types::UNSIGNED_FLAG as u32);let got = op.eval(&mut ctx, &[]).unwrap();assert_eq!(got, exp);}// test overflowlet cases = vec![(Datum::I64(-1), Datum::I64(2)),(Datum::I64(i64::MAX), Datum::I64(i64::MAX)),(Datum::I64(i64::MIN), Datum::I64(i64::MIN)),];for (left, right) in cases {let lhs = datum_expr(left);let rhs = datum_expr(right);let mut op = Expression::build(&mut ctx,scalar_func_expr(ScalarFuncSig::MultiplyIntUnsigned, &[lhs, rhs]),).unwrap();op.mut_tp().set_flag(types::UNSIGNED_FLAG as u32);let got = op.eval(&mut ctx, &[]).unwrap_err();assert!(check_overflow(got).is_ok());}}

Step 8:运行测试

运行 make expression,确保所有的 test case 都能跑过。

完成以上几个步骤之后,就可以给 TiKV 项目提 PR 啦。想要了解提 PR 的基础知识,尝试移步 此文,看看是否有帮助。

这篇关于三十分钟成为 Contributor | 为 TiKV 添加 built-in 函数的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/884427

相关文章

Python的time模块一些常用功能(各种与时间相关的函数)

《Python的time模块一些常用功能(各种与时间相关的函数)》Python的time模块提供了各种与时间相关的函数,包括获取当前时间、处理时间间隔、执行时间测量等,:本文主要介绍Python的... 目录1. 获取当前时间2. 时间格式化3. 延时执行4. 时间戳运算5. 计算代码执行时间6. 转换为指

Python正则表达式语法及re模块中的常用函数详解

《Python正则表达式语法及re模块中的常用函数详解》这篇文章主要给大家介绍了关于Python正则表达式语法及re模块中常用函数的相关资料,正则表达式是一种强大的字符串处理工具,可以用于匹配、切分、... 目录概念、作用和步骤语法re模块中的常用函数总结 概念、作用和步骤概念: 本身也是一个字符串,其中

shell编程之函数与数组的使用详解

《shell编程之函数与数组的使用详解》:本文主要介绍shell编程之函数与数组的使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录shell函数函数的用法俩个数求和系统资源监控并报警函数函数变量的作用范围函数的参数递归函数shell数组获取数组的长度读取某下的

MySQL高级查询之JOIN、子查询、窗口函数实际案例

《MySQL高级查询之JOIN、子查询、窗口函数实际案例》:本文主要介绍MySQL高级查询之JOIN、子查询、窗口函数实际案例的相关资料,JOIN用于多表关联查询,子查询用于数据筛选和过滤,窗口函... 目录前言1. JOIN(连接查询)1.1 内连接(INNER JOIN)1.2 左连接(LEFT JOI

MySQL中FIND_IN_SET函数与INSTR函数用法解析

《MySQL中FIND_IN_SET函数与INSTR函数用法解析》:本文主要介绍MySQL中FIND_IN_SET函数与INSTR函数用法解析,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友一... 目录一、功能定义与语法1、FIND_IN_SET函数2、INSTR函数二、本质区别对比三、实际场景案例分

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序

C语言函数递归实际应用举例详解

《C语言函数递归实际应用举例详解》程序调用自身的编程技巧称为递归,递归做为一种算法在程序设计语言中广泛应用,:本文主要介绍C语言函数递归实际应用举例的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录前言一、递归的概念与思想二、递归的限制条件 三、递归的实际应用举例(一)求 n 的阶乘(二)顺序打印

C/C++错误信息处理的常见方法及函数

《C/C++错误信息处理的常见方法及函数》C/C++是两种广泛使用的编程语言,特别是在系统编程、嵌入式开发以及高性能计算领域,:本文主要介绍C/C++错误信息处理的常见方法及函数,文中通过代码介绍... 目录前言1. errno 和 perror()示例:2. strerror()示例:3. perror(

Kotlin 作用域函数apply、let、run、with、also使用指南

《Kotlin作用域函数apply、let、run、with、also使用指南》在Kotlin开发中,作用域函数(ScopeFunctions)是一组能让代码更简洁、更函数式的高阶函数,本文将... 目录一、引言:为什么需要作用域函数?二、作用域函China编程数详解1. apply:对象配置的 “流式构建器”最

Android Kotlin 高阶函数详解及其在协程中的应用小结

《AndroidKotlin高阶函数详解及其在协程中的应用小结》高阶函数是Kotlin中的一个重要特性,它能够将函数作为一等公民(First-ClassCitizen),使得代码更加简洁、灵活和可... 目录1. 引言2. 什么是高阶函数?3. 高阶函数的基础用法3.1 传递函数作为参数3.2 Lambda