Presto 標量函數註冊和調用過程簡述

Presto 函數開發一文中已經介紹過如何進行函數開發,本文主要講述標量函數(Scalar Function)實現之後,是如何在Presto內部進行註冊和被調用的。主要講述標量函數是因為:三類函數的註冊和調用過程略有不同,而實際查詢中調用最多的是標量函數。

標量函數註冊

函數在能夠調用之前,首先要進行註冊,上一篇文章已經介紹過函數註冊的方法,那麼函數在註冊時究竟註冊了哪些資訊呢?函數註冊實際上是維護FunctinoRegistry類中的一個 MultiMap,Key 為函數的限定名(QualifiedName,可以簡單地理解為函數名),Value 為SqlFunction介面的實現類,實際主要為SqlAggregationFunctionSqlWindowFunctionSqlScalarFunction這三個類的子類。SqlScalarFunction是一個抽象類,定義如下:

public abstract class SqlScalarFunction
        implements SqlFunction
{
    private final Signature signature;

    protected SqlScalarFunction(Signature signature)
    {
        this.signature = requireNonNull(signature, "signature is null");
        checkArgument(signature.getKind() == SCALAR, "function kind must be SCALAR");
    }

    @Override
    public final Signature getSignature()
    {
        return signature;
    }

    public abstract ScalarFunctionImplementation specialize(BoundVariables boundVariables, int arity, TypeManager typeManager, FunctionRegistry functionRegistry);

    public static PolymorphicScalarFunctionBuilder builder(Class<?> clazz)
    {
        return new PolymorphicScalarFunctionBuilder(clazz);
    }
}

可以看出,其子類需要獲取Signature和實現specialize方法。

首先來看Signature

public final class Signature
{
    private final String name;
    private final FunctionKind kind;
    private final List<TypeVariableConstraint> typeVariableConstraints;
    private final List<LongVariableConstraint> longVariableConstraints;
    private final TypeSignature returnType;
    private final List<TypeSignature> argumentTypes;
    private final boolean variableArity;

    ....
}

類的成員變數說明如下:

  • name:函數名,不包括參數類型和結果類型,例如:函數isnull(T):boolean的函數名為isnull
  • kind:枚舉類型,有 SCALAR、AGGREGATE 和 WINDOW三種取值,用於區分函數類型
  • typeVariableConstraints:類型變數約束,記錄函數中的類型變數名,以及類型變數所需要滿足的約束條件:類型是否為comparable、orderable 和是否綁定具體類型。例如:contains<T:comparable>(array(T),T):boolean函數要求類型T滿足comparablearray_sort<E:orderable>(array(E)):array(E)函數要求類型E滿足orderable;判斷兩個ROW類型是否相等的操作符(操作符也屬於標量函數)$operator$EQUAL<T:comparable:row<*>>(T,T):boolean要求類型T為ROW類型。
  • LongVariableConstraint:長整型變數約束,記錄函數中帶有約束的長整型變數的計算表達式(一般用於計算返回類型中的長整型變數)。例如:函數concat<u:x + y>(char(x),char(y)):char(u)的返回類型中長整型變數u的計算表達式為x + y
  • returnType:函數的返回類型
  • argumentTypes:函數參數類型
  • variableArity:標記是否為變長參數

以上成員變數都可以從函數實現的類對象中,根據註解規則解析獲得。除了獲取Signature,由於同一個函數可能會有多個實現(例如上一篇文章介紹的isnull<T>(T):boolean函數,因為傳入的參數類型可能不同,所以有五個實現方法),所以還要記錄函數的實現方法。源碼中將實現方法分為三類:

  • exactimplementation:函數中不包含類型變數,即函數的參數類型和返回類型都是確定的
  • specializedImplementation:函數中包含類型變數,但類型變數作用在具體的 Java 類型(Native Container Type)上
  • genericImplement:函數中包含類型變數,但是類型變數作用在 Object 類型上

Presto 保存的是實現方法的MethodHandle,通過反射獲取Method,再保存Method對應的MethodHandleMethodHandle在JDK1.7引入,調用的效率比反射高),如果該方法不是靜態方法,還要將MethodHandle的中的this參數改為Object來避免調用時的類載入問題。所以,抽象方法specialize的本質是通過傳入的參數,來獲取匹配到的MethodHandle,這部分放到下一節的標量函數調用中進行講解。

