함수

함수 (Functions)

SQL 함수를 구현하는 방법을 설명하는 문서예요. Trino 함수 프레임워크와 애노테이션 사용법을 다뤄요.

출처: 문서

본문

플러그인 구현 (Plugin implementation)

함수 프레임워크는 SQL 함수를 구현하는 데 사용돼요. Trino에는 많은 내장 함수가 포함돼 있어요. 새 함수를 구현하려면 getFunctions()에서 함수를 하나 이상 반환하는 플러그인을 작성하면 돼요.

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() 메서드는 이 튜토리얼에서 구현할 함수들의 모든 클래스를 담아요.

코드베이스의 전체 예시는 Trino 소스의 plugin 디렉터리에 있는, 머신러닝 함수용 trino-ml 모듈이나 Teradata 호환 함수용 trino-teradata-functions 모듈을 참고하세요.

스칼라 함수 구현 (Scalar function implementation)

함수 프레임워크는 함수 이름, 설명, 반환 타입, 파라미터 타입을 포함한 함수 관련 정보를 나타내는 애노테이션을 사용해요. 아래는 is_null을 구현한 예시 함수예요.

public class ExampleNullFunction
{
    @ScalarFunction("is_null", deterministic = 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이 아니라 Slice를 사용하는데, 이는 사실상 byte[]를 감싼 래퍼예요.

deterministic 인자는 함수에 부작용이 없고, 같은 인자(들)로 이후 호출하면 정확히 같은 값(들)을 반환한다는 뜻이에요. Trino에서 결정적(deterministic) 함수는 변하는 상태에 의존하지 않고 어떤 상태도 수정하지 않아요. deterministic 플래그는 선택 사항이며 기본값은 true예요.

예를 들어 shuffle() 함수는 난수 값을 사용하므로 비결정적이에요. 반면 now()는 단일 쿼리 안의 이후 호출이 같은 타임스탬프를 반환하므로 결정적이에요.

비결정적 동작이 있는 함수는 예상치 못한 결과를 피하려면 반드시 deterministic = false로 설정해야 해요.

  • @SqlType:

    @SqlType 애노테이션은 반환 타입과 인자 타입을 선언하는 데 사용돼요. Java 코드의 반환 타입과 인자가 해당 애노테이션의 네이티브 컨테이너 타입과 일치해야 해요.

  • @SqlNullable:

    @SqlNullable 애노테이션은 인자가 NULL일 수 있음을 나타내요. 이 애노테이션이 없으면 프레임워크는 인자 중 하나라도 NULL이면 모든 함수가 NULL을 반환한다고 가정해요. BigintType처럼 원시(primitive) 네이티브 컨테이너 타입을 가진 Type을 다룰 때는 @SqlNullable을 쓸 때 네이티브 컨테이너 타입의 객체 래퍼를 사용해요. 인자가 null이 아닐 때 NULL을 반환할 수 있으면 메서드는 @SqlNullable로 애노테이션해야 해요.

  • @Name:

    @Name 애노테이션은 인자의 SQL에서 보이는 파라미터 이름을 선언해요. 이름이 선언되면 함수는 위치 형식(positional form)에 더해 명명 인자 형식 f(name => value)으로도 호출할 수 있어요. 파라미터에 @Name 애노테이션이 없는 함수는 위치적으로만 호출할 수 있어요. 예:

@ScalarFunction("clamp")
@SqlType(StandardTypes.BIGINT)
public static long clamp(
        @Name("value") @SqlType(StandardTypes.BIGINT) long value,
        @Name("lo") @SqlType(StandardTypes.BIGINT) long lo,
        @Name("hi") @SqlType(StandardTypes.BIGINT) long hi)
{
    return Math.max(lo, Math.min(hi, value));
}

등록 후 함수는 두 방식 모두로 호출할 수 있어요.

SELECT clamp(7, 0, 5);
SELECT clamp(value => 7, hi => 5, lo => 0);

파라메트릭 스칼라 함수 (Parametric scalar functions)

타입 파라미터를 가진 스칼라 함수는 약간 더 복잡해요. 아까 예시를 어떤 타입에서도 동작하게 하려면 다음이 필요해요.

@ScalarFunction(name = "is_null")
@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 타입의 파라미터를 애노테이션하는 데도 쓸 수 있어요. 런타임에 엔진이 이 파라미터에 구체 타입을 바인딩해요. @OperatorDependency는 주어진 타입 파라미터에서 동작하는 추가 함수가 필요하다고 선언하는 데 사용할 수 있어요. 예를 들어 다음 함수는 equals 함수가 정의된 타입에만 바인딩돼요.

@ScalarFunction(name = "is_equal_or_null")
@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
}

또 다른 스칼라 함수 예시 (Another scalar function example)

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이 돼요(함수는 호출되지 않아요).

집계 함수 구현 (Aggregation function implementation)

집계 함수는 스칼라 함수와 비슷한 프레임워크를 사용하지만 조금 더 복잡해요.

