MaxCompute は、次世代 SQL エンジンに基づき、ユーザー定義型(UDT)を導入しました。UDT を使用すると、SQL ステートメント内でサードパーティのプログラミング言語のクラスやオブジェクトを参照し、メソッドを呼び出したりデータを取得したりできます。
UDT と UDF の使い分け
UDT およびユーザー定義関数(UDF)はどちらも、カスタムロジックによって MaxCompute SQL を拡張します。ワークフローに応じて選択してください。
| 状況 | 推奨アプローチ |
|---|---|
組み込み Java クラスのメソッドを直接呼び出す(例:Integer.MAX_VALUE) |
UDT — 関数定義不要 |
| SQL 式内でサードパーティライブラリを直接再利用する | UDT — ラッパーなしでクラスをインラインで参照 |
| 複数ステージのジョブ間でコンパイル済みのソース言語オブジェクトを含める | UDT — クロスステージの JVM 状態を自動的にカプセル化 |
| 複数プロジェクト間で再利用可能なビジネスロジックを実装する | UDF — 明示的な関数登録により共有可能 |
ユースケース
-
関数を定義せずに Java 標準ライブラリのメソッドを呼び出す。 MaxCompute SQL がネイティブで公開していない組み込み Java クラスのメソッドが必要なタスクでは、UDT を使用して式内から直接呼び出せます。
-
サードパーティライブラリをインラインで参照する。 サードパーティライブラリの関数を UDF 内でラップする代わりに、SQL ステートメント内でクラスを直接参照できます。
-
SQL にコンパイル済みのソースコードを埋め込む。 コンパイルを必要とする Java のような言語の場合、UDT を使用すれば、別途登録ステップを経ることなく SQL 式内でオブジェクトやクラスを参照できます。スクリプトベースの代替手段については、「SELECT TRANSFORM」をご参照ください。
前提条件
UDT を使用する前に、以下の点を確認してください。
-
ご利用の環境で JDK 1.8 が利用可能です。JDK 1.8 以降のバージョンはサポートされていない場合があります。
-
INT などのデータ型を使用する場合は、新しいデータ型が有効になっていること:
set odps.sql.type.system.odps2=true;
仕組み
他の SQL エンジンにおける UDT(通常は STRUCT 型に類似した型エイリアスを定義するもの)とは異なり、MaxCompute の UDT は CREATE TYPE 文のように動作します。フィールドとメソッドの両方を含み、DDL を記述することなく SQL 内で直接参照できます。
以下の例はその違いを示しています。Java の java.lang パッケージから Integer.MAX_VALUE にアクセスする場合:
UDT を使用した場合(直接参照):
-- 新しいデータ型を有効化(INTEGER などの型に必須)。
set odps.sql.type.system.odps2=true;
SELECT java.lang.Integer.MAX_VALUE;
java.lang は Java と同様に自動インポートされるため、これは以下と同等です。
set odps.sql.type.system.odps2=true;
SELECT Integer.MAX_VALUE;
結果:
+-----------+
| max_value |
+-----------+
| 2147483647 |
+-----------+
UDF を使用した場合(比較用):
-
UDF クラスを記述します。
package com.aliyun.odps.test; public class IntegerMaxValue extends com.aliyun.odps.udf.UDF { public Integer evaluate() { return Integer.MAX_VALUE; } } -
コンパイル、アップロード、登録を行います。
add jar odps-test.jar; create function integer_max_value as 'com.aliyun.odps.test.IntegerMaxValue' using 'odps-test.jar'; -
呼び出します。
select integer_max_value();
UDT を使用すれば、この一連の手順が単一の SQL ステートメントに簡略化されます。
マルチステージ実行
UDT オブジェクトは MapReduce ステージ間を自然に流れます。以下の例では、異なるデータソースから計算された 2 つの BigInteger 列を結合しています。
-- サンプルデータ。
@table1 := select * from values ('100000000000000000000') as t(x);
@table2 := select * from values (100L) as t(y);
-- new メソッドでオブジェクトを作成。
@a := select new java.math.BigInteger(x) x from @table1;
-- 静的メソッドを呼び出し。
@b := select java.math.BigInteger.valueOf(y) y from @table2;
-- 結合時にインスタンスメソッドを呼び出し。
select /*+mapjoin(b)*/ x.add(y).toString() from @a a join @b b;
-- 出力:
100000000000000000100
このジョブは 3 つのステージ(M1、R2、J3)にわたって実行されます。new java.math.BigInteger(x) は M1 で実行され、java.math.BigInteger.valueOf(y) および x.add(y).toString() は J3 で異なるプロセスおよび物理マシン上で実行されます。UDT はこれをカプセル化し、すべてのステージが同一の Java 仮想マシン(JVM)上で実行されているかのように動作させます。
この UDT の例となる SQL ジョブの DAG には 3 つのステージが含まれます:M1 → R2_1 → J3_2。すべてのステージが 100 % 完了し、各ステージ間で 1 行のデータが渡されます。
変数 a の x 列は、ビルトイン型ではなく java.math.BigInteger 型です。この UDT 値は他の演算子に渡され、データのシャッフリングにも使用できます。
JAR パッケージの参照と Java インポートの設定
Java クラス用のすべての SDK は、デフォルトで UDT から利用可能です。追加の JAR パッケージを参照する場合やデフォルトのインポートパスを設定する場合は、以下のセッションフラグを使用します。
JAR パッケージを参照する:
set odps.sql.type.system.odps2=true;
set odps.sql.session.resources=odps-test.jar;
-- JAR は事前にプロジェクトにアップロードしておく必要があります。
select new com.aliyun.odps.test.IntegerMaxValue().evaluate();
カンマ区切りで複数のリソースを指定できます:set odps.sql.session.resources=foo.sh,bar.txt;
odps.sql.session.resources は UDT および SELECT TRANSFORM の両方を制御します。ここで設定された JAR は、両方の機能で利用可能です。
デフォルトの Java インポートパスを設定する:
set odps.sql.type.system.odps2=true;
set odps.sql.session.resources=odps-test.jar;
set odps.sql.session.java.imports=com.aliyun.odps.test.*;
-- インポートを設定すると、完全修飾パッケージ名を省略できます。
select new IntegerMaxValue().evaluate();
odps.sql.session.java.imports にはクラスパス(例:java.math.BigInteger)またはワイルドカード(*)を指定できます。静的インポートはサポートされていません。
サポートされる操作
UDT は SQL 式内で以下の操作をサポートします。
-
newを使用したオブジェクトの作成 — 例:new java.math.BigInteger('123') -
初期化子リスト付きの
newを使用した配列の作成 — 例:new Integer[] { 1, 2, 3 } -
インスタンスメソッドおよび静的メソッドの呼び出し
-
パブリックインスタンスフィールドおよび静的フィールドへのアクセス
パブリックメソッドおよびパブリックフィールドのみアクセス可能です。すべての識別子(パッケージ名、クラス名、メソッド名、フィールド名)は大文字と小文字を区別します。無名クラスおよびラムダ式はサポートされていません。戻り値を持たない関数は式内で呼び出せません。
データ型
型マッピング
Java データ型は MaxCompute のビルトイン型にマッピングされます。Java UDF で使用されるマッピングは、UDT にも適用されます。
-
ビルトイン型のメソッドを直接呼び出す:
'123'.length()、1L.hashCode() -
ビルトイン関数内で UDT を使用する:
chr(Long.valueOf('100'))—Long.valueOfはjava.lang.Longを返し、これはビルトイン BIGINT 型にマッピングされます。 -
Java のプリミティブ型は自動的に対応するボクシング型に変換されます。
新しいビルトインデータ型 を使用する場合は、クエリ実行前に set odps.sql.type.system.odps2=true; を追加してください。
型変換
-
SQL 型変換 がサポートされています:
cast(1 as java.lang.Object) -
Java スタイルのキャスト はサポートされていません:
(Object)1 -
UDT オブジェクトは基底クラスオブジェクトに暗黙的に変換可能です。
-
UDT オブジェクトは明示的に(キャストにより)基底クラスまたはサブクラスオブジェクトに変換可能です。
-
互いに関係のない 2 つの型間の変換は、ビルトイン型変換と同じルールに従います。たとえば、
java.lang.Longからjava.lang.Integerへの変換は、BIGINT から INT への変換と同じルールが適用され、データ損失が発生する可能性があります。
UDT オブジェクトはディスクに保存できず、テーブルに直接挿入できません(DDL は列型として UDT をサポートしていません)。UDT 値がビルトイン型に暗黙的に変換可能な場合は、テーブルに書き込めます。BINARY は自動シリアル化をサポートしており、byte[]配列は保存および逆シリアル化可能です。UDT を永続化するには、シリアル化および逆シリアル化メソッドを使用して BINARY に変換してください。UDT 値は最終出力に直接表示できません。toString()を呼び出して任意の UDT を表示用のjava.lang.Stringに変換してください。デバッグ中にすべての UDT 出力を自動的に文字列に変換するには、以下のフラグを使用します。このフラグは PRINT 文にのみ適用され、INSERT 文には適用されません。
set odps.sql.udt.display.tostring=true;
ジェネリクス
UDT は Java ジェネリクスをサポートします。コンパイラは引数から型パラメーターを推論します。
-- java.util.List<java.math.BigInteger> を返す
java.util.Arrays.asList(new java.math.BigInteger('1'))
コンストラクター呼び出しで型パラメーターを明示的に指定するか、java.lang.Object を使用してください。
-- ArrayList<Object>
new java.util.ArrayList(java.util.Arrays.asList('1', '2'))
-- ArrayList<String>
new java.util.ArrayList<String>(java.util.Arrays.asList('1', '2'))
演算子のセマンティクス
すべての演算子は Java のセマンティクスではなく、MaxCompute SQL のセマンティクスに従います。
-
文字列連結:
String.valueOf(1) + String.valueOf(2)は3を返します(両方の文字列が DOUBLE に暗黙的にキャストされ、加算されます)。文字列として連結するには、代わりに文字列連結関数を使用してください。 -
等価性:
=演算子は Java の参照等価性ではなく、SQL の比較演算子です。2 つのオブジェクトが等価かどうかを確認するには、equalsメソッドを使用してください。
オブジェクトの等価性とデータのシャッフリング
UDT にはオブジェクトの等価性の明確な定義がありません。データのシャッフリング中に、オブジェクトはプロセス間や物理マシン間で送信される可能性があり、単一のオブジェクトが 2 つの異なる参照として表示されることがあります。UDT オブジェクトを比較する際は常に、equals メソッドを使用し、= は使用しないでください。
同一行または同一列内のオブジェクトは相関していますが、行間または列間の相関は保証されません。
制限事項
UDT は JOIN、GROUP BY、DISTRIBUTE BY、SORT BY、ORDER BY、および CLUSTER BY 句のシャッフルキーとして使用できません。これらのステージでの式内では UDT が有効ですが、出力としては使用できません。例:
-
group by new java.math.BigInteger('123')— サポートされていません -
group by new java.math.BigInteger('123').hashCode()— サポートされています。hashCode()はint.classを返し、これはビルトイン INT 型にマッピングされるためです。
UDF、ユーザー定義集約関数(UDAF)、および UDT は、以下のテーブルタイプからデータを読み取れません。
-
スキーマ進化が実行されたテーブル
-
複雑なデータ型を含むテーブル
-
JSON データ型を含むテーブル
-
トランザクションテーブル
リソースへのアクセス
MaxCompute SQL 内で、静的メソッド com.aliyun.odps.udf.impl.UDTExecutionContext.get() を呼び出して ExecutionContext オブジェクトを取得します。このオブジェクトを使用して、リソースとして登録されたファイルやテーブルを含む現在の実行コンテキストにアクセスできます。
パフォーマンスに関する考慮事項
UDT のパフォーマンスは UDF のパフォーマンスと同程度です。最適化された計算エンジンにより、特定のシナリオでさらなる改善が図られます。
-
ローカル操作におけるシリアル化オーバーヘッドなし。 UDT オブジェクトが同一プロセス内で使用される場合(JOIN や AGGREGATE ステージなど、データのシャッフリングが不要な場合)、シリアル化および逆シリアル化はスキップされます。
-
コード生成ベースのランタイム。 UDT はリフレクションではなくコード生成(Codegen)を介して実行されるため、リフレクションのオーバーヘッドがありません。複数の UDT 呼び出しは単一の関数呼び出しにバッチ処理されます。たとえば、
values[x].add(values[y]).divide(java.math.BigInteger.valueOf(2))は一度だけ呼び出され、呼び出しごとのインターフェイスオーバーヘッドを回避します。
セキュリティ
UDT は UDF と同様に、同じJava サンドボックスモデルの制約を受けます。サンドボックスにより制限された操作を実行するには、それらの操作についてサンドボックス隔離を解除するか、サンドボックスホワイトリストへの参加を申請してください。
今後の改善予定機能
以下の機能は今後のバージョンで実装を予定しています。
-
戻り値を持たない関数や、転送されたデータを直接使用する関数(List インターフェイスの
addメソッドのように戻り値が無視されるもの)の呼び出し。 -
無名クラスおよびラムダ式の使用。
-
UDT をシャッフルキーとして使用すること。
-
Python などのより多くのプログラミング言語のサポート。
次のステップ
-
Java UDF — データ型マッピング表および UDF 実装リファレンス
-
新しいビルトインデータ型 —
set odps.sql.type.system.odps2=true;を必要とする型 -
SELECT TRANSFORM — SQL ステートメントにスクリプトを埋め込む
-
COLLECT_SET および その他の集計関数 — UDT と組み合わせて集計およびテーブル値関数の動作を実装
-
Java サンドボックス — サンドボックスモデルおよびホワイトリスト申請