全部產品
Search
文件中心

MaxCompute:UDT概述

更新時間:Jul 11, 2024

MaxCompute基於新一代的SQL引擎推出新功能UDT(User Defined Type)。MaxCompute的UDT功能允許您在SQL中直接調用第三方語言的類使用其方法,或直接使用第三方對象擷取其資料內容。

UDT介紹

很多SQL引擎中UDT與MaxCompute的複雜類型STRUCT類似,相比之下,MaxCompute中的UDT與Create Type的概念更類似,Type中包含資料域和方法。MaxCompute不需要用特殊的DDL文法來定義新類型,通過UDT可以在SQL中直接使用新類型。通過如下樣本,為您直觀地介紹UDT的功能。

例如,在SQL語句中調用Java的java.lang包。您可以使用以下兩種方法:

  • 通過UDT功能在SQL語句中直接調用java.lang

    --開啟新類型,因為下面的操作會用到INTEGER,即INT類型。
    set odps.sql.type.system.odps2=true;    
    SELECT java.lang.Integer.MAX_VALUE;

    和Java語言一樣,java.lang包可以省略,所以上述樣本可以簡寫為如下語句。

    set odps.sql.type.system.odps2=true;
    SELECT Integer.MAX_VALUE;

    輸出結果如下。

    +-----------+
    | max_value |
    +-----------+
    | 2147483647 |
    +-----------+
  • 使用UDF在SQL語句中調用java.lang

    1. 代碼開發(定義一個UDF的類。)

      package com.aliyun.odps.test;
      public class IntegerMaxValue extends com.aliyun.odps.udf.UDF {
        public Integer evaluate() {
           return Integer.MAX_VALUE;
        } 
      }
    2. 進行打包、上傳及註冊操作(將上面的UDF編譯,並打成JAR包,然後上傳JAR包,並建立Function。)

      add jar odps-test.jar;
      create function integer_max_value as 'com.aliyun.odps.test.IntegerMaxValue' using 'odps-test.jar';
    3. 在SQL中調用UDF。

      select integer_max_value();

    由上例可以看出,UDT簡化了上述一系列的過程,方便您使用其它語言擴充SQL的功能。

應用情境

UDT的常用情境如下:

  • MaxCompute沒有提供,但可以使用其它語言簡單實現的功能。

    例如,只需調用一次Java內建類的方法即可實現,但MaxCompute卻沒有提供簡單的方法實現這個功能。如果使用UDF實現,整個過程會過於繁雜。

  • SQL中需要調用第三方庫實現相關功能。希望能夠在SQL中直接調用,而不需要再Wrap一層UDF。

  • SQL中需要直接調用第三方語言原始碼。Select Transform支援把指令碼寫到SQL語句中,提升可讀性和代碼易維護性。但是某些語言無法這樣使用,例如Java原始碼必須經過編譯才能執行,通過UDT功能將這些語言也可以直接寫入SQL中。

使用限制

目前版本不支援使用UDF/UDAF/UDT讀取以下情境的表資料:

  • 做過表結構修改(Schema Evolution)的表資料。

  • 包含複雜資料類型的表資料。

  • 包含JSON資料類型的表資料。

  • Transactional表的表資料。

實現原理

通過以下樣本為您介紹UDT的執行過程。

--樣本資料。
@table1 := select * from values ('100000000000000000000') as t(x);
@table2 := select * from values (100L) as t(y);
--代碼邏輯。
--new建立對象。
@a := select new java.math.BigInteger(x) x from @table1; 
--靜態方法調用。         
@b := select java.math.BigInteger.valueOf(y) y from @table2;  
--執行個體方法調用。    
select /*+mapjoin(b)*/ x.add(y).toString() from @a a join @b b;  

--輸出結果如下所示。
100000000000000000100

整個樣本的運行過程如下圖所示。

該UDT共有三個Stage:M1、R2和J3。如果您熟悉MapReduce原理即可知道,由於Join的存在需要做資料Reshuffle,所以會出現多個Stage。通常,不同的Stage是在不同的進程、不同的物理機器上啟動並執行。

M1隻執行new java.math.BigInteger(x)操作。

J3在不同階段執行了java.math.BigInteger.valueOf(y)x.add(y).toString()操作。這幾個操作不僅分階段執行,而且在不同的進程、不同的物理機器上執行。UDT把這個過程封裝起來,將這個過程變得看起來和在同一個JVM中執行的效果幾乎一樣。

