MaxCompute(原ODPS)是阿里雲自主研發的具備業界領先水平的分佈式大數據處理平臺, 尤爲在集團內部獲得普遍應用,支撐了多個BU的核心業務。 MaxCompute除了持續優化性能外,也致力於提高SQL語言的用戶體驗和表達能力,提升廣大ODPS開發者的生產力。html
MaxCompute基於ODPS2.0新一代的SQL引擎,顯著提高了SQL語言編譯過程的易用性與語言的表達能力。咱們在此推出MaxCompute(ODPS2.0)重裝上陣系列文章java
MaxCompute自定義函數的參數和返回值不夠靈活,是數據開發過程當中時常被說起的問題。Hive 提供給了 GenericUDF 的方式,經過調用一段用戶代碼,讓用戶來根據參數類型決定返回值類型。MaxCompute 出於性能、安全性等考慮,沒有支持這種方式。可是MaxCompute也提供了許多方式,讓您可以靈活地自定義函數。正則表達式
本文帶你們一塊兒看看MaxCompute對這些你們關心的問題都作了哪些改進。json
參數化視圖數組
問題安全
參數化視圖是MaxCompute本身設計的一種視圖。容許用戶定義參數,從而可以大大視圖代碼的複用率。不少用戶都利用這一功能,將一些公共SQL提取到視圖中,造成公共SQL代碼池。分佈式
參數化視圖在聲明過程當中具備侷限性:參數類型,長度都是固定的。尤爲是參數化視圖容許傳入表值參數,表值參數要求形參與實參在列的個數和類型上都一致。這一點限制了許多使用場景,以下面的例子:ide
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類型的列。這其中有幾點須要注意:
上面提到的第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註解的語法作了許多擴展,如今可以支持必定的靈活性。
用一個例子來講明。以下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一直在努力爲你們提供更好的服務。
上雲就看雲棲號:更多雲資訊,上雲案例,最佳實踐,產品入門,訪問:https://yqh.aliyun.com/
本文爲阿里雲原創內容,未經容許不得轉載。