flink(十五):udf自定義函數

說明

本博客每週五更新一次。 自定義函數(UDF)是一種Flink 擴展開發機制,可在查詢語句裏實現自定義的功能邏輯。 自定義函數可用 JVM 語言(例如 Java 或 Scala)或 Python 實現,推薦java或scala。

分享

資料

種類

  • UDF按功能大致分爲4類(也可以3類,聚合函數和表值聚合函數算一類),如下表
名稱 說明
標量函數 把0到多個標量值映射成 1 個標量值
表值函數 把0到多個標量值映射成多行數據
聚合函數 把一行或多行數據聚合爲1個值
表值聚合函數 把一行或多行數據聚合爲多行

標量函數

說明

  • 標量函數必須繼承 org.apache.flink.table.functions.ScalarFunction 類,實現 eval 方法,java實例代碼如下:

實例

//-------------- 實現標量函數 ----------------
import org.apache.flink.table.annotation.InputGroup;
import org.apache.flink.table.api.*;
import org.apache.flink.table.functions.ScalarFunction;
import static org.apache.flink.table.api.Expressions.*;

public static class HashFunction extends ScalarFunction {

  // 接受任意類型輸入,返回 INT 型輸出
  public int eval(@DataTypeHint(inputGroup = InputGroup.ANY) Object o) {
    return o.hashCode();
  }
}


// 調用自定義函數
TableEnvironment env = TableEnvironment.create(...);

//-------------- 方式1 不註冊函數 ----------------
// 在 Table API 裏不經註冊直接“內聯”調用函數
env.from("MyTable").select(call(HashFunction.class, $("myField")));

//-------------- 方式2 註冊函數 ----------------
// 註冊函數
env.createTemporarySystemFunction("HashFunction", HashFunction.class);

// 在 Table API 裏調用註冊好的函數
env.from("MyTable").select(call("HashFunction", $("myField")));

// 在 SQL 裏調用註冊好的函數
env.sqlQuery("SELECT HashFunction(myField) FROM MyTable");

表值函數

說明

  • 實現類 org.apache.flink.table.functions.TableFunction,通過實現多個名爲 eval 的方法對求值方法進行重載。

實例

//-------------- 實現表值函數 ----------------
import org.apache.flink.table.annotation.DataTypeHint;
import org.apache.flink.table.annotation.FunctionHint;
import org.apache.flink.table.api.*;
import org.apache.flink.table.functions.TableFunction;
import org.apache.flink.types.Row;
import static org.apache.flink.table.api.Expressions.*;

@FunctionHint(output = @DataTypeHint("ROW<word STRING, length INT>"))
public static class SplitFunction extends TableFunction<Row> {

  public void eval(String str) {
    for (String s : str.split(" ")) {
      // use collect(...) to emit a row
      collect(Row.of(s, s.length()));
    }
  }
}

//-------------- 使用標量函數 ----------------
TableEnvironment env = TableEnvironment.create(...);

//-------------- 方式1:不註冊使用 ----------------
// 在 Table API 裏不經註冊直接“內聯”調用函數
env
  .from("MyTable")
  .joinLateral(call(SplitFunction.class, $("myField")))
  .select($("myField"), $("word"), $("length"));
env
  .from("MyTable")
  .leftOuterJoinLateral(call(SplitFunction.class, $("myField")))
  .select($("myField"), $("word"), $("length"));

// 在 Table API 裏重命名函數字段
env
  .from("MyTable")
  .leftOuterJoinLateral(call(SplitFunction.class, $("myField")).as("newWord", "newLength"))
  .select($("myField"), $("newWord"), $("newLength"));

//-------------- 方式1:註冊使用 ----------------
// 註冊函數
env.createTemporarySystemFunction("SplitFunction", SplitFunction.class);

// 在 Table API 裏調用註冊好的函數
env
  .from("MyTable")
  .joinLateral(call("SplitFunction", $("myField")))
  .select($("myField"), $("word"), $("length"));
env
  .from("MyTable")
  .leftOuterJoinLateral(call("SplitFunction", $("myField")))
  .select($("myField"), $("word"), $("length"));

// 在 SQL 裏調用註冊好的函數
env.sqlQuery(
  "SELECT myField, word, length " +
  "FROM MyTable, LATERAL TABLE(SplitFunction(myField))");
env.sqlQuery(
  "SELECT myField, word, length " +
  "FROM MyTable " +
  "LEFT JOIN LATERAL TABLE(SplitFunction(myField)) ON TRUE");

// 在 SQL 裏重命名函數字段
env.sqlQuery(
  "SELECT myField, newWord, newLength " +
  "FROM MyTable " +
  "LEFT JOIN LATERAL TABLE(SplitFunction(myField)) AS T(newWord, newLength) ON TRUE");

