跳转至主要内容

本地JavaScript自定义函数

最近,我们还增加了对基于 JavaScript 的本地 UDF 的支持。 您可以使用现代 JavaScript(由 V8提供支持)开发用户定义的标量函数 (UDF) 或用户定义的聚合函数 (UDAF)。 无需为 UDF 部署额外的服务器/服务。 将来将支持更多语言。

注册 JS UDF

  1. 点击右上角的工作空间名称并选择 设置,打开工作空间设置页面。
  2. 单击 Register New Function按钮。
  3. 指定函数名称,例如 second_max。 确保名称不会与内置函数或其他 UDF 冲突。 描述(可选)
  4. 为输入参数和返回值选择数据类型。
  5. 选择 JavaScript 作为 UDF 类型。
  6. 指定该函数是否用于聚合。
  7. 输入 UDF 的 JavaScript 代码。 (我们将进一步解释如何编写代码。)
  8. 单击 创建 按钮注册该函数。

开发标量函数

标量函数是每次调用返回一个值的函数;在大多数情况下,您可以将其视为每行返回一个值。 这与 聚合函数不同,它返回每行组的一个值。

带有 1 个参数的标量函数

例如,您想检查用户是否在其个人资料中设置了工作电子邮件。 虽然这在普通 SQL 中是可以实现的,但如果你能创建 UDF 来提高 SQL 的可读性,那就更好了,例如

SELECT * FROM user_clicks where is_work_email(email)

您可以使用以下代码定义一个新函数 is_work_email ,其中一个输入类型 string 并返回 bool

function is_work_email(values){
return values.map(email=>email.endsWith("@gmail.com"));
}

备注:

  1. 第一行定义了一个与 UDF 名称完全相同的函数。 参数的数量应与您在 UDF 表单中指定的数量相匹配。
  2. 请注意,输入实际上是一个 JavaScript 列表。 为了提高性能,Timeplus 将通过将参数组合在一起来减少函数调用的次数。 你需要返回一个与输入长度完全相同的列表。
  3. values.map(..) 创建一个新的数组,其结果是在调用数组中的每个元素上调用一个提供的函数(doc)。
  4. email=>email.endSwith (” @gmail .com”) 是通过检查电子邮件是否以 “@gmail .com” 结尾来返回 bool 的快捷方式。 你可以添加更复杂的逻辑,也可以写入多行并以 return ..返回结果。

带有 2 个参数的标量函数

让我们通过定义一个不被视为与工作相关的电子邮件域名列表来增强前面的示例。 例如

SELECT * FROM user_clicks where email_not_in(email,'gmail.com,icloud.com,live.com')

与上一个教程类似,您创建了一个名为 email_not_in的新函数。 这次你指定两个 string类型的参数。 注意:目前 JS UDF 不支持复杂的数据类型,例如 array(string)

以下代码实现了这个新函数:

function email_not_in(emails,lists){
let list=lists[0].split(','); // convert string to array(string)
return emails.map(email=>{
for(let i=0;i<list.length;i++){
if(email.endsWith('@'+list[i]))
return false; // if the email ends with any of the domain, return false, otherwise continue
}
return true; // no match, return true confirming the email is in none of the provided domains
});
}

没有参数的标量函数

目前,我们不支持没有参数的 JS UDF。 作为一种解决方法,你可以定义一个参数,例如

SELECT *, magic_number(1) FROM user_clicks

magic_number 需要一个 int 参数。

function magic_number(values){
return values.map(v=>42)
}

在这种情况下,无论指定什么参数,该函数都将返回 42

定义一个新的聚集函数

聚合函数为每组行返回一个值。 注册 UDF 时,请务必打开该选项以表明这是聚合函数。 与标量函数相比,生命周期要复杂一些。

3 个必需的和 3 个可选的函数

比如我们希望获得一组数据中的第二个最大值。

顺序函数是否必需?描述示例
1initialize()初始化状态。function(){
this.max=-1.0;
this.sec_max=-1.0;
}
2process(args..)该函数的主要逻辑function(values){
values.map(..)
}
3finalize()返回最终的聚合结果function(){
return this.sec_max
}
4serialize()将 JS 内部状态序列化为字符串,这样 Timeplus 就可以持续进行故障转移/恢复。function(){
return JSON.stringify({'max':this.max,'sec_max':this.sec_max})
}
5deserialize(str)与serialize()相反。 读取字符串并转换回 JS 内部状态。function(str){
let s=JSON.parse(str);
this.max=s['max'];
this.sec_max=s['sec_max'];
}
6merge(str)将两个状态合并为一个。 用于多分片处理。function(str){
let s=JSON.parse(str);
if..else..}

示例:获取第二大数

此 JS UDAF 的完整源代码是

{
initialize: function() {
this.max = -1.0;
this.sec_max = -1.0;
},

process: function(values) {
for (let i = 0; i < values.length; i++) {
this._update(values[i]);
}
},

_update: function(value) {
if (value > this.max) {
this.sec_max = this.max;
this.max = value;
} else if (value > this.sec_max) {
this.sec_max = value;
}
},

finalize: function() {
return this.sec_max
},

serialize: function() {
return JSON.stringify({
'max': this.max,
'sec_max': this.sec_max
});
},

deserialize: function(state_str) {
let s = JSON.parse(state_str);
this.max = s['max'];
this.sec_max = s['sec_max']
},

merge: function(state_str) {
let s = JSON.parse(state_str);
this._update(s['max']);
this._update(s['sec_max']);
}
};

要注册此函数,请选择 JavaScript 作为 UDF 类型,确保打开 “是聚合”。 将函数名称设置为 second_max (您无需在 JS 代码中重复函数名称)。 在 float 类型中添加一个参数,并将返回类型也设置为 float

请注意,与 JS 标量函数不同,您需要将所有函数放在对象 {}下。 你可以定义内部私有函数,只要名称不会与 JavaScript 或 UDF 生命周期中的原生函数冲突。

备注

  • 将来我们将提供更好的测试工具。

  • 自定义 JavaScript 代码在装有 V8 引擎的沙箱中运行。 它不会影响其他工作空间。

  • JS UDF 不支持诸如 arraymap 之类的复合数据结构。 JavaScript 数据类型更为通用,以下是 Timeplus 中 JavaScript 数据类型和数据类型的映射:

    Timeplus 数据类型JavaScript 数据类型
    int8/16/32/64number
    uint8/16/32/64number
    float32/64数字
    fixed_string/stringstring
    date/date32/datetime/datetime64Date (毫秒为精度)