MaxCompute(原ODPS)是阿裡雲自主研發的具有業界領先水準的分布式大資料處理平台, 尤其在集團内部得到廣泛應用,支撐了多個BU的核心業務。 MaxCompute除了持續優化性能外,也緻力于提升SQL語言的使用者體驗和表達能力,提高廣大ODPS開發者的生産力。
MaxCompute基于ODPS2.0新一代的SQL引擎,顯著提升了SQL語言編譯過程的易用性與語言的表達能力。我們在此推出MaxCompute(ODPS2.0)重裝上陣系列文章
- 第一彈 - 善用MaxCompute編譯器的錯誤和警告
- 第二彈 - 新的基本資料類型與内建函數
- 第三彈 - 複雜類型
- 第四彈 - CTE,VALUES,SEMIJOIN
- 第五彈 - SELECT TRANSFORM
- 第六彈 - User Defined Type
- 第七彈 - Grouping Set, Cube and Rollup
- 第八彈 - 動态類型函數
MaxCompute自定義函數的參數和傳回值不夠靈活,是資料開發過程中時常被提及的問題。Hive 提供給了 GenericUDF 的方式,通過調用一段使用者代碼,讓使用者來根據參數類型決定傳回值類型。MaxCompute 出于性能、安全性等考慮,沒有支援這種方式。但是MaxCompute也提供了許多方式,讓您能夠靈活地自定義函數。
-
場景1
需要實作一個UDF,可以接受任意類型的輸入,但是MaxCompute的UDF不支援泛型,要做一個接受任何類型的函數,就必須為每種類型都寫一個evaluate函數。
-
場景2
MaxCompute的UDAF和UDTF使用@Resolve的注解來指定輸入輸出類型,無法重載。要做一個接受多種類型的自定義功能,就需要定義多個不同的函數。
-
場景3
MaxCompute支援了參數化視圖,能夠把一些公共的SQL提出來。參數化視圖的表值參數要求輸入表的列數和類型與視圖定義時完全一緻,如果想要寫一個能夠接受具有相似特征的不同的表的視圖,還無法定義出來。
本文帶大家一起看看MaxCompute對這些大家關心的問題都做了哪些改進。
參數化視圖
問題
是MaxCompute自己設計的一種視圖。允許使用者定義參數,進而能夠大大視圖代碼的複用率。很多使用者都利用這一功能,将一些公共SQL提取到視圖中,形成公共SQL代碼池。
參數化視圖在聲明過程中具有局限性:參數類型,長度都是固定的。尤其是參數化視圖允許傳入表值參數,表值參數要求形參與實參在列的個數和類型上都一緻。這一點限制了許多使用場景,如下面的例子:
CREATE VIEW paramed_view (@a TABLE(key bigint)) AS SELECT @a.* FROM @a JOIN dim on a.key = dim.key;
這個例子封裝了一段使用dim表來過濾輸入表的邏輯,本來這個是個通用的邏輯,任何包含key這一列的表,都可以用來做輸入表。但是由于定義視圖時隻能确定輸入中包含key列,是以聲明的參數類型隻包含這一列。導緻了視圖的調用者傳遞的表參數必須隻能有一列,而傳回的資料集也隻包含一列,這顯然與這個視圖的設計初衷不合。
改進
最新的MaxCompute版本對參數化視圖做了一些改進,可以大大提升參數化視圖定義的靈活性。
首先,參數化視圖的參數可以使用ANY關鍵字,表示任意類型。如
CREATE VIEW paramed_view (@a ANY) AS SELECT * FROM src WHERE case when @a is null then key1 else key2 end = key3;
這裡定義的視圖,第一個參數可以接受任意類型。注意ANY類型不能參與如
'+'
,
'AND'
之類的需要明确類型才能做的運算。ANY類型更多是在TABLE參數中做passthrough列,如
CREATE VIEW paramed_view (@a TABLE(name STRING, id ANY, age BIGINT)) AS SELECT * FROM @a WHRER name = 'foo' and age < 25;
-- 調用示例
SELECT * FROM param_view((SELECT name, id, age from students));
上面的視圖接受一個表值參數,但是并不關心這個表的第二列,那麼這個列可以直接定義為ANY類型。參數化視圖在調用時,每次都會根據輸入參數的實際類型重新推算傳回值類型。比如上面的視圖,當輸入的表是
TABLE(c1 STRING, c2 DOUBLE, c3 BIGINT)
,那麼輸出的資料集的第二列也會自動變成DOUBLE類型,讓視圖的調用者可以使用任何可用于DOUBLE類型的操作來操作這一列。
需要注意的一點是,我們用CREATE VIEW建立了視圖後,可以用DESC來擷取視圖的描述,這個描述中會包含視圖的傳回類型資訊。但是由于視圖的傳回類型是在調用的時候重新推算的,重新推算出來的類型可能與建立視圖時推導出來的不一緻。一個例子就是上面的ANY類型。
在ANY之外,參數化視圖中的表值參數還支援了
*
,表示任意多個列。這個
*
可以帶類型,也可以使用ANY類型。如
CREATE VIEW paramed_view (@a TABLE(key STRING, * ANY), @b TABLE(key STRING, * STRING)) AS SELECT a.* FROM @a JOIN @b ON a.key = b.key;
-- 調用示例
SELECT name, address FROM param_view((SELECT school, name, age, address FROM student), school) WHERE age < 20;
上面這個視圖接受兩個表值參數,第一個表值參數第一列是string類型,後面可以是任意多個任意類型的列,而第二個表值參數的第一列是string,後面可以是任意多個STRING類型的列。這其中有幾點需要注意:
- 變長部分必須要寫在表值參數定義的最後面,即在
的後面不允許再有其他列。這也間接導緻了一個表值參數中最多隻有一個變長列清單。*
- 由于變長部分必須在最後,有的時候輸入表的列不一定是按照這種順序排列的,這時候需要對輸入表的列做一定重排,可以以subquery作為參數(參考上面的例子),注意subquery外面要加一層括号。
- 由于表值參數中變長部分沒有名字,是以在視圖定義過程中沒辦法獲得對這部分資料的引用,也就沒有辦法對這些資料做運算。這個限制是特意設定的,如果需要對變長部分的資料做運算,需要把要運算的列聲明在定長部分,而編譯器會對調用時傳入的參數進行檢查。
- 雖然不能對變長部分做運算,但是
這種通配符的使用依舊可以将變長部分的列傳遞出去,如上面的例子在paramed_view中将SELECT *
的所有列傳回,雖然建立視圖的時候,a中隻有key這一列,但是調用視圖的時候,編譯器推算出@a
中還包含了name, age, address,是以視圖傳回的資料集中也包含這三列,而視圖的調用者也可以對着三列進行操作(如@a
)。WHERE age < 20
- 表值參數的列與視圖聲明時指定的定長列部分不一定完全一緻。如果名字不一樣,編譯器會自動做重命名,如果類型不一樣,編譯器會做隐式轉換(不能隐式轉換則會報錯)。
上面提到的第4點非常有用,一方面保證了調用視圖是輸入參數的靈活性,另一方面又不降低資料的資訊量。好好利用能夠很大程度上增加公共代碼的複用率。
下面是一個調用示例。該例子使用的視圖是:
CREATE VIEW paramed_view (@a TABLE(key STRING, * ANY), @b TABLE(key STRING, * STRING)) AS SELECT a.* FROM @a JOIN @b ON a.key = b.key;
在MaxCompute Studio中調用,可以享受文法高亮和錯誤提示等功能。執行的調用代碼如下:

執行的狀态圖如下:
放大執行過程仔細觀察,圖中可以發現幾點有意思的地方:
上述執行輸出的結果如下:
+------+---------+
| name | address |
+------+---------+
| 小明 | 杭州 |
+------+---------+
其他用法
經常有使用者誤用參數化視圖,将參數化視圖的參數當做是宏替換參數來使用。這裡說明一下。參數化視圖實際上是函數調用,而不是宏替換。如下面的例子:
CREATE VIEW paramed_view(@a TABLE(key STRING, value STRING), @b STRING)
AS SELECT * FROM @a ORDER BY @b;
-- 調用示例
select * from paramed_view(src, 'key');
上面的例子中,使用者的期望是
ORDER BY @b
被宏替換為
ORDER BY key
,即根據輸入參數,決定了按照key列做排序。然而,實際上參數@b是作為一個值來傳遞的,
ORDER BY @b
相當于
ORDER BY 'key'
,即 ORDER BY一個字元串常量('key')而不是一列。要想實作"讓調用者決定排序列"這一功能,可以考慮下述做法。
CREATE VIEW orderByFirstCol(@a TABLE(columnForOrder ANY, * ANY)) AS SELECT `(columnForOrder)?+.+` FROM (SELECT * FROM @a ORDER BY columnForOrder) t;
-- 調用示例
select * from orderByFirstCol((select key, * from src));
上面的例子,要求調用者将要排序的列放在第一列,于是在調用的時候使用子查詢将src的需要排序的列抽取到最前面。視圖傳回的
(columnForOrder)?+.+
是一個正則通配符,比對columnForOrder之外的所有列,清單達式使用正規表達式可參考
SELECT文法介紹>清單達式關于正規表達式的說明。
UDF:函數重載方式
MaxCompute 的 UDF 使用重載 evalaute 方法的方式來重載函數,如下面的UDF定義了兩個重載,當輸入是 String 類型時,輸出String類型,輸入是BIGINT類型時,輸出DOUBLE類型。
public UDFClass extends UDF {
public String evaluate(String input) { return input + "123"; }
public Double evaluate(Long input) { return input + 123.0; }
}
這種方式固然能解決一些問題,但有一定的局限性。比如不支援泛型,要做一個接受任何類型的函數,就必須為每種類型都寫一個evaluate函數。有的時候重載甚至是不能實作的,比如ARRAY 和 ARRAY 的重載是做不到的。
public UDFClass extends UDF {
public String evaluate(List<Long> input) { return input.size(); }
// 這裡會報錯,因為在java類型擦除後,這個函數和 String evaluate(List<Long> input) 的參數是一樣的
public Double evaluate(List<Double> input) { input.size(); }
// UDF 不支援下面這種定義方式
public String evaluate(List<Object> input) { return input.size(); }
}
PYTHON UDF 或 UDTF 在不提供 Resolve 注解(annotation)的時候,會根據參數個數決定輸入參數,也支援變長,是以非常靈活。但也因為過于靈活,編譯器無法靜态找到某些錯誤。比如
class Substr(object):
def evaluate(self, a, b):
return a[b:];
上面的函數接受兩個參數,從實作上看,第一個參數需要是STRING類型,第二個參數應該是整形。而這個限制需要使用者在調用時自己去把握。即使使用者傳錯了參數,編譯器也沒有辦法報錯。同時,這種方式定義的UDF傳回值類型隻能是STRING,不夠靈活。
要解決上面的問題。可以考慮使用
UDT。 UDT經常被簡單在調用JDK中的方法的時候使用,比如
java.util.Objects.toString(x)
将任何對象 x 轉成STRING類型。但是在自定義函數方面同樣也有很好的用途。 UDT支援泛型,支援類繼承,支援變長等功能,讓定義函數更友善。如下面的例子:
public class UDTClass {
// 這個函數接受一個數值類型(可以是 TINYINT, SMALLINT, INT, BIGINT, FLOAT, DOUBLE 以及任何以Number為基類的UDT),傳回DOUBLE
public static Double doubleValue(Number input) {
return input.doubleValue();
}
// 這個方法,接受一個數值類型參數和一個任意類型的參數,傳回值類型與第二個參數的類型相同
public static <T extends Number, R> R nullOrValue(T a, R b) {
return a.doubleValue() > 0 ? b : null;
}
// 這個方法接受一個任意元素類型的array或List,傳回BIGINT
public static Long length(java.util.List<? extends Object> input) {
return input.size();
}
// 注意這個在不做強制轉換的情況下參數隻能接受 UDT 的 java.util.Map<Object, Object> 對象。如果需要傳入任何map對象,比如 map<bigint,bigint> 可以考慮:
// 1. 定義函數時使用java.util.Map<? extends Object, ? extends Object>
// 2. 調用時強轉,比如 UDTClass.mapSize(cast(mapObj as java.util.Map<Object, Object>))
public static Long mapSize(java.util.Map<Object, Object> input) {
return input.size();
}
}
UDT 能夠提供靈活的函數定義方式。但是有的時候UDF 需要通過
com.aliyun.odps.udf.ExecutionContext
(在setup方法中傳入)來擷取一些上下文。現在UDT也可以通過
com.aliyun.odps.udt.UDTExecutionContext.get()
方法來或者這樣的一個 ExecutionContext 對象。
Aggregator 與 UDTF:Annotation方式
MaxCompute 的 UDAF 和 UDTF 使用Resolve注解來決定函數Signature。比如下面的方式定義了一個UDTF,該UDTF接受一個BIGINT參數,傳回DOUBLE類型。
@com.aliyun.odps.udf.annotation.Resolve("BIGINT->DOUBLE")
public class UDTFClass extends UDTF {
...
}
這種方式的局限性很明顯,輸入參數和輸出參數都是固定的,沒辦法重載。
MaxCompute對Resolve注解的文法做了許多擴充,現在能夠支援一定的靈活性。
- 參數清單中可以使用星号
,表示接受任意長度的,任意類型的輸入參數。比如('*')
,接受第一個是double,後接任意類型,任意個數的參數清單。這裡需要UDF的作者在代碼裡面自己去判斷輸入的個數和類型,然後做出相應的動作(可以對比 C 語言裡面的 printf 函數來了解)。注意星号用在傳回值清單中時,表示的是不同的含義,在後續第三點中說明。@Resolve('double,*->String')
- 參數清單中可以使用 ANY 關鍵字,表示任意類型的參數。比如
,接受第一個是double,第二個任意類型的參數清單。注意,ANY在傳回值清單中不能使用,也不能在複雜類型的子類型中使用(如不能寫ARRAY)。@Resolve('double,any->string')
- UDTF的傳回值可以使用星号,表示傳回任意多個string類型。這裡需要注意,傳回值的個數并非真的是任意多個,而是與調用函數時給出的alias個數有關。比如
,調用方式是@Resolve("ANY,ANY->DOUBLE,*")
,這裡as後面給出了三個alias (a, b, c),編譯器會認定a為double類型(annotation中傳回值第一列的類型是給定的),b,c為string類型,而因為這裡給出了三個傳回值,是以UDTF在forward的時候,也一定要forward長度為3的數組,否則會出現運作時錯誤。注意這個錯誤是無法在編譯時給出的,是以通常需要UDTF的作者與調用者互相溝通好,調用者在SQL中給出alias個數的時候,一定要按照UDTF的需要來寫。由于Aggregator傳回值個數固定是1,是以這個功能對UDAF無意義。UDTF(x, y) as (a, b, c)
用一個例子來說明。如下UDTF:
import com.aliyun.odps.udf.UDFException;
import com.aliyun.odps.udf.UDTF;
import com.aliyun.odps.udf.annotation.Resolve;
import org.json.JSONException;
import org.json.JSONObject;
@Resolve("STRING,*->STRING,*")
public class JsonTuple extends UDTF {
private Object[] result = null;
@Override
public void process(Object[] input) throws UDFException {
if (result == null) {
result = new Object[input.length];
}
try {
JSONObject obj = new JSONObject((String)input[0]);
for (int i = 1; i < input.length; i++) {
// 傳回值要求變長部分都是STRING
result[i] = String.valueOf(obj.get((String)(input[i])));
}
result[0] = null;
} catch (JSONException ex) {
for (int i = 1; i < result.length; i++) {
result[i] = null;
}
result[0] = ex.getMessage();
}
forward(result);
}
}
這個UDTF的傳回值個數會根據輸入參數的個數來決定。輸出參數的第一個是一個JSON文本,後面是需要從JSON中解析的key。傳回值第一個是解析JSON過程中的出錯資訊,如果沒有出錯,則後續根據輸入的key依次輸出從json中解析出來的内容。使用示例如下。
-- 根據輸入參數的個數定制輸出alias個數
SELECT my_json_tuple(json, ’a‘, 'b') as exceptions, a, b FROM jsons;
-- 變長部分可以一列都沒有
SELECT my_json_tuple(json) as exceptions, a, b FROM jsons;
-- 下面這個SQL會出現運作時錯誤,因為alias個數與實際輸出個數不符
-- 注意編譯時無法發現這個錯誤
SELECT my_json_tuple(json, 'a', 'b') as exceptions, a, b, c FROM jsons;
上面雖然做出了許多擴充,但是這些擴充并不一定能滿足所有的需求。這時候依然可以考慮使用UDT。UDT也是可以用來實作Aggregator和UDTF的功能的。詳細可以參考
UDT示例文檔,“聚合操作的實作示例” 及 “表值函數的實作示例” 的内容。
總結
MaxCompute自定義函數的函數原型不夠靈活,在資料開發過程中帶來諸多不便利,本文列舉了各種函數定義方式存在的問題與解決方案,希望對大家有幫助,同時也告訴大家MaxCompute一直在努力為大家提供更好的服務。