可以看出,標量函數註冊的本質是保存函數的SignatureMethodHandle。開發者根據註解框架實現的標量函數,註冊時再根據註解解析出SignatureMethodHandle,封裝在ParametricScalar對象中。當然,開發者也可以自行繼承SqlScalarFunction,自己定義Signature和實現specialize方法。

標量函數調用

標量函數調用的入口為InterpretedFunctionInvoker類的public Object invoke(Signature function, ConnectorSession session, List<Object> arguments)方法,形參里的Signature是由語義分析時,根據詞法分析得到函數QualifiedName和語法分析得到的參數類型,調用FunctionRegistry中的public Signature resolveFunction(QualifiedName name, List<TypeSignatureProvider> parameterTypes)方法得到。所以,標量函數調用的關鍵是resolveFunction方法和invoke方法。

首先來看resolveFunction方法,該方法主要通過函數名和函數參數類型來確定Signature,流程如下:

虛線紅框中的三個匹配過程實際上是調用了同一個方法:Optional<Signature> matchFunction(Collection<SqlFunction> candidates, List<TypeSignatureProvider> parameters, boolean coercionAllowed),其中的coercionAllowed為是否將實參類型轉化為形參類型的標識。matchFunction方法等價於為Signature中的變數尋找賦值,不僅要滿足變數類型是對應的實際參數類型的超類,而且對應的實際參數還要滿足Signature中聲明的變數約束。將形參類型和實參進行綁定時,還會做一些約定性的檢查:

  • 一個類型不能既賦給類型變數(type parameter),又賦給字面變數(literal parameter,如varchar(x)中的x
  • 字面變數不允許跨類型使用

為了便於理解第二個規定,下面例舉幾個字面變數跨類型使用的例子:

  • x 出現在不同的基本類型中:char(x)和varchar(x)
  • x 出現在同一種基本類型的不同位置:decimal(x,y) 和 decimal(z,x)
  • p 與不同的字面量、類型或者字面變數組合使用:decimal(p,s1) and decimal(p,s2)

還有一個限制是,如果嘗試將實際參數類型decimal(1,0)賦給Signature中聲明的decimal(x,2),會失敗,但是使用decimal(3,1)可以賦值成功。因為根據decimal的定義,precision 必須大於 scale,即x必須大於2。

經過一系列的規則匹配和變數求解,最終會返回一個具體的函數函數簽名,簽名中的類型都是具體類型(即不含變數)。比如簡單 SQLselect isnull('a'),最終得到的Signatureisnull(varchar(1)):boolean,實參中的類型varchar(1)賦給了原先註冊的isnull<T>(T):boolean中的類型變數T

再來看invoke方法,該方法首先會根據傳入的Signature調用FunctionRegistry中的getScalarFunctionImplementation來獲取最終的MethodHandle,然後使用具體的參數值來進行實際方法的調用(方法中若需要ConnectorSession,也在此進行注入)。因為函數註冊維護的是QualifiedName->SqlFunction的映射關係,而調用getScalarFunctionImplementation時傳入的Signature並沒有記錄變數與實參的綁定關係,所以這裡需要再進行一次類型變數的求解,這一步的計算其實是可以避免的,因為在resolveFunction中其實已經拿到了變數綁定的關係,可以進行復用,所以340版本中已改為傳入帶綁定關係的FunctionBinding。函數註冊時說明了一個函數可能有多個實現方法,接下來就是根據形參和實參的綁定關係,調用SqlFunctionspecialize方法進行對應參數的 Java 類型的匹配,按照exactimplementation類型->specializedImplementation類型->genericImplement類型的順序進行匹配,一旦匹配成功則直接返回匹配到的實現方法,如果方法中需要傳入依賴變數,也在此步驟中根據綁定關係對MethodHandle進行參數值注入。因為對MethodHandle的反覆編譯會導致full GC(懷疑是觸發了 JVM Bug),所以 Presto 在FunctionRegistry中為三類函數分別做了個大小為1000,有效時長為1小時的快取來避免這個問題。

至此,函數的註冊和調用的過程已經完成。熟悉這兩個過程可以幫助我們在函數開發和調用中快速地定位問題,除此之外,求解Signature時的類型轉換匹配可以作為類型隱式轉換的一個入口。

Tags: