Java UDFs の設計

このトピックは、Java UDFs を設計するための助けになります。

このトピックの内容:

データ型の選択

コードを記述する前に、

  • 関数が引数として受け入れるデータ型と、関数が返すデータ型を選択します。

  • タイムゾーン関連の問題を考慮に入れます。

  • NULL 値の処理方法を決定します。

パラメーターと戻り値型の SQL-Javaデータ型のマッピング

次のテーブルは、 SQL とJavaの間の型マッピングを示しています。これらのマッピングは通常、Java UDF に渡される引数と UDF から返される値の両方に適用されます。ただし、脚注に記載されているいくつかの例外があります。

一部の SQL データ型(例: NUMBER)は、複数のJavaデータ型(例: int、long)と互換性があります。このような場合、渡される実際の値を保持するための十分な容量がある、任意のJavaデータ型を使用できます。互換性のないJavaデータ型に SQL 値を渡すと(またはその逆)、Snowflakeはエラーをスローします。

SQL 型

Java型

メモ

NUMBER

short

nullにはできません。shortの範囲内に収まる必要があります(小数部分なし。整数部分は、shortの最大値/最小値を超えることはできません)。

NUMBER

Short

shortの範囲内に収まる必要があります(小数部分なし。整数部分は、shortの最大値/最小値を超えることはできません)。

NUMBER

int

nullにはできません。intの範囲内に収まる必要があります(小数部分なし。整数部分は、intの最大値/最小値を超えることはできません)。

NUMBER

Integer

intの範囲内に収まる必要があります(小数部分なし。整数部分は、intの最大値/最小値を超えることはできません)。

NUMBER

long

nullにはできません。longの範囲内に収まる必要があります(小数部分なし。整数部分は、longの最大値/最小値を超えることはできません)。

NUMBER

Long

longの範囲内に収まる必要があります(小数部分なし。整数部分は、longの最大値/最小値を超えることはできません)。

NUMBER

java.math.BigDecimal

NUMBER

java.math.BigInteger

BigInteger の範囲内に収まる必要があります(小数部分なし)。

NUMBER

String

FLOAT

double

nullにはできません。

FLOAT

Double

FLOAT

float

nullにはできません。精度が低下する可能性があります。

FLOAT

Float

精度が低下する可能性があります。

FLOAT

String

精度が低下する可能性があります(floatから文字列への変換で損失が発生)。

VARCHAR

String

BINARY

byte[]

BINARY

String

バイナリ文字列を16進数でエンコードします。 4

BOOLEAN

boolean

nullにはできません。

BOOLEAN

Boolean

BOOLEAN

String

4

DATE

java.sql.Date

DATE

String

日付を YYYY-MM-DD としてフォーマットします。 4

TIME

java.sql.Time

3

TIME

String

時刻を HH:MI:SS.SSSSSSSSS としてフォーマットします。秒の小数部は、時刻の精度に依存します。 3

TIMESTAMP_LTZ

java.sql.Timestamp

java.sql.Timestampの範囲内に収まる必要があります。 3

TIMESTAMP_LTZ

String

出力形式は DY, DD MON YYYY HH24:MI:SS TZHTZM です。 134

TIMESTAMP_NTZ

java.sql.Timestamp

java.sql.Timestampの範囲内に収まる必要があります。実時間をUnixエポックからのオフセットとして扱います(事実上、 UTCタイムゾーンを強制)。 3

TIMESTAMP_NTZ

String

実時間をUnixエポックからのオフセットとして扱います(事実上、 UTCタイムゾーンを強制)。出力形式は DY, DD MON YYYY HH:MI:SS です。 234

TIMESTAMP_TZ

java.sql.Timestamp

java.sql.Timestampの範囲内に収まる必要があります。 3

TIMESTAMP_TZ

String

出力形式は DY, DD MON YYYY HH24:MI:SS TZHTZM です。 134

VARIANT

String

表示されるタイプに応じて値をフォーマットします。 バリアントnull は、文字列「null」としてフォーマットされます。

OBJECT

Map<文字列, 文字列>

マップのキーはオブジェクトのキーであり、値は文字列としてフォーマットされます。

OBJECT

String

オブジェクトを JSON 文字列(例: {"x": 3, "y": true})としてフォーマットします。

ARRAY

String[]

配列の要素を文字列としてフォーマットします。

ARRAY

