【十分钟】学会 Byzer 语言全部语法

Byzer-lang 拥有声明式语言的特点,这和 SQL 非常类似。不同的是,Byzer-lang 也支持 Python 脚本,用户也可以使用 Scala/Java 动态开发和注册 UDF 函数,这使得其灵活度得到了很大提高。

Byzer-lang 针对大数据领域的流程抽象出了如下几个句法结构:

  1. 数据加载/Load
  2. 数据转换/Select
  3. 数据保存/Save
  4. 代码引入/Include
  5. 宏函数/!Macro
  6. 变量设置/Set
  7. 分支语句/!If|!Else

而针对机器学习领域,也做了类似的抽象:

  1. 模型训练/Train|Run 
  2. 模型注册/Register
  3. 模型预测/Predict

此外,在代码复用上,Byzer-lang 支持脚本和包的管理。

数据加载 / Load

  • Load json 数据源
-- 通过 set 语法 定义一个 json 数据
set abc='''
{ "x": 100, "y": 200, "z": 200 ,"dataType":"A group"}
{ "x": 120, "y": 100, "z": 260 ,"dataType":"B group"}
''';
-- load json string
load jsonStr.`abc` as table1;
  • Load csv 数据源
-- 定义数据类型为 csv, 传入路径,保留 header,开启数据类型推断
load csv.`/tmp/upload/green_tripdata_2022-01.csv` where header='true' and inferSchema='true' as trip_data;
  • Load JDBC Connection
connect jdbc where
 url="jdbc:mysql://127.0.0.1:3306/wow?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&tinyInt1isBit=false"
 and driver="com.mysql.jdbc.Driver"
 and user="xxxx"
 and password="xxxx"
 as mysql_instance;

load jdbc.`mysql_instance.test1` where directQuery='''
select * from test1 limit 10
''' as newtable;

select * from newtable as output;

数据查询转换 / Select

  • 使用 select 语法创建和查询 table
-- 创建
select 1 as col1 as table1;

-- 查询
select * from table1 as output1;
  • 使用 select 语法的模板功能处理数据

方式一:

select "" as features, 1 as label as mockData;
select #set($colums=["features","label"])
#foreach( $column in $colums )
    SUM( case when `$column` is null or `$column`='' then 1 else 0 end ) as $column,
#end
 1 as a from mockData as output1;

方式二:

select "" as features, 1 as label as mockData;

set sum_tpl = '''
SUM( case when `{0}` is null or `{0}`='' then 1 else 0 end ) as {0}
''';

select ${template.get("sum_tpl","label")}, ${template.get("sum_tpl","label")} from mockData as output1;

保存数据 / Save

基本用法,以保存 json 为例:

  • overwrite :全量
set rawData='''
    {"jack":1,"jack2":2}
    {"jack":2,"jack2":3} ''';

load jsonStr.`rawData` as table1;

save overwrite table1 as json.`/tmp/jack`;
  • append :增量(注意:mock 数据必须要换行才能解析为两条数据)
set rawData1='''
    {"jack":1,"jack2":2}
    {"jack":2,"jack2":3}
''';

load jsonStr.`rawData1` as table1;

save overwrite table1 as json.`/tmp/jack`;

set rawData2='''
    {"jack":3,"jack2":4}
    {"jack":4,"jack2":5}
''';

load jsonStr.`rawData2` as table2; 

save append table2 as json.`/tmp/jack`;
load json.`/tmp/jack` as output;
  • ignore :文件存在则跳过不写
set rawData1='''
    {"jack":1,"jack2":2}
    {"jack":2,"jack2":3}
''';

load jsonStr.`rawData1` as table1;
save overwrite table1 as json.`/tmp/jack`;

set rawData2='''
    {"jack":3,"jack2":4}
    {"jack":4,"jack2":5}
''';

load jsonStr.`rawData2` as table2; 

save ignore table2 as json.`/tmp/jack`;
load json.`/tmp/jack` as output;
  • errorIfExists :文件存在则报错
set rawData1='''
    {"jack":1,"jack2":2}
    {"jack":2,"jack2":3}
''';

load jsonStr.`rawData1` as table1;
save overwrite table1 as json.`/tmp/jack`;

set rawData2='''
    {"jack":3,"jack2":4}
    {"jack":4,"jack2":5}
''';

load jsonStr.`rawData2` as table2; 