聚合函數

說明

  • 自定義聚合函數(UDAGG)是把一個表(一行或者多行,每行可以有一列或者多列)聚合成一個標量值。

  • 如上圖,有一個關於飲料的表,有三個字段id、name、price,有 5 行數據。假設需要找到所有飲料裏最貴的飲料價格,即執行一個 max() 聚合。需要遍歷所有5行數據,結果只有一個數值。

  • 自定義聚合函數是通過擴展 AggregateFunction 來實現的。AggregateFunction 需要 accumulator 定義數據結構,存儲了聚合的中間結果。通過 AggregateFunction 的 createAccumulator() 方法創建一個空的 accumulator。對於每一行數據,會調用 accumulate() 方法來更新 accumulator。當所有的數據都處理完了之後,通過調用 getValue() 計算和返回最終結果。

  • 因此實現AggregateFunction 必須實現方法:createAccumulator()accumulate()getValue()

  • 某些場景下還需要實現其他方法。

    • retract() 在 bounded OVER 窗口中是必須實現的。
    • merge() 在許多批式聚合和會話以及滾動窗口聚合中是必須實現的。除此之外,這個方法對於優化也很多幫助。例如,兩階段聚合優化就需要所有的 AggregateFunction 都實現 merge 方法。
    • resetAccumulator() 在許多批式聚合中是必須實現的。

代碼實例

//----------------創建數據對象 ----------------
/**
 * Accumulator for WeightedAvg.
 */
public static class WeightedAvgAccum {
    public long sum = 0;
    public int count = 0;
}

//-------------- 定義聚合函數 ----------------

/**
 * Weighted Average user-defined aggregate function.
 */
public static class WeightedAvg extends AggregateFunction<Long, WeightedAvgAccum> {

    @Override
    public WeightedAvgAccum createAccumulator() {
        return new WeightedAvgAccum();
    }

    @Override
    public Long getValue(WeightedAvgAccum acc) {
        if (acc.count == 0) {
            return null;
        } else {
            return acc.sum / acc.count;
        }
    }

    public void accumulate(WeightedAvgAccum acc, long iValue, int iWeight) {
        acc.sum += iValue * iWeight;
        acc.count += iWeight;
    }

    public void retract(WeightedAvgAccum acc, long iValue, int iWeight) {
        acc.sum -= iValue * iWeight;
        acc.count -= iWeight;
    }

    public void merge(WeightedAvgAccum acc, Iterable<WeightedAvgAccum> it) {
        Iterator<WeightedAvgAccum> iter = it.iterator();
        while (iter.hasNext()) {
            WeightedAvgAccum a = iter.next();
            acc.count += a.count;
            acc.sum += a.sum;
        }
    }

    public void resetAccumulator(WeightedAvgAccum acc) {
        acc.count = 0;
        acc.sum = 0L;
    }
}

//-------------- 使用聚合函數 ----------------
// 註冊函數
StreamTableEnvironment tEnv = ...
tEnv.registerFunction("wAvg", new WeightedAvg());

// 使用函數
tEnv.sqlQuery("SELECT user, wAvg(points, level) AS avgPoints FROM userScores GROUP BY user");


表值聚合函數

說明

  • 自定義表值聚合函數(UDTAGG)可以把一個表(一行或者多行,每行有一列或者多列)聚合成另一張表,結果中可以有多行多列。

  • 如上圖有一個表,3個字段分別爲 id、name 和 price 共 5 行。假設需要找到價格最高的兩個飲料,類似於 top2() 表值聚合函數。需要遍歷所有 5 行數據,結果是有 2 行數據的一個表。

  • 自定義表值聚合函數通過擴展 TableAggregateFunction 類來實現的,具體執行過程如下。首先,需要一個 accumulator 負責存儲聚合的中間結果。 通過調用 TableAggregateFunction 的 createAccumulator() 方法來一個空的 accumulator。對於每一行數據,調用 accumulate() 方法更新 accumulator。當所有數據都處理完之後,調用 emitValue() 方法計算和返回最終的結果。

  • 實現TableAggregateFunction 必須要實現的方法:createAccumulator()accumulate()

  • 某些場景下必須實現的方法:

    • retract() 在 bounded OVER 窗口中的聚合函數必須要實現。
    • merge() 在許多批式聚合和以及流式會話和滑動窗口聚合中是必須要實現的。
    • resetAccumulator() 在許多批式聚合中是必須要實現的。
    • emitValue() 在批式聚合以及窗口聚合中是必須要實現的。
  • emitUpdateWithRetract() 在 retract 模式下,可以提升人物效率,該方法負責發送被更新的值。