String

配列を JSON 文字列(例: [1, "foo", null])としてフォーマットします。

GEOGRAPHY

String

地理を GeoJSON としてフォーマットします。

1(1,2)

この形式は、 タイムスタンプ形式 で説明されているインターネット(RFC)タイムスタンプ形式 DY, DD MON YYYY HH24:MI:SS TZHTZM と一致します。タイムゾーンオフセット(TZHTZM コンポーネント)が存在する場合、通常は数字です(例: -0700 は UTC からマイナス7時間であることをを示す)。タイムゾーンオフセットが数字ではなく Z (「Zulu」の頭文字)の場合、それは「+0000」(UTC)と同義です。

2

この形式は、 タイムスタンプ形式 で説明されているインターネット(RFC)タイムスタンプ形式 DY, DD MON YYYY HH24:MI:SS と一致します。文字列の後にスペースと Z (「Zulu」の頭文字)が続く場合、オフセットが「+0000」(UTC)であることを明示的に示します。

3(1,2,3,4,5,6,7,8)

Snowflakeはナノ秒の精度で時間値を保存できますが、java.sql.timeライブラリはミリ秒の精度しか維持できません。SnowflakeとJavaの間でデータ型を変換すると、実効精度がミリ秒に低下する可能性があります。

4(1,2,3,4,5,6)

この型マッピングは、 SQL 引数をJavaに変換する場合はサポートされますが、Javaの戻り型を SQL 型に変換する場合はサポートされません。

TIMESTAMP_LTZ 値とタイムゾーン

Java UDF は多くの場合、呼び出される環境から分離されています。ただし、タイムゾーンは呼び出し元の環境から継承されます。呼び出し元のセッションがJava UDF を呼び出す前にデフォルトのタイムゾーンを設定した場合、Java UDF のデフォルトのタイムゾーンは同じになります。

NULL 値

Snowflakeは、 SQL NULL と VARIANT の JSON null という2つの異なる NULL 値をサポートしています。(Snowflake VARIANT NULL については、 NULL 値 を参照。)

Javaは、1つの null 値をサポートします。これは、非プリミティブデータ型専用です。

Java UDF に対する SQL NULL 引数は、 null をサポートするJavaデータ型の場合にのみ、Java null 値に変換されます。

返されたJava null 値は、 SQL NULL に変換されます。

引数の配列および可変の数

Java UDFs は、次に挙げるJavaデータ型のいずれかの配列を受け取ることができます。

  • String

  • boolean

  • double

  • float

  • int

  • long

  • short

渡される SQL 値のデータ型には、対応するJavaデータ型との互換性が必要です。データ型の互換性の詳細については、 パラメーターと戻り値型の SQL-Javaデータ型のマッピング をご参照ください。

指定されたJavaデータ型のそれぞれに、次の追加ルールが適用されます。

  • boolean: Snowflake ARRAY には BOOLEAN 要素のみが含まれている必要があり、 NULL 値は含まれていてはなりません。

  • int/short/long: Snowflake ARRAY には、スケールが0の 固定小数点 要素のみが含まれている必要があり、 NULL 値が含まれていてはなりません。

  • float/double: Snowflake ARRAY には次のいずれかが含まれている必要があります。

    ARRAY には NULL の値を含めることはできません。

Javaメソッドは、次に挙げる2つの方法のいずれかでこれらの配列を受け取ることができます。

  • Javaの配列機能を使用。

  • Javaの varargs (引数の数が可変)機能を使用。

どちらの場合も、 SQL コードは ARRAY を渡す必要があります。

ARRAY 経由の引き渡し

Javaパラメーターを配列として宣言します。たとえば、次に挙げるメソッドの3番目のパラメーターは文字列配列です。

static int MyMethod(int fixed_argument_1, int fixed_argument_2, String[] string_array)

以下は包括的な例です。

テーブルを作成してロードします。

CREATE TABLE string_array_table(id INTEGER, a ARRAY);
INSERT INTO string_array_table (id, a) SELECT
        1, ARRAY_CONSTRUCT('Hello');
INSERT INTO string_array_table (id, a) SELECT
        2, ARRAY_CONSTRUCT('Hello', 'Jay');
INSERT INTO string_array_table (id, a) SELECT
        3, ARRAY_CONSTRUCT('Hello', 'Jay', 'Smith');

UDF を作成します。

create or replace function concat_varchar_2(a ARRAY)
returns varchar
language java
handler='TestFunc_2.concat_varchar_2'
target_path='@~/TestFunc_2.jar'
as
$$
    class TestFunc_2 {
        public static String concat_varchar_2(String[] string_array) {
            return String.join(" ", string_array);
        }
    }
$$;

UDF を呼び出します。

SELECT concat_varchar_2(a)
    FROM string_array_table
    ORDER BY id;
+---------------------+
| CONCAT_VARCHAR_2(A) |
|---------------------|
| Hello               |
| Hello Jay           |
| Hello Jay Smith     |
+---------------------+

Varargs経由の引き渡し

Varargsの使用は、配列の使用と非常によく似ています。

Javaコードでは、Javaのvarargs宣言スタイルを使用します。

static int MyMethod(int fixed_argument_1, int fixed_argument_2, String ... string_array)

以下は包括的な例です。この例と前の例(配列の場合)の間における唯一の重要な違いは、メソッドへのパラメーター宣言です。

テーブルを作成してロードします。

CREATE TABLE string_array_table(id INTEGER, a ARRAY);
INSERT INTO string_array_table (id, a) SELECT
        1, ARRAY_CONSTRUCT('Hello');
INSERT INTO string_array_table (id, a) SELECT
        2, ARRAY_CONSTRUCT('Hello', 'Jay');
INSERT INTO string_array_table (id, a) SELECT
        3, ARRAY_CONSTRUCT('Hello', 'Jay', 'Smith');

UDF を作成します。

create or replace function concat_varchar(a ARRAY)
returns varchar
language java
handler='TestFunc.concat_varchar'
target_path='@~/TestFunc.jar'
as
$$
    class TestFunc {
        public static String concat_varchar(String ... string_array) {
            return String.join(" ", string_array);
        }
    }
$$;

UDF を呼び出します。

SELECT concat_varchar(a)
    FROM string_array_table
    ORDER BY id;
+-------------------+
| CONCAT_VARCHAR(A) |
|-------------------|
| Hello             |
| Hello Jay         |
| Hello Jay Smith   |
+-------------------+

Snowflakeによる制約の範囲内にとどまるJava UDFs の設計

Snowflake環境内の安定性を確保するために、Snowflakeは UDFs に次の制限を設けています。特に明記されていない限り、これらの制限は、 UDF が作成されたときではなく、実行されたときに適用されます。

メモリ

あまり多くのメモリを消費しないようにしてください。

  • 大きなデータ値(通常は BINARY、長い VARCHAR、または大きな OBJECT または VARIANT データ型)は、大量のメモリを消費する可能性があります。

  • スタックが深すぎると、大量のメモリを消費する可能性があります。Snowflakeでは、ネストされた50レベルの単純な関数呼び出しをエラーなしでテストしました。実際の上限は、スタックに配置される情報の量によって異なります。

UDFs がメモリを消費しすぎると、エラーを返します。特定の制限は変更される場合があります。

時間

呼び出しごとに長い時間がかかるアルゴリズムは避けてください。

UDF の完了に時間がかかりすぎると、Snowflakeは SQL ステートメントを強制終了し、ユーザーにエラーを返します。これにより、無限ループなどのエラーの影響およびコストが制限されます。

ライブラリ

Javaメソッドは標準のJavaライブラリのクラスとメソッドを使用できますが、Snowflakeのセキュリティ制限により、ファイルへの書き込みなど、一部の機能が無効になります。ライブラリの制限の詳細については、 優れたセキュリティプラクティスに準拠 というタイトルのセクションをご参照ください。

クラスの設計

SQL ステートメントがJava UDF を呼び出すと、Snowflakeは作成したJavaメソッドを呼び出します。Javaメソッドは、「ハンドラーメソッド」または略して「ハンドラー」と呼ばれます。

他のJavaメソッドと同様に、メソッドはクラスの一部として宣言する必要があります。ハンドラーメソッドは、クラスの静的メソッドまたはインスタンスメソッドにすることができます。ハンドラーがインスタンスメソッドであり、クラスが引数なしのコンストラクターを定義している場合、Snowflakeは初期化時にコンストラクターを呼び出して、クラスのインスタンスを作成します。ハンドラーが静的メソッドの場合、クラスにコンストラクターは必要ありません。