save errorIfExists table2 as json.`/tmp/jack`;
load json.`/tmp/jack` as output;

使用 save 语法保存 jdbc 数据

select 1 as a as tmp_article_table;

connect jdbc where
url="jdbc:mysql://127.0.0.1:3306/wow?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&tinyInt1isBit=false"
and driver="com.mysql.jdbc.Driver"
and user="xxxxx"
and password="xxxxx"
as db_1;

save append tmp_article_table as jdbc.`db_1.crawler_table`;

机器学习 / Train | Run | Predict

1. train 基本语法

train 顾名思义,就是进行训练,主要是对算法进行训练时使用。下面是一个比较典型的示例:

load json.`/tmp/train` as trainData;

train trainData as RandomForest.`/tmp/rf` where
keepVersion="true"
and fitParam.0.featuresCol="content"
and fitParam.0.labelCol="label"
and fitParam.0.maxDepth="4"
and fitParam.0.checkpointInterval="100"
and fitParam.0.numTrees="4"
;
  • 第一行代码,含义是加载位于 /tmp/train 目录下的,数据格式为 JSON 的数据,并且给着这样表取名为 trainData;
  • 第二行代码,则表示提供 trainData 为数据集,使用算法 RandomForest,将模型保存在 /tmp/rf 下;
  • 训练的参数 fitParam 指定的是参数组。其中 fitParam.0 表示第一组参数,用户可以递增设置 N 组,Byzer-lang 会自动运行多组,最后返回模型结果列表。

2. run 基本语法

run 的语义是对数据进行处理

load json.`/tmp/test` as testData;

run testData as TableRepartition.`` where partitionNum="5" as newdata;

在这个例子中,TableRepartition 是 Byzer 语言内置的算子,可以用于将 table 分区,可配置的参数如下:

参数名 参数含义
partitionNum 重新分区的分区数
partitionType 重新分区的分区类型,现支持:hash、range
partitionCols 重新分区使用的列,当 partitionType 为 range 类型时,partitionCols 必须指定;shuffle为 false 时不允许指定
shuffle 重新分区时是否开启 shuffle。true 为开启,false为关闭

3. predict 基本语法

predict 顾名思义,用于机器学习预测相关。比如上面的 train 示例中,用户将随机森林的模型放在了 /tmp/rf 目录下,用户可以通过 predict 语句加载该模型,并且对表 testData 进行预测。

示例代码如下:

predict testData as RandomForest.`/tmp/rf`;

注册函数,模型 / Register

  • 支持动态创建 UDF/UDAF 函数
register ScriptUDF.`` as plusFun where
lang="scala"
and udfType="udf"
and code='''
def apply(a:Double,b:Double)={
   a + b
}
''';

上面代码的含义是,使用 ET ScriptUDF 注册一个函数叫 plusFun,这个函数使用 Scala 语言,函数的类型是 UDF,对应的实现代码在 code 参数里。

在 Byzer-lang 中, 执行完上面代码后,用户可以直接在 select 语句中使用 plusFun 函数:

-- 创建数据表
 set data='''
 {"a":1}
 {"a":2}
 {"a":3}
 {"a":4}
 ''';
 load jsonStr.`data` as dataTable;

 -- 在 SQL 中使用 plusfun
 select plusFun(a,2) as res from dataTable as output;
  • Scala UDAF 示例
set plusFun='''
import org.apache.spark.sql.expressions.{MutableAggregationBuffer, UserDefinedAggregateFunction}
import org.apache.spark.sql.types._
import org.apache.spark.sql.Row
class SumAggregation extends UserDefinedAggregateFunction with Serializable{
    def inputSchema: StructType = new StructType().add("a", LongType)
    def bufferSchema: StructType =  new StructType().add("total", LongType)
    def dataType: DataType = LongType
    def deterministic: Boolean = true
    def initialize(buffer: MutableAggregationBuffer): Unit = {
      buffer.update(0, 0l)
    }
    def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
      val sum   = buffer.getLong(0)
      val newitem = input.getLong(0)
      buffer.update(0, sum + newitem)
    }
    def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
      buffer1.update(0, buffer1.getLong(0) + buffer2.getLong(0))
    }
    def evaluate(buffer: Row): Any = {
      buffer.getLong(0)
    }
}
''';


--加载脚本
load script.`plusFun` as scriptTable;
--注册为UDF函数 名称为plusFun
register ScriptUDF.`scriptTable` as plusFun options
className="SumAggregation"
and udfType="udaf"
;