  • AccumulatorState:

    모든 집계 함수는 입력 행을 상태 객체에 축적하는데, 이 객체는 AccumulatorState를 구현해야 해요. 단순한 집계의 경우에는 원하는 getter·setter로 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 카운트예요. LongAndDoubleStateAccumulatorState를 확장하는 인터페이스예요.

public interface LongAndDoubleState
        extends AccumulatorState
{
    long getLong();

    void setLong(long value);

    double getDouble();

    void setDouble(double value);
}

위에서 말한 대로, 단순한 AccumulatorState 객체는 getter·setter로 인터페이스를 정의하기만 하면 충분하고 프레임워크가 구현을 생성해줘요.

집계 함수 작성에 관련된 다양한 애노테이션에 대해 자세히 살펴볼게요.

  • @InputFunction:

    @InputFunction 애노테이션은 입력 행을 받아 AccumulatorState에 저장하는 함수를 선언해요. 스칼라 함수와 비슷하게 인자를 @SqlType로 애노테이션해야 해요. 위의 스칼라 예시에서 VARCHAR를 담는 데 Slice를 쓴 것과 달리, input의 인자에는 원시 double 타입이 사용된다는 점에 유의하세요. 이 예시에서 input 함수는 행의 누적 카운트(setLong())와 누적 합(setDouble())을 계속 추적해요.

  • @CombineFunction:

    @CombineFunction 애노테이션은 두 상태 객체를 결합하는 데 사용하는 함수를 선언해요. 이 함수는 모든 부분 집계 상태를 병합하는 데 사용돼요. 두 상태 객체를 받아 결과를 첫 번째 것에 병합해요(위 예시에서는 단순히 서로 더하는 방식).

  • @OutputFunction:

    @OutputFunction은 집계를 계산할 때 마지막으로 호출되는 함수예요. 최종 상태 객체(모든 부분 상태를 병합한 결과)를 받아 결과를 BlockBuilder에 써요.

  • 직렬화는 어디서 일어나고 GroupedAccumulatorState는 무엇인가요?

    @InputFunction은 보통 @CombineFunction과 다른 워커에서 실행되므로, 상태 객체는 집계 프레임워크가 이 워커들 사이에서 직렬화·전송해요. GroupedAccumulatorStateGROUP BY 집계를 수행할 때 사용되며, AccumulatorStateFactory를 지정하지 않으면 구현이 자동 생성돼요.

폐기된 함수 (Deprecated function)

더 이상 사용하면 안 되는 함수에는 @Deprecated 애노테이션을 사용해야 해요. 이 애노테이션으로 Trino는 SQL 문이 폐기된 함수를 사용할 때마다 경고를 생성해요. 함수가 폐기되면 @Description을 폐기 안내와 대체 함수에 대한 메모로 교체해야 해요.

public class ExampleDeprecatedFunction
{
    @Deprecated
    @ScalarFunction("bad_function")
    @Description("(DEPRECATED) Use good_function() instead")
    @SqlType(StandardTypes.BOOLEAN)
    public static boolean bad_function()
    {
        return false;
    }
}

더 알아보기 (Learn more)

함수 프레임워크 전체 동작 방식은 SPI 개요와 플러그인 문서를 함께 보면 흐름이 이어져요.