関数

プラグインの実装

関数フレームワークは、SQL 関数を実装するために使用されます。Presto には、多数の組み込み関数が含まれています。新しい関数を実装するには、getFunctions() から 1 つ以上の関数を返すプラグインを作成できます。

public class ExampleFunctionsPlugin
        implements Plugin
{
    @Override
    public Set<Class<?>> getFunctions()
    {
        return ImmutableSet.<Class<?>>builder()
                .add(ExampleNullFunction.class)
                .add(IsNullFunction.class)
                .add(IsEqualOrNullFunction.class)
                .add(ExampleStringFunction.class)
                .add(ExampleAverageFunction.class)
                .build();
    }
}

ImmutableSet クラスは Guava のユーティリティクラスであることに注意してください。getFunctions() メソッドには、このチュートリアルで以下に実装する関数のすべてのクラスが含まれています。

コードベースの完全な例については、Presto ソースのルートにある、機械学習関数用の presto-ml モジュール、または Teradata 互換関数用の presto-teradata-functions モジュールのいずれかを参照してください。

スカラー関数の実装

関数フレームワークは、名前、説明、戻り値の型、パラメーターの型など、関数に関する関連情報を示すためにアノテーションを使用します。以下は、is_null を実装するサンプル関数です。

public class ExampleNullFunction
{
    @ScalarFunction("is_null", calledOnNullInput = true)
    @Description("Returns TRUE if the argument is NULL")
    @SqlType(StandardTypes.BOOLEAN)
    public static boolean isNull(@SqlNullable @SqlType(StandardTypes.VARCHAR) Slice string)
    {
        return (string == null);
    }
}

関数 is_null は、単一の VARCHAR 引数を取り、引数が NULL であるかどうかを示す BOOLEAN を返します。関数の引数が Slice 型であることに注意してください。VARCHAR はネイティブコンテナ型として String ではなく、本質的に byte[] のラッパーである Slice を使用します。

  • @SqlType:

    @SqlType アノテーションは、戻り値の型と引数の型を宣言するために使用されます。Java コードの戻り値の型と引数が、対応するアノテーションのネイティブコンテナ型と一致する必要があることに注意してください。

  • @SqlNullable:

    @SqlNullable アノテーションは、引数が NULL になる可能性があることを示します。このアノテーションがない場合、フレームワークは、いずれかの引数が NULL の場合、すべての関数が NULL を返すと想定します。BigintType のように、プリミティブなネイティブコンテナ型を持つ Type を使用する場合は、@SqlNullable を使用するときにネイティブコンテナ型のオブジェクトラッパーを使用します。引数が null でない場合に NULL を返すことができる場合は、メソッドに @SqlNullable のアノテーションを付ける必要があります。

パラメトリック スカラー関数

型パラメーターを持つスカラー関数には、追加の複雑さがあります。前の例を任意の型で動作させるには、次のものが必要です。

@ScalarFunction(name = "is_null", calledOnNullInput = true)
@Description("Returns TRUE if the argument is NULL")
public final class IsNullFunction
{
    @TypeParameter("T")
    @SqlType(StandardTypes.BOOLEAN)
    public static boolean isNullSlice(@SqlNullable @SqlType("T") Slice value)
    {
        return (value == null);
    }

    @TypeParameter("T")
    @SqlType(StandardTypes.BOOLEAN)
    public static boolean isNullLong(@SqlNullable @SqlType("T") Long value)
    {
        return (value == null);
    }

    @TypeParameter("T")
    @SqlType(StandardTypes.BOOLEAN)
    public static boolean isNullDouble(@SqlNullable @SqlType("T") Double value)
    {
        return (value == null);
    }

    // ...and so on for each native container type
}
  • @TypeParameter:

    @TypeParameter アノテーションは、引数の型 @SqlType アノテーション、または関数の戻り値の型で使用できる型パラメーターを宣言するために使用されます。また、Type 型のパラメーターにアノテーションを付けるためにも使用できます。実行時に、エンジンはこのパラメーターに具体的な型をバインドします。オプションで、@TypeParameterboundedBy 型クラスを提供することにより、型パラメーターを特定の型の派生型に制限できます。@OperatorDependency は、指定された型パラメーターを操作するための追加の関数が必要であることを宣言するために使用できます。たとえば、次の関数は、等しい関数が定義されている型にのみバインドします。