set data='''
{"a":1}
{"a":1}
{"a":1}
{"a":1}
''';
load jsonStr.`data` as dataTable;

-- 使用plusFun
select a,plusFun(a) as res from dataTable group by a as output1;
  • Java 语言 UDF 示例:
set echoFun='''
import java.util.HashMap;
import java.util.Map;
public class UDF {
  public Map apply(String s) {
    Map m = new HashMap<>();
    Integer[] arr = {1};
    m.put(s, arr);
    return m;
  }
}
''';

load script.`echoFun` as scriptTable;

register ScriptUDF.`scriptTable` as funx
options lang="java"
;

-- 创建数据表
set data='''
{"a":"a"}
''';
load jsonStr.`data` as dataTable;

select funx(a) as res from dataTable as output;
  • 注册模型
register  RandomForest.`/tmp/rf` as rf_predict;

select rf_predict(features) as predict_label from trainData
as output1;

register 语句的含义是: 将 /tmp/rf  中的 RandomForest 模型注册成一个函数,函数名叫 rf_predict.

register 后面也能接 where/options 子句:

register  RandomForest.`/tmp/rf` as rf_predict
options algIndex="0"
-- and autoSelectByMetric="f1" 
;

如果训练时同时训练了多个模型的话:

  1. algIndex 可以让用户手动指定选择哪个模型
  2. autoSelectByMetric 则可以通过一些指标,让系统自动选择一个模型。内置算法可选的指标有: f1|weightedPrecision|weightedRecall|accuracy。
  3. 如果两个参数都没有指定话的,默认会使用 f1 指标。
  • 流式程序中注册 watermark

在流式计算中,有 watermark 以及 window 的概念。我们可以使用 Register 句式来完成这个需求:

-- 为 table1 注册 watermark
register WaterMarkInPlace.`table1` as tmp1
options eventTimeCol="ts"
and delayThreshold="10 seconds";

变量设置 / Set

1. 基础应用

  • 使用 set 语法声明变量
set hello="world";

-- 然后使用 select 语法查看变量
select "value: ${hello}" as title as output1;
  • 使用 set 语法声明表名

 在 select 语句中使用变量作为表名(需要用反引号括起来) 

set hello="world";

select "hello William" as title as `${hello}`;
select * from world as output;

2. 生命周期

通常在 Byzer-lang 中,生命周期分成以下部分:

  1. request (当前执行请求有效/ Notebook 中实现为 Cell 等级)
  2. session (当前会话周期有效 /Notebook 的用户等级)
  • request
set hello="world";
  • session
set hello="world" where scope="session";

然后在其他 cell 或 notebook 中便可以执行:

select "hello William" as title 
as `${hello}`;

select * from world as output1;

3. 变量类型

Byzer-lang 的变量被分为五种类型:

  1. text
  2. conf
  3. shell
  4. sql
  5. defaultParam
  • text
set hello="world";
  • conf

conf 表示这是一个配置选项,通常用于配置系统的行为,比如:

set spark.sql.shuffle.partitions=200 where type="conf";
  • shell(不推荐使用,安全风险较大)

shell,也就是 set 后的 key 最后是由 shell 执行生成的。

set date=`date` where type="shell";
select "${date}" as dt as output;
  • sql

sql,这意味着 set 后的 key 最后是由 sql 引擎执行生成的。

set date=`select date_sub(CAST(current_timestamp() as DATE), 1) as dt` where type="sql";
select "${date}" as dt as output;
  • defaultParam
set hello="foo";
set hello="bar" where type="defaultParam";

select "${hello}" as name as output;

上述代码中后面的"bar" 会覆盖前面的 "foo"。而 Byzer-lang 引入了 defaultParam 类型的变量来达到一种效果:如果变量已经设置了,新变量声明就失效,如果变量没有被设置过,则生效。

4. 编译时和运行时变量

Byzer-lang 有非常完善的权限体系,可以轻松控制任何数据源到列级别的访问权限,而且创新性的提出了预处理时权限, 也就是通过静态分析 Byzer-lang 脚本从而完成表级别权限的校验(列级别依然需要运行时完成)。

但是预处理期间,权限最大的挑战在于 set 变量的解析,比如:

select "foo" as foo as foo_table;
set hello=`select foo from foo_table` where type="sql";
select "${hello}" as name as output; 