從上述樣本中,您可以看到子查詢的結果允許UDT類型的列。例如上面變數a的x列是java.math.BigInteger類型,而不是內建類型。UDT類型的資料可以被帶到下一個Operator中,再調用其它方法,甚至可以參與資料Shuffle。

功能說明

  • UDT僅支援Java語言,Java SDK的類都是預設可用的。

    說明

    Runtime使用的JDK版本是JDK1.8,可能不支援更新版本的JDK。

  • UDT支援您上傳自己的JAR包,並且直接引用。當前提供了一些Flag方便您的使用。

    • set odps.sql.session.resources指定引用的資源,可以指定多個,用逗號隔開,例如set odps.sql.session.resources=foo.sh,bar.txt;

      說明

      這個Flag和select transform中指定資源的Flag相同,所以這個Flag會同時影響兩個功能。例如UDT概述中UDF的JAR包,用於UDT使用。

      set odps.sql.type.system.odps2=true;
      set odps.sql.session.resources=odps-test.jar; 
      --指定要引用的JAR。這些JAR需要提前上傳至Project,並且需要是JAR類型的資源。
      select new com.aliyun.odps.test.IntegerMaxValue().evaluate();
    • odps.sql.session.java.imports指定預設的Java Package,可以指定多個,用逗號隔開。和Java的import語句類似,可以提供完整類路徑,例如java.math.BigInteger,也可以使用*。暫不支援static import

      UDT概述中UDF的JAR包,使用UDT功能還有如下寫法。

      set odps.sql.type.system.odps2=true;
      set odps.sql.session.resources=odps-test.jar;
      set odps.sql.session.java.imports=com.aliyun.odps.test.*;  
      -- 指定預設的Package。
      select new IntegerMaxValue().evaluate();
  • UDT支援資源(Resource)的訪問,您可以在SQL中通過com.aliyun.odps.udf.impl.UDTExecutionContext.get()靜態方法擷取ExecutionContext對象,從而訪問當前的ExecutionContext,進而訪問資源(例如檔案資源和表格資源)。

  • UDT支援的操作:

    • 執行個體化對象的new操作。

    • 執行個體化數組的new操作,包括使用初始化列表建立數組,例如new Integer[] { 1, 2, 3 }

    • 方法調用,包括靜態方法調用。

    • 域訪問,包括靜態域。

    說明
    • 僅支援公有方法和公有域的訪問。

    • UDT中的標識符是大小寫敏感的,包括Package、類名、方法名和網域名稱。

    • 暫不支援匿名類和Lambda運算式。

    • 暫不支援無傳回值的函數調用(因為UDT均出現在Expression中,沒有傳回值的函數調用無法嵌入到Expression中,這個問題在後續的版本中會有解決方案)。

  • UDT支援的資料類型:

    • UDT支援類型轉換,支援SQL的風格,例如cast(1 as java.lang.Object)。但不支援Java風格的類型轉換,例如(Object)1

    • UDT內建類型與特定Java類型有一一映射關係,詳情請參見Java UDF中的資料類型映射表,這個映射關係對UDT也有效。

      • 內建類型的資料能夠直接調用其映射到的Java類型的方法,例如'123'.length() , 1L.hashCode()

      • UDT類型能夠直接參与內建函數或者UDF的運算, 例如chr(Long.valueOf('100')),其中Long.valueOf返回的是java.lang.Long類型的資料,而內建函數Chr接受的資料類型是內建類型BIGINT。

      • Java的PRIMITIVE類型可以自動轉化為其BOXING類型,並應用前兩條規則。

      說明

      部分內建的新資料類型需要先設定set odps.sql.type.system.odps2=true;才可使用,否則會報錯。

    • UDT擴充了類型轉換規則:

      • UDT對象可以被隱式類型轉換為其基類對象。

      • UDT對象可以被強制類型轉換為其基類或子類對象。

      • 沒有繼承關係的兩個對象之間遵守原來的類型轉換規則,注意這時可能會導致內容的變化。例如java.lang.Long類型的資料是可以強制轉換為java.lang.Integer的,應用的是內建類型的BIGINT強制轉換為INT的過程,而這個過程會導致資料內容的變化,甚至可能導致精度的損失。

      說明

      目前除隱式類型轉換變成內建類型外,UDT對象不能儲存到硬碟,即不能將UDT對象INSERT到表中(實際上DDL不支援UDT,不能建立這樣的表)。內建類型支援BINARY,即支援自己實現序列化的過程,將byte[]的資料存放區到硬碟,下次讀出時再還原回來。因此需要您自己調用序列化還原序列化方法,將其轉換為BINARY資料類型再儲存到硬碟。

      屏顯的最終結果不可以是UDT類型。對於屏顯的情境,由於所有的Java類都有toString()方法,而java.lang.String類型是合法的。所以Debug時,可以用這種方法觀察UDT的內容。

      您也可以設定set odps.sql.udt.display.tostring=true;,MaxCompute會自動幫您把所有以UDT為最終輸出的列Wrap上java.util.Objects.toString(...),以方便調試。這個Flag只對屏顯語句生效,對INSERT語句不生效,所以它只在調試的過程中使用。

    • UDT支援比較完整的泛型。例如java.util.Arrays.asList(new java.math.BigInteger('1')),編譯器可以根據參數類型判斷出該方法的傳回值是java.util.List<java.math.BigInteger>類型。

      說明

      建構函式需要指定型別參數,否則需要使用java.lang.Object,這點和Java保持一致。

      new java.util.ArrayList(java.util.Arrays.asList('1', '2'))的結果是java.util.ArrayList<Object>類型,而new java.util.ArrayList<String>(java.util.Arrays.asList('1', '2'))的結果是java.util.ArrayList<String>類型。

  • 所有的運算子都是MaxCompute SQL的語義,不是UDT的語義。例如:

    • STRING的相加操作:String.valueOf(1) + String.valueOf(2)的結果是3(STRING隱式轉換為DOUBLE,並且DOUBLE相加) ,而不是12(Java中STRING相加是Concatenate的語義)。

    • =操作:SQL中的=不是賦值而是判斷相等。而對於Java對象來說,判斷相等應該用Equals方法,而非=操作。

  • UDT對同一對象的概念是模糊的,這是由資料的Reshuffle導致的。對象有可能會在不同進程、不同物理機器之間傳輸。在傳輸過程中,同一個對象可能分別引用了不同的對象(例如對象先被Shuffle到兩台機器,然後下次又Shuffle回一起)。 所以在使用UDT時,應該使用equals方法判斷相等,避免使用=判斷相等。

    某行某列的對象,其內部包含的各個資料對象的相關性是可以保證的。不同行或者不同列的對象的資料相關性是不保證的。

  • UDT不能用作Shuffle Key,包括JoinGroup ByDistribute BySort ByOrder ByCluster By等結構的Key。

    UDT可以在Expression中間的任意階段使用,但不能作為最終輸出。例如,不可以使用語句group by new java.math.BigInteger('123'),但可以使用語句group by new java.math.BigInteger('123').hashCode()。因為hashCode方法的傳回值是int.class類型,可以當做內建類型INT來使用(應用上述內建類型與特定Java類型規則)。

  • UDT不僅可以實現Scalar函數的功能,配合內建函數COLLECT_SET其他函數,UDT還可以實現Aggregator和Table Function功能。