代碼實例

  • 定義TableAggregateFunction 來計算給定列的最大的 2 個值,在 TableEnvironment 中註冊函數,在 Table API 查詢中使用函數(當前只在 Table API 中支持 TableAggregateFunction)。
//----------------創建數據對象 ----------------
/**
 * Accumulator for Top2.
 */
public class Top2Accum {
    public Integer first;
    public Integer second;
}

//-------------- 定義聚合函數 ----------------
/**
 * The top2 user-defined table aggregate function.
 */
public static class Top2 extends TableAggregateFunction<Tuple2<Integer, Integer>, Top2Accum> {

    @Override
    public Top2Accum createAccumulator() {
        Top2Accum acc = new Top2Accum();
        acc.first = Integer.MIN_VALUE;
        acc.second = Integer.MIN_VALUE;
        return acc;
    }


    public void accumulate(Top2Accum acc, Integer v) {
        if (v > acc.first) {
            acc.second = acc.first;
            acc.first = v;
        } else if (v > acc.second) {
            acc.second = v;
        }
    }

    public void merge(Top2Accum acc, java.lang.Iterable<Top2Accum> iterable) {
        for (Top2Accum otherAcc : iterable) {
            accumulate(acc, otherAcc.first);
            accumulate(acc, otherAcc.second);
        }
    }

    public void emitValue(Top2Accum acc, Collector<Tuple2<Integer, Integer>> out) {
        // emit the value and rank
        if (acc.first != Integer.MIN_VALUE) {
            out.collect(Tuple2.of(acc.first, 1));
        }
        if (acc.second != Integer.MIN_VALUE) {
            out.collect(Tuple2.of(acc.second, 2));
        }
    }
}

//-------------- 使用聚合函數 ----------------
// 註冊函數
StreamTableEnvironment tEnv = ...
tEnv.registerFunction("top2", new Top2());

// 初始化表
Table tab = ...;

// 使用函數
tab.groupBy("key")
    .flatAggregate("top2(a) as (v, rank)")
    .select("key, v, rank");
  • 下面例子使用 emitUpdateWithRetract 方法來只發送更新的數據。爲了只發送更新的結果,accumulator 保存上一次的最大2個值,也保存了當前最大2個值。
  • 注意:如果 TopN 中的 n 非常大,這種既保存上次的結果,也保存當前的結果的方式不太高效。一種解決這種問題的方式是把輸入數據直接存儲到 accumulator 中,然後在調用 emitUpdateWithRetract 方法時再進行計算。
//----------------創建數據對象 ----------------
/**
 * Accumulator for Top2.
 */
public class Top2Accum {
    public Integer first;
    public Integer second;
    public Integer oldFirst;
    public Integer oldSecond;
}

//-------------- 定義聚合函數 ----------------
/**
 * The top2 user-defined table aggregate function.
 */
public static class Top2 extends TableAggregateFunction<Tuple2<Integer, Integer>, Top2Accum> {

    @Override
    public Top2Accum createAccumulator() {
        Top2Accum acc = new Top2Accum();
        acc.first = Integer.MIN_VALUE;
        acc.second = Integer.MIN_VALUE;
        acc.oldFirst = Integer.MIN_VALUE;
        acc.oldSecond = Integer.MIN_VALUE;
        return acc;
    }

    public void accumulate(Top2Accum acc, Integer v) {
        if (v > acc.first) {
            acc.second = acc.first;
            acc.first = v;
        } else if (v > acc.second) {
            acc.second = v;
        }
    }

    public void emitUpdateWithRetract(Top2Accum acc, RetractableCollector<Tuple2<Integer, Integer>> out) {
        if (!acc.first.equals(acc.oldFirst)) {
            // if there is an update, retract old value then emit new value.
            if (acc.oldFirst != Integer.MIN_VALUE) {
                out.retract(Tuple2.of(acc.oldFirst, 1));
            }
            out.collect(Tuple2.of(acc.first, 1));
            acc.oldFirst = acc.first;
        }

        if (!acc.second.equals(acc.oldSecond)) {
            // if there is an update, retract old value then emit new value.
            if (acc.oldSecond != Integer.MIN_VALUE) {
                out.retract(Tuple2.of(acc.oldSecond, 2));
            }
            out.collect(Tuple2.of(acc.second, 2));
            acc.oldSecond = acc.second;
        }
    }
}

//-------------- 使用聚合函數 ----------------s
// 註冊函數
StreamTableEnvironment tEnv = ...
tEnv.registerFunction("top2", new Top2());

// 初始化表
Table tab = ...;

// 使用函數
tab.groupBy("key")
    .flatAggregate("top2(a) as (v, rank)")
    .select("key, v, rank");

總結

  • 個人感覺UDF本質是抽象類的實現,擴展了Flink計算能力。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章