Presto 標量函數註冊和調用過程簡述
在Presto 函數開發一文中已經介紹過如何進行函數開發,本文主要講述標量函數(Scalar Function)實現之後,是如何在Presto內部進行註冊和被調用的。主要講述標量函數是因為:三類函數的註冊和調用過程略有不同,而實際查詢中調用最多的是標量函數。
標量函數註冊
函數在能夠調用之前,首先要進行註冊,上一篇文章已經介紹過函數註冊的方法,那麼函數在註冊時究竟註冊了哪些資訊呢?函數註冊實際上是維護FunctinoRegistry類中的一個 MultiMap,Key 為函數的限定名(QualifiedName,可以簡單地理解為函數名),Value 為SqlFunction
介面的實現類,實際主要為SqlAggregationFunction
、SqlWindowFunction
和SqlScalarFunction
這三個類的子類。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
滿足comparable
;array_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
對應的MethodHandle
(MethodHandle
在JDK1.7引入,調用的效率比反射高),如果該方法不是靜態方法,還要將MethodHandle
的中的this
參數改為Object
來避免調用時的類載入問題。所以,抽象方法specialize
的本質是通過傳入的參數,來獲取匹配到的MethodHandle
,這部分放到下一節的標量函數調用中進行講解。
可以看出,標量函數註冊的本質是保存函數的Signature
和MethodHandle
。開發者根據註解框架實現的標量函數,註冊時再根據註解解析出Signature
和MethodHandle
,封裝在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')
,最終得到的Signature
是isnull(varchar(1)):boolean
,實參中的類型varchar(1)
賦給了原先註冊的isnull<T>(T):boolean
中的類型變數T
。
再來看invoke
方法,該方法首先會根據傳入的Signature
調用FunctionRegistry
中的getScalarFunctionImplementation
來獲取最終的MethodHandle
,然後使用具體的參數值來進行實際方法的調用(方法中若需要ConnectorSession
,也在此進行注入)。因為函數註冊維護的是QualifiedName->SqlFunction
的映射關係,而調用getScalarFunctionImplementation
時傳入的Signature
並沒有記錄變數與實參的綁定關係,所以這裡需要再進行一次類型變數的求解,這一步的計算其實是可以避免的,因為在resolveFunction
中其實已經拿到了變數綁定的關係,可以進行復用,所以340版本中已改為傳入帶綁定關係的FunctionBinding
。函數註冊時說明了一個函數可能有多個實現方法,接下來就是根據形參和實參的綁定關係,調用SqlFunction
的specialize
方法進行對應參數的 Java 類型的匹配,按照exactimplementation
類型->specializedImplementation
類型->genericImplement
類型的順序進行匹配,一旦匹配成功則直接返回匹配到的實現方法,如果方法中需要傳入依賴變數,也在此步驟中根據綁定關係對MethodHandle
進行參數值注入。因為對MethodHandle
的反覆編譯會導致full GC(懷疑是觸發了 JVM Bug),所以 Presto 在FunctionRegistry
中為三類函數分別做了個大小為1000,有效時長為1小時的快取來避免這個問題。
至此,函數的註冊和調用的過程已經完成。熟悉這兩個過程可以幫助我們在函數開發和調用中快速地定位問題,除此之外,求解Signature
時的類型轉換匹配可以作為類型隱式轉換的一個入口。