@ScalarFunction(name = "is_equal_or_null", calledOnNullInput = true)
@Description("Returns TRUE if arguments are equal or both NULL")
public final class IsEqualOrNullFunction
{
    @TypeParameter("T")
    @SqlType(StandardTypes.BOOLEAN)
    public static boolean isEqualOrNullSlice(
            @OperatorDependency(operator = OperatorType.EQUAL, returnType = StandardTypes.BOOLEAN, argumentTypes = {"T", "T"}) MethodHandle equals,
            @SqlNullable @SqlType("T") Slice value1,
            @SqlNullable @SqlType("T") Slice value2)
    {
        if (value1 == null && value2 == null) {
            return true;
        }
        if (value1 == null || value2 == null) {
            return false;
        }
        return (boolean) equals.invokeExact(value1, value2);
    }

    // ...and so on for each native container type
}

別のスカラー関数の例

lowercaser 関数は、単一の VARCHAR 引数を取り、小文字に変換された引数である VARCHAR を返します。

public class ExampleStringFunction
{
    @ScalarFunction("lowercaser")
    @Description("converts the string to alternating case")
    @SqlType(StandardTypes.VARCHAR)
    public static Slice lowercaser(@SqlType(StandardTypes.VARCHAR) Slice slice)
    {
        String argument = slice.toStringUtf8();
        return Slices.utf8Slice(argument.toLowerCase());
    }
}

文字列を小文字に変換するなど、ほとんどの一般的な文字列関数では、Slice ライブラリも基になる byte[] で直接動作する実装を提供しており、パフォーマンスが大幅に向上することに注意してください。この関数には @SqlNullable アノテーションがないため、引数が NULL の場合、結果は自動的に NULL になります(関数は呼び出されません)。

Codegen スカラー関数の実装

スカラー関数はバイトコードで実装することもでき、@TypeParameter に従って関数を特殊化および最適化できます。

  • @CodegenScalarFunction:

    @CodegenScalarFunction アノテーションは、バイトコードで実装されるスカラー関数を宣言するために使用されます。@SqlType アノテーションは、戻り値の型を宣言するために使用されます。これは、@SqlType アノテーションも持つパラメーターとして Type を取ります。戻り値の型は、codegen 関数メソッドである MethodHandle です。

public class CodegenArrayLengthFunction
{
    @CodegenScalarFunction("array_length", calledOnNullInput = true)
    @SqlType(StandardTypes.INTEGER)
    @TypeParameter("K")
    public static MethodHandle arrayLength(@SqlType("array(K)") Type arr)
    {
        CallSiteBinder binder = new CallSiteBinder();
        ClassDefinition classDefinition = new ClassDefinition(a(Access.PUBLIC, FINAL), makeClassName("ArrayLength"), type(Object.class));
        classDefinition.declareDefaultConstructor(a(PRIVATE));

        Parameter inputBlock = arg("inputBlock", Block.class);
        MethodDefinition method = classDefinition.declareMethod(a(Access.PUBLIC, STATIC), "array_length", type(Block.class), ImmutableList.of(inputBlock));
        BytecodeBlock body = method.getBody();
        body.append(inputBlock.invoke("getPositionCount", int.class).ret());

        Class<?> clazz = defineClass(classDefinition, Object.class, binder.getBindings(), CodegenArrayLengthFunction.class.getClassLoader());
        return new methodHandle(clazz, "array_length", Block.class), Optional.of();
    }
}

集計関数の実装

集計関数はスカラー関数と同様のフレームワークを使用しますが、少し複雑です。

  • AccumulatorState:

    すべてのアグリゲーション関数は、入力行を状態オブジェクトに累積します。このオブジェクトは AccumulatorState を実装する必要があります。単純なアグリゲーションの場合、必要なゲッターとセッターを持つ新しいインターフェースに AccumulatorState を拡張するだけで、フレームワークがすべての実装とシリアライザーを生成します。より複雑な状態オブジェクトが必要な場合は、AccumulatorStateFactoryAccumulatorStateSerializer を実装し、AccumulatorStateMetadata アノテーションを介して提供する必要があります。

以下のコードは、DOUBLE 列の平均を計算するアグリゲーション関数 avg_double を実装しています。

@AggregationFunction("avg_double")
public class AverageAggregation
{
    @InputFunction
    public static void input(LongAndDoubleState state, @SqlType(StandardTypes.DOUBLE) double value)
    {
        state.setLong(state.getLong() + 1);
        state.setDouble(state.getDouble() + value);
    }

    @CombineFunction
    public static void combine(LongAndDoubleState state, LongAndDoubleState otherState)
    {
        state.setLong(state.getLong() + otherState.getLong());
        state.setDouble(state.getDouble() + otherState.getDouble());
    }