功能優勢

UDT的功能優勢:

  • 使用簡單,無需定義任何函數。

  • 支援JDK的所有功能,擴充了SQL的能力。

  • 代碼可與SQL放於同一檔案,便於管理。

  • 可直接使用其它類庫,代碼重用率高。

  • 可以使用物件導向的思想設計某些功能。

後續待完善功能:

  • 支援無傳回值的函數調用,或支援(有傳回值但忽略傳回值)直接取運算元本身的函數調用。例如,調用List的add方法會返回執行完add操作的List。

  • 支援匿名類和Lambda運算式。

  • 支援用作Shuffle Key。

  • 支援Java外的其他語言,例如Python。

效能

因為UDT和UDF的執行過程非常接近,所以UDT與UDF的效能幾乎一致。最佳化後的計算引擎使得UDT在特定情境下的效能更高。

  • UDT對象只有在跨進程時才需要做序列化和還原序列化,因此在執行不需要資料Reshuffle的操作(如JOINAGGREGATE)時,UDT可節省序列化和還原序列化的開銷。

  • 因為UDT的Runtime基於Codegen而非反射實現的,所以不存在反射帶來的效能損失。在您使用過程中,連續多個UDT操作會合并在一個FunctionCall裡一起執行。例如在之前的樣本中,values[x].add(values[y]).divide(java.math.BigInteger.valueOf(2))實際上只會調用一次UDT。所以,UDT操作的單元雖然較小,卻並不會因多次函數調用而造成額外的介面開銷。

安全性

在安全控制方面,UDT和UDF完全一樣,都會受到Java沙箱Policy的限制。因此如果要使用受限的操作,需要開啟沙箱隔離,或者申請沙箱白名單。