このトピックでは、Java ユーザー定義関数 (UDF) の開発方法と使用方法について説明します。
背景
v2.2.0 以降、StarRocks は Java で記述されたユーザー定義関数 (UDF) をサポートしています。
v3.0 以降、StarRocks はグローバル UDF をサポートしています。関連する SQL ステートメント (CREATE、SHOW、DROP) に GLOBAL キーワードを追加するだけで、グローバルに適用できます。これにより、各データベースに対して個別にステートメントを実行する手間が省けます。ビジネスシナリオに基づいてカスタム関数を開発し、StarRocks の機能を拡張できます。
StarRocks は現在、以下の種類の UDF をサポートしています。
-
スカラーユーザー定義関数 (スカラー UDF)
-
ユーザー定義集計関数 (UDAF)
-
ユーザー定義ウィンドウ関数 (UDWF)
-
ユーザー定義テーブル関数 (UDTF)
前提条件
StarRocks で Java UDF を使用する前に、次の要件を満たしていることを確認してください:
-
Java プロジェクトを作成および開発するには、Apache Maven がインストールされている必要があります。
-
サーバーに JDK 1.8 がインストールされている必要があります。
-
インスタンス構成 ページで FE 設定項目
enable_udfをTRUEに設定して UDF 機能を有効にします。その後、インスタンスを再起動して変更を適用します。
データ型マッピング
|
SQL 型 |
Java 型 |
|
BOOLEAN |
java.lang.Boolean |
|
TINYINT |
java.lang.Byte |
|
SMALLINT |
java.lang.Short |
|
INT |
java.lang.Integer |
|
BIGINT |
java.lang.Long |
|
FLOAT |
java.lang.Float |
|
DOUBLE |
java.lang.Double |
|
STRING/VARCHAR |
java.lang.String |
ユーザー定義関数 (UDF) の開発と使用
Maven プロジェクトを作成し、Java で関数を記述します。
手順 1:Maven プロジェクトの作成
次の基本的なディレクトリ構造で Maven プロジェクトを作成します。
project
|--pom.xml
|--src
| |--main
| | |--java
| | |--resources
| |--test
|--target
手順 2:依存関係の追加
pom.xml ファイルに次の依存関係を追加します。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>udf</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>
</project>
手順 3:UDF の開発
必要な UDF を Java で開発します。
スカラー UDF の開発
スカラー UDF は一度に 1 行を処理し、各行に対して単一の値を返します。クエリでスカラー UDF を使用すると、各行が結果セットに表示されます。一般的なスカラー関数には、UPPER、LOWER、ROUND、ABS などがあります。
次の例は、JSON オブジェクトからデータを抽出する方法を示しています。一部のビジネスシナリオでは、JSON データ内のフィールドの値が JSON オブジェクトではなく JSON 文字列である場合があります。ネストされた JSON 文字列を抽出するには、GET_JSON_STRING 関数のネストされた呼び出し (例:GET_JSON_STRING(GET_JSON_STRING('{"key":"{\\"k0\\":\\"v0\\"}"}', "$.key"), "$.k0")) を使用する必要があります。
SQL ステートメントを簡略化するために、UDF を開発して JSON 文字列を直接抽出できます (例:MY_UDF_JSON_GET('{"key":"{\\"k0\\":\\"v0\\"}"}', "$.key.k0"))。
package com.starrocks.udf.sample;
import com.alibaba.fastjson.JSONPath;
public class UDFJsonGet {
public final String evaluate(String jsonObj, String key) {
if (jsonObj == null || key == null) return null;
try {
// JSONPath ライブラリは、フィールド値が JSON 形式の文字列であっても、パスを完全に展開できます。
return JSONPath.read(jsonObj, key).toString();
} catch (Exception e) {
return null;
}
}
}
カスタムクラスには、次のメソッドを実装する必要があります。
メソッドのリクエストパラメーターと戻り値のデータ型は、手順 6 の CREATE FUNCTION ステートメントで宣言されたものと一致する必要があります。データ型マッピングは、「データ型マッピング」のルールに準拠する必要があります。
|
メソッド |
説明 |
|
TYPE1 evaluate(TYPE2, ...) |
|
ユーザー定義集計関数 (UDAF) の開発
UDAF は行のグループに対して動作し、単一の値を返します。一般的な集計関数には、SUM、COUNT、MAX、MIN などがあります。これらの関数は、各 GROUP BY グループ内の複数行のデータを集計し、グループごとに単一の結果を出力します。
次の例では、MY_SUM_INT 関数を使用します。BIGINT 値を返す組み込みの SUM 関数とは異なり、MY_SUM_INT は INT 引数を受け取り、INT を返します。
package com.starrocks.udf.sample;
public class SumInt {
public static class State {
int counter = 0;
public int serializeLength() { return 4; }
}
public State create() {
return new State();
}
public void destroy(State state) {
}
public final void update(State state, Integer val) {
if (val != null) {
state.counter+= val;
}
}
public void serialize(State state, java.nio.ByteBuffer buff) {
buff.putInt(state.counter);
}
public void merge(State state, java.nio.ByteBuffer buffer) {
int val = buffer.getInt();
state.counter += val;
}
public Integer finalize(State state) {
return state.counter;
}
}
カスタムクラスには、次のメソッドを実装する必要があります。
メソッドのリクエストパラメーターと戻り値のデータ型は、手順 6 の CREATE FUNCTION ステートメントで宣言されたものと一致する必要があります。データ型マッピングは、「データ型マッピング」のルールに準拠する必要があります。
|
メソッド |
説明 |
|
State create() |
ステートオブジェクトを作成します。 |
|
void destroy(State) |
ステートオブジェクトを破棄します。 |
|
void update(State, ...) |
ステートを更新します。最初の引数はステートオブジェクトで、後続の引数は関数宣言で指定されたリクエストパラメーターです。1 つ以上のリクエストパラメーターがサポートされています。 |
|
void serialize(State, ByteBuffer) |
ステートをシリアル化します。 |
|
void merge(State, ByteBuffer) |
シリアル化されたステートを現在のステートにマージします。 |
|
TYPE finalize(State) |
ステートから最終結果を計算して返します。 |
UDAF を開発する場合、java.nio.ByteBuffer バッファクラスを使用して中間結果を格納し、serializeLength メソッドを使用してシリアル化された中間結果の長さを指定する必要があります。
|
クラスとメソッド |
説明 |
|
java.nio.ByteBuffer() |
このバッファクラスは中間結果を格納します。結果はノード間で転送するためにシリアル化されるため、 |
|
serializeLength() |
シリアル化された中間結果の長さ (バイト単位) です。 |
java.nio.ByteBuffer のシリアル化には、次の要件に注意してください:
-
ステートを逆シリアル化するために
ByteBufferのremaining()メソッドに依存しないでください。 -
ByteBufferでclear()メソッドを呼び出さないでください。 -
serializeLength()によって返される値は、バッファに書き込まれたデータの実際の長さと一致する必要があります。不一致があると、シリアル化および逆シリアル化のエラーが発生します。
ユーザー定義ウィンドウ関数 (UDWF) の開発
UDWF は、特殊な種類の集計関数です。通常の集計関数とは異なり、ウィンドウ関数は行のグループ (ウィンドウ) に対して値を計算し、各行に対して個別の結果を返します。通常、ウィンドウ関数には、行をパーティションに分割する OVER 句が含まれます。その後、関数は、その行が属するウィンドウに基づいて各行の結果を計算します。
次の例では、MY_WINDOW_SUM_INT 関数を使用します。BIGINT 値を返す組み込みの SUM 関数とは異なり、MY_WINDOW_SUM_INT は INT 引数を受け取り、INT を返します。
package com.starrocks.udf.sample;
public class WindowSumInt {
public static class State {
int counter = 0;
public int serializeLength() { return 4; }
@Override
public String toString() {
return "State{" +
"counter=" + counter +
'}';
}
}
public State create() {
return new State();
}
public void destroy(State state) {
}
public void update(State state, Integer val) {
if (val != null) {
state.counter+=val;
}
}
public void serialize(State state, java.nio.ByteBuffer buff) {
buff.putInt(state.counter);
}
public void merge(State state, java.nio.ByteBuffer buffer) {
int val = buffer.getInt();
state.counter += val;
}
public Integer finalize(State state) {
return state.counter;
}
public void reset(State state) {
state.counter = 0;
}
public void windowUpdate(State state,
int peer_group_start, int peer_group_end,
int frame_start, int frame_end,
Integer[] inputs) {
for (int i = (int)frame_start; i < (int)frame_end; ++i) {
state.counter += inputs[i];
}
}
}
ウィンドウ関数は特殊な種類の集計関数であるため、カスタムクラスには、必要なすべての UDAF メソッドに加えて、windowUpdate() メソッドを実装する必要があります。
メソッドのリクエストパラメーターと戻り値のデータ型は、手順 6 の CREATE FUNCTION ステートメントで宣言されたものと一致する必要があります。データ型マッピングは、「データ型マッピング」のルールに準拠する必要があります。
追加で必要なメソッド
|
メソッド |
説明 |
|
|
ウィンドウデータを更新します。ウィンドウ関数の詳細については、ウィンドウ関数をご参照ください。各入力行について、対応するウィンドウ情報が取得され、中間結果が更新されます。
|
ユーザー定義テーブル関数 (UDTF) の開発
UDTF は、単一の行を入力として受け取り、出力行のテーブルを生成します。UDTF は、列を複数の行に分割するなどの操作によく使用されます。
現在、UDTF は単一の列を持つ複数行を返すことのみをサポートしています。
次の例では、MY_UDF_SPLIT 関数を使用します。スペースを区切り文字として使用して文字列を分割します。入力引数と戻り値はどちらも STRING 型です。
package com.starrocks.udf.sample;
public class UDFSplit{
public String[] process(String in) {
if (in == null) return null;
return in.split(" ");
}
}
カスタムクラスには、次のメソッドを実装する必要があります。
メソッドのリクエストパラメーターと戻り値のデータ型は、手順 6 の CREATE FUNCTION ステートメントで宣言されたものと一致する必要があります。データ型マッピングは、「データ型マッピング」のルールに準拠する必要があります。
|
メソッド |
説明 |
|
TYPE[] process() |
|
手順 4:Java プロジェクトのパッケージ化
次のコマンドを実行して、Java プロジェクトをパッケージ化します。
mvn package
このコマンドは、target ディレクトリに 2 つの JAR ファイルを生成します:udf-1.0-SNAPSHOT.jar と udf-1.0-SNAPSHOT-jar-with-dependencies.jar。
手順 5:プロジェクトのアップロード
udf-1.0-SNAPSHOT-jar-with-dependencies.jar ファイルを OSS バケットにアップロードし、JAR ファイルにパブリック読み取り権限を付与します。詳細については、「Simple upload」および「Bucket ACL」をご参照ください。
手順 6 では、FE が UDF の JAR ファイルを検証し、チェックサムを計算します。その後、BE が JAR ファイルをダウンロードして実行します。
手順 6:StarRocks での UDF の作成
StarRocks は UDF 用に、データベースレベルとグローバルレベルの 2 つの名前空間を提供します。
-
UDF に可視性の分離が必要ない場合は、グローバル UDF を作成できます。グローバル UDF を参照する場合、カタログやデータベースのプレフィックスなしで関数名で直接呼び出すことができ、アクセスが簡素化されます。
-
可視性の分離が必要な場合、または異なるデータベースで同じ名前の UDF を作成する必要がある場合は、データベースレベル UDF を作成できます。現在のセッションがそのデータベース内にある場合、関数名で直接関数を呼び出すことができます。セッションが異なるカタログまたはデータベースにある場合は、
catalog.database.functionなどの完全修飾名を使用する必要があります。
グローバル UDF を作成するには、システムレベルの CREATE GLOBAL FUNCTION 権限が必要です。データベースレベル UDF を作成するには、データベースに対する CREATE FUNCTION 権限が必要です。UDF を使用するには、その UDF に対する USAGE 権限が必要です。権限を付与する方法については、GRANT をご参照ください。
JAR ファイルをアップロードした後、StarRocks で対応する UDF を作成します。グローバル UDF を作成するには、SQL ステートメントに GLOBAL キーワードを追加するだけです。
構文
CREATE [GLOBAL][AGGREGATE | TABLE] FUNCTION function_name(arg_type [, ...])
RETURNS return_type
[PROPERTIES ("key" = "value" [, ...]) ]
パラメーター
|
パラメーター |
必須 |
説明 |
|
GLOBAL |
いいえ |
UDF がグローバル UDF であることを指定します。StarRocks v3.0 以降でサポートされています。 |
|
AGGREGATE |
いいえ |
作成する関数が UDAF または UDWF であることを指定します。 |
|
TABLE |
いいえ |
作成する関数が UDTF であることを指定します。 |
|
function_name |
はい |
関数の名前。 |
|
arg_type |
はい |
関数引数のデータ型。サポートされているデータ型については、「データ型マッピング」をご参照ください。 |
|
return_type |
はい |
関数の戻り値のデータ型。サポートされているデータ型については、「データ型マッピング」をご参照ください。 |
|
properties |
はい |
関数に関連するプロパティです。UDF の種類によって必要なプロパティが異なります。詳細については、次の例をご参照ください。 |
スカラー UDF の作成
次のコマンドを実行して、前の例のスカラー UDF を StarRocks に作成します。
CREATE [GLOBAL] FUNCTION MY_UDF_JSON_GET(string, string)
RETURNS string
PROPERTIES (
"symbol" = "com.starrocks.udf.sample.UDFJsonGet",
"type" = "StarrocksJar",
"file" = "http://<YourBucketName>.oss-cn-xxxx-internal.aliyuncs.com/<YourPath>/udf-1.0-SNAPSHOT-jar-with-dependencies.jar"
);
|
パラメーター |
説明 |
|
symbol |
UDF の完全修飾クラス名です。形式は |
|
type |
UDF のタイプです。Java UDF の場合、これを |
|
file |
UDF の JAR ファイルへの HTTP パスです。これは OSS 内のファイルの HTTP URL であり、できれば内部エンドポイントを使用することを推奨します。形式は |
UDAF の作成
次のコマンドを実行して、前の例の UDAF を StarRocks に作成します。
CREATE [GLOBAL] AGGREGATE FUNCTION MY_SUM_INT(INT)
RETURNS INT
PROPERTIES
(
"symbol" = "com.starrocks.udf.sample.SumInt",
"type" = "StarrocksJar",
"file" = "http://<YourBucketName>.oss-cn-xxxx-internal.aliyuncs.com/<YourPath>/udf-1.0-SNAPSHOT-jar-with-dependencies.jar"
);
PROPERTIES 句のパラメーターは、「スカラー UDF の作成」で説明されているものと同じです。
UDWF の作成
次のコマンドを実行して、前の例の UDWF を StarRocks に作成します。
CREATE [GLOBAL] AGGREGATE FUNCTION MY_WINDOW_SUM_INT(Int)
RETURNS Int
PROPERTIES
(
"analytic" = "true",
"symbol" = "com.starrocks.udf.sample.WindowSumInt",
"type" = "StarrocksJar",
"file" = "http://<YourBucketName>.oss-cn-xxxx-internal.aliyuncs.com/<YourPath>/udf-1.0-SNAPSHOT-jar-with-dependencies.jar"
);
analytic プロパティは、関数がウィンドウ関数であることを示します。UDWF の場合、これを true に設定します。他のパラメーターは、「スカラー UDF の作成」で説明されているものと同じです。
UDTF の作成
次のコマンドを実行して、前の例の UDTF を StarRocks に作成します。
CREATE [GLOBAL] TABLE FUNCTION MY_UDF_SPLIT(string)
RETURNS string
PROPERTIES
(
"symbol" = "com.starrocks.udf.sample.UDFSplit",
"type" = "StarrocksJar",
"file" = "http://<YourBucketName>.oss-cn-xxxx-internal.aliyuncs.com/<YourPath>/udf-1.0-SNAPSHOT-jar-with-dependencies.jar"
);
PROPERTIES 句のパラメーターは、「スカラー UDF の作成」で説明されているものと同じです。
手順 7:UDF の使用
UDF を作成した後、テストして使用できます。
スカラー UDF の使用
次のコマンドを実行して、手順 6 で作成したスカラー UDF を使用します。
SELECT MY_UDF_JSON_GET('{"key":"{\\"in\\":2}"}', '$.key.in');
UDAF の使用
次のコマンドを実行して、手順 6 で作成した UDAF を使用します。
SELECT MY_SUM_INT(col1);
UDWF の使用
次のコマンドを実行して、手順 6 で作成した UDWF を使用します。
SELECT MY_WINDOW_SUM_INT(intcol)
OVER (PARTITION BY intcol2
ORDER BY intcol3
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
FROM test_basic;
UDTF の使用
次のコマンドを実行して、前の例の UDTF を使用します。
-- 列 a、b、c1 を持つテーブル t1 が存在すると仮定します。
SELECT t1.a,t1.b,t1.c1 FROM t1;
> output:
1,2.1,"hello world"
2,2.2,"hello UDTF."
-- MY_UDF_SPLIT() 関数を使用します。
SELECT t1.a,t1.b, MY_UDF_SPLIT FROM t1, MY_UDF_SPLIT(t1.c1);
> output:
1,2.1,"hello"
1,2.1,"world"
2,2.2,"hello"
2,2.2,"UDTF."
-
SELECT リストの最初の
MY_UDF_SPLITは、MY_UDF_SPLIT関数の出力のデフォルトの列エイリアスです。 -
現在、
AS t2(f1)のように、UDTF の出力にテーブルエイリアスや列エイリアスを指定することはできません。
UDF の一覧表示
UDF の一覧を表示するには、次のコマンドを実行します。
SHOW [GLOBAL] FUNCTIONS;
UDF の削除
指定した UDF を削除するには、次のコマンドを実行します。
DROP [GLOBAL] FUNCTION <function_name>(arg_type [, ...]);
よくある質問
Q:UDF の開発時に静的変数を使用できますか。異なる UDF の静的変数は互いに干渉しますか。
A:はい、静的変数を使用できます。静的変数は、UDF が同じクラス名を共有している場合でも、異なる UDF 間で分離されており、互いに干渉しません。