    @OutputFunction(StandardTypes.DOUBLE)
    public static void output(LongAndDoubleState state, BlockBuilder out)
    {
        long count = state.getLong();
        if (count == 0) {
            out.appendNull();
        }
        else {
            double value = state.getDouble();
            DOUBLE.writeDouble(out, value / count);
        }
    }
}

平均には、列の各行の DOUBLE の合計と、表示された行数の LONG カウントの2つの部分があります。LongAndDoubleState は、AccumulatorState を拡張するインターフェースです。

public interface LongAndDoubleState
        extends AccumulatorState
{
    long getLong();

    void setLong(long value);

    double getDouble();

    void setDouble(double value);
}

上記のように、単純な AccumulatorState オブジェクトの場合、ゲッターとセッターを持つインターフェースを定義するだけで十分であり、フレームワークが実装を生成します。

アグリゲーション関数の記述に関連するさまざまなアノテーションの詳細な説明を以下に示します。

  • @InputFunction:

    @InputFunction アノテーションは、入力行を受け取り、AccumulatorState に保存する関数を宣言します。スカラ関数と同様に、引数に @SqlType をアノテーションする必要があります。上記のスカラの例では SliceVARCHAR を保持するために使用されるのとは異なり、入力の引数にはプリミティブな double 型が使用されることに注意してください。この例では、入力関数は、実行中の行数(setLong() を介して)と、実行中の合計(setDouble() を介して)を追跡するだけです。

  • @CombineFunction:

    @CombineFunction アノテーションは、2つの状態オブジェクトを結合するために使用される関数を宣言します。この関数は、すべての部分的なアグリゲーション状態をマージするために使用されます。2つの状態オブジェクトを取り、結果を最初のオブジェクトにマージします(上記の例では、単にそれらを加算するだけです)。

  • @OutputFunction:

    @OutputFunction は、アグリゲーションの計算時に最後に呼び出される関数です。最終的な状態オブジェクト(すべての部分状態をマージした結果)を取得し、結果を BlockBuilder に書き込みます。

  • シリアライズはどこで行われるのか、そして GroupedAccumulatorState とは何ですか?

    @InputFunction は通常、@CombineFunction とは異なるワーカーで実行されるため、状態オブジェクトはアグリゲーションフレームワークによってシリアライズされ、これらのワーカー間で転送されます。GroupedAccumulatorState は、GROUP BY アグリゲーションを実行するときに使用され、AccumulatorStateFactory を指定しない場合は、実装が自動的に生成されます。

高度なユースケース

生のブロック入力

スカラ関数とアグリゲーション関数のアノテーションの両方で、ネイティブ型を操作するメソッドを定義できます。Javaでは、これらのネイティブ型は booleanSlice、および long です。パラメーター化された実装またはパラメトリック型の場合、標準のJava型は入力データを表現できないため使用できません。

任意の型を受け入れることができるメソッドハンドルを定義するには、@BlockPosition@BlockIndex パラメーターと組み合わせて使用します。@SqlNullable アノテーションと同様に、ブロック位置が NULL の場合にのみ関数を呼び出すことを示すには、@NullablePosition アノテーションを使用します。

これは、スカラ関数とアグリゲーション関数の実装の両方で機能します。

@ScalarFunction("example")
public static Block exampleFunction(
        @BlockPosition @NullablePosition @SqlType("array(int)") Block block,
        @BlockIndex int index) { /* ...implementation */ }

@BlockPosition を使用したジェネリック型の適用

@BlockPosition 構文を使用する関数シグネチャは、関数が @TypeParameter アノテーションで定義されている場合、ジェネリック型を操作できます。@BlockPosition 引数を追加の @SqlType("T") アノテーションで拡張して、ジェネリック型に対応する引数を受け入れることを示します。これは、スカラ関数とアグリゲーション関数の実装の両方で機能します。

@ScalarFunction("example")
@TypeParameter("T")
public static Block exampleFunction(
        @BlockPosition @SqlType("T") Block block,
        @BlockIndex int index) { /* ...implementation */ }

@TypeParameter を使用したジェネリック型の取得

実装が型固有のロジックを実行できるようにするには、関数の引数リストの先頭に @TypeParameter アノテーションを追加します。関数のシグネチャの最初の引数として、Type に型付けされ、@TypeParameter でアノテーションされた引数を追加して、Type にアクセスできるようにします。これは、スカラ関数とアグリゲーション関数の両方で機能します。

@ScalarFunction("example")
@TypeParameter("T")
public static Block exampleFunction(
        @TypeParameter("T") Type type,
        @BlockPosition @SqlType("T") Block block,
        @BlockIndex int index) { /* ...implementation */ }