ハンドラーは、Java UDF に渡される行ごとに1回呼び出されます。(注: クラスの新しいインスタンスは行ごとに作成されません。Snowflakeは、同じインスタンスのハンドラーメソッドを複数回呼び出すことも、同じ静的メソッドを複数回呼び出すこともできます。)

コードの実行を最適化するために、Snowflakeは、ハンドラーメソッドの実行が速い一方で、初期化が遅い可能性があると想定しています。Snowflakeは、初期化を実行するためのタイムアウト(UDF をロードする時間と、コンストラクターが定義されている場合は、ハンドラーメソッドを含むクラスのコンストラクターを呼び出すための時間を含む)を、ハンドラーを実行するためのタイムアウト(入力1行でハンドラーを呼び出す時間)よりも長く設定します。

クラスの設計に関する追加情報は Java UDFs の作成 にあります。

スカラー UDFs における初期化の最適化とグローバル状態の制御

ほとんどのスカラー UDFs は、以下のガイドラインに従う必要があります。

  • 行間で変化しない共有状態を初期化する必要がある場合は、 UDF クラスのコンストラクターで初期化します。

  • スレッドセーフになるようにハンドラーメソッドを記述します。

  • 行間で動的状態を保存および共有することは避けてください。

UDF がこれらのガイドラインに従えない場合、またはこれらのガイドラインの理由をより深く理解したい場合は、次のいくつかのサブセクションをお読みください。

紹介

Snowflakeは、スカラー UDFs が独立して処理されることを期待しています。システムは任意の順序で行を処理し、それらの呼び出しを複数の JVMs に分散できるため、呼び出し間で共有される状態に依存すると、予期しない動作が発生する可能性があります。UDFs ハンドラーメソッドの呼び出し全体で共有状態に依存することは避けてください。ただし、 UDF に共有状態を保存する必要がある状況は2つあります。

  • 行ごとに繰り返さない高価な初期化ロジックを含むコード。

  • キャッシュなど、行間で共有状態を活用するコード。

複数の行で状態を共有する必要があり、その状態が時間の経過とともに変化しない場合は、コンストラクターを使用してインスタンスレベルの変数を設定し、共有状態を作成します。コンストラクターはインスタンスごとに1回だけ実行されますが、ハンドラーは行ごとに1回呼び出されるため、ハンドラーが複数の行を処理する場合は、コンストラクターによる初期化の方が安価です。また、コンストラクターは1回だけ呼び出されるため、スレッドセーフになるようにコンストラクターを作成する必要はありません。

UDF に変更された共有状態が保存されている場合は、その状態への同時アクセスを処理できるようにコードを準備する必要があります。次の2つのセクションでは、並列処理と共有状態について詳しく説明します。

Java UDF 並列化について

パフォーマンスを向上させるために、Snowflakeは JVMs の全体および内部で並列化します。

  • JVMs 全体:

    Snowflakeは、 ウェアハウス のワーカー間で並列化されます。各ワーカーは1つ(または複数)の JVMs を実行します。これは、グローバル共有状態がないことを意味します。最大で、状態は単一の JVM 内でのみ共有できます。

  • JVMs 内部:

    • 各 JVM は、同じインスタンスのハンドラーメソッドを並行して呼び出すことができる複数のスレッドを実行できます。これは、各ハンドラーメソッドがスレッドセーフである必要があることを意味します。

    • SQL ステートメントが同じ行に対して同じ引数を使用して UDF を複数回呼び出す場合、 UDF は、その行の呼び出しごとに同じ値を返します。たとえば、次の場合、各行に対して同じ値が2回返されます。

      select
             my_java_udf(42),
             my_java_udf(42)
          from table1;
      

      同じ引数が渡された場合でも複数の呼び出しで独立した値を返すようにする場合は、複数の個別の UDFs を同じハンドラーメソッドにバインドします。例:

      1. 次のコードを使用して、 @java_udf_stage/rand.jar という名前の JAR ファイルを作成します。

        class MyClass {
        
            private double x;
        
            // Constructor
            public MyClass()  {
                x = Math.random();
            }
        
            // Handler
            public double myHandler() {
                return x;
            }
        }
        
      2. 以下に示すように、Java UDFs を作成します。これらの UDFs の名前は異なりますが、同じ JAR ファイルと、その JAR ファイル内の同じハンドラーを使用します。

        create my_java_udf_1()
            returns double
            language java
            imports = ('@java_udf_stage/rand.jar')
            handler = 'MyClass.myHandler';
        
        create my_java_udf_2()
            returns double
            language java
            imports = ('@java_udf_stage/rand.jar')
            handler = 'MyClass.myHandler';
        
      3. 次のコードは、両方の UDFs を呼び出します。UDFs は、同じ JAR ファイルとハンドラーをポイントします。これらの呼び出しは、同じクラスのインスタンスを2つ作成します。各インスタンスは独立した値を返すため、以下の例では、同じ値を2回返すのではなく、2つの独立した値を返します。

        select
                my_java_udf_1(),
                my_java_udf_2()
            from table1;
        