在没有执行第一个句子,那么第二条 set 语句在预处理期间执行就会报错,因为此时并没有叫 foo_table 的表。

为了解决这个问题,Byzer-lang 引入了 compile/runtime 两个模式。如果用户希望在 set 语句预处理阶段就可以 evaluate 值,那么添加该参数即可。

set hello=`select 1 as foo ` where type="sql" and mode="compile";

如果希望 set 变量,只在运行时才需要执行,则设置为 runtime

set hello=`select 1 as foo ` where type="sql" and mode="runtime";

此时,Byzer-lang 在预处理阶段不会进行该变量的创建。

代码引入 / Include

1. 引入第三方库

Byzer-lang 支持复杂的代码组织结构,这赋予了 Byzer-lang 强大的代码复用能力。

  1. 可以将一个 Byzer 脚本引入到另外一个 Byzer 脚本
  2. 也可以将一堆 Byzer 脚本组装成一个功能集,然后以 Lib 的方式提供给其他用户使用

例如,lib-core 是 @allwefantasy 维护的一个 Byzer-lang Lib 库,里面有很多用 Byzer-lang 写成的一些功能。Byzer-lang 使用 Github 来作为 Lib 管理工具。

如果需要引入 lib-core,可以通过如下方式:

include lib.`github.com/allwefantasy/lib-core`
where 
-- libMirror="gitee.com"
-- commit="xxxxx"
-- force="true"
alias="libCore";

在上面的代码示例中,通过 include 引入了 lib-core 库,为了方便使用它,用户可以给其取了一个别名叫 libCore

除了 alias 参数以外,还有其他三个可选参数:

  1. libMirror :可以配置库的镜像地址。比如如果 gitee 也同步了该库,那么可以通过该配置使得国内下载速度加快。
  2. commit :可以指定库的版本
  3. force :决定每次执行的时候,都强制删除上次下载的代码,然后重新下载。如果你每次都希望获得最新的代码,那么可以开启该选项。

引入该库后,就可以使用库里的包。假设用户希望在自己的项目中使用 lib-core 里一个叫 hello 的函数, 那么可以通过如下语法引入该函数:

include local.`libCore.udf.hello`;

引入后,就可以在 select 句式中使用该函数了:

select hello() as name as output;

2. 项目内脚本引用

  • lib 内脚本依赖
include local.`github.com/allwefantasy/lib-core.udf.hello`;
  • 普通项目内 Byzer 脚本依赖

如果是 Web 端的,请参考 Byzer Notebook 的使用功能手册 。如果是桌面版的,则使用 project 关键字,比如希望在 src/algs/a.byzer 中引入 src/algs/b.byzer ,那么可以在 a.byzer 中使用如下方式对 b.byzer 进行引用:

include project.`src/algs/b.byzer`;
  • Python 脚本引用

如果你是在开发一个 Lib 库,那么必须使用 local + 全路径:

!pyInclude local 'github.com/allwefantasy/lib-core.alg.xgboost.py' named rawXgboost;

如果仅仅是在自己项目中互相引用,则使用 project + 全路径或者相对路径就可以:

!pyInclude project 'src/algs/xgboost.py' named rawXgboost;

条件语句 / !If | !Else

  • if/else 条件判断基本用法
set a = "wow,jack";
!if ''' split(:a,",")[0] == "jack" ''';
   select 1 as a as b;
!else;
   select 2 as a as b;
!fi;
select * from b as output;
  • 分支语句嵌套
set a="jack,2";
!if ''' select split(:a,",")[0] as :name, split(:a,",")[1] as :num;
:name == "jack" and :num == 3
''';
    select 0 as a as b;
!elif ''' select split(:a,",")[1] as :num; :num==2 ''';
    !if ''' 2==1 ''';
       select 1.1 as a as b;
    !else;
       select 1.2 as a as b;
    !fi;
!else;
  select 2 as a as b;
!fi;

select * from b as output;

以上为 Byzer 语言基础语法的使用案例介绍,更多应用方式请参考 Byzer-lang 手册。

欢迎加入 Byzer 社区,也欢迎更多的小伙伴加入我们的Slack 社区讨论组,共同参与 Data & AI 的前沿话题讨论。

你可能感兴趣的:(Byzer-lang,干货教学,开发语言,大数据,云原生,kylin,python)