JVM 状態情報の保存

動的共有状態に依存しない理由の1つは、行が必ずしも予測可能な順序で処理されるとは限らないことです。Snowflakeは、 SQL ステートメントが実行されるたびに、バッチの数、バッチが処理される順序、およびバッチ内の行の順序を変更できます。1つの行が後続の行の戻り値に影響を与えるようにスカラー UDF が設計されている場合、 UDF は、 UDF が実行されるたびに異なる結果を返す可能性があります。

エラーの処理

UDF として使用されるJavaメソッドは、通常のJava例外処理手法を使用して、メソッド内のエラーをキャッチできます。

メソッド内で例外が発生し、メソッドによってキャッチされない場合、Snowflakeは例外のスタックトレースを含むエラーを発生させます。

クエリを終了して SQL エラーを生成するために、例外をキャッチせずにスローできます。例:

if (x < 0)  {
    throw new IllegalArgumentException("x must be non-negative.");
    }

デバッグ時に、 SQL エラーメッセージテキストに値を含めることができます。そうするには、Javaメソッドの本文全体をtry-catchブロックに配置し、キャッチしたエラーのメッセージに引数値を追加し、拡張メッセージで例外をスローします。機密データが公開されないようにするには、 JAR ファイルを実稼働環境に展開する前に引数値を削除します。

ベストプラクティスに準拠

  • プラットフォームに依存しないコードを記述します。

    • 特定の CPU アーキテクチャ(例: x86)を想定したコードは避けます。

    • 特定のオペレーティングシステムを想定したコードは避けます。

  • 初期化コードを実行する必要があり、呼び出すメソッドに含めたくない場合は、初期化コードを静的初期化ブロックに入れることができます。

こちらもご参照ください。

優れたセキュリティプラクティスに準拠

  • メソッド(および呼び出すライブラリメソッド)は純粋関数として機能する必要があり、受信したデータのみに作用し、そのデータに基づいて、副作用を引き起こすことなく値を返します。コードは、妥当な量のメモリとプロセッサ時間を消費することを除いて、基になるシステムの状態に影響を与える可能性がないようにします。

  • Java UDFs は、制限されたエンジン内で実行されます。使用するコードも、呼び出すライブラリメソッド内のコードも、次のような禁止されているシステムコールを呼び出さないようにしてください。

    • プロセス制御。たとえば、プロセスをフォークすることはできません。(ただし、複数のスレッドは使用可。)

    • ファイルシステムへのアクセス。

      次の例外を除いて、Java UDFs はファイルを読み取ったり、書き込んだりすべきではありません。

      • Java UDFs は、 CREATE FUNCTION コマンドの imports 句で指定されたファイルを読み取ることができます。詳細については、 CREATE FUNCTION をご参照ください。

      • Java UDFs は、ログファイルなどのファイルを /tmp ディレクトリに書き込むことができます。

        各クエリは、独自の/tmpが保存されている独自のメモリバックアップファイルシステムを取得するため、異なるクエリでファイル名が競合することはありません。

        ただし、1つのクエリが複数の UDF を呼び出し、それらの UDFs が同じファイル名に書き込もうとすると、クエリ内で競合が発生する可能性があります。

    • ネットワークアクセス。

      注釈

      コードは、ネットワークに直接的または間接的にアクセスできないため、Snowflake JDBC ドライバーのコードを使用してデータベースにアクセスすることはできません。UDF 自体がSnowflakeのクライアントとして機能することはできません。

  • JNI (Java Native Interface)はサポートされていません。Snowflakeは、(Javaバイトコードではなく)ネイティブコードを含むライブラリのロードを禁止しています。