1.19.9.函数、概览、函数引用、精确函数引用、模糊函数引用、函数解析顺序、精确函数引用、模糊函数引用、自定义函数、准备工作、概述、开发指南、函数类、求值方法、标量函数、表值函数、聚合函数

1.19.9.函数
1.19.9.1.概览
1.19.9.1.1.函数引用
1.19.9.1.2.精确函数引用
1.19.9.1.3.模糊函数引用
1.19.9.1.4.函数解析顺序
1.19.9.1.5.精确函数引用
1.19.9.1.6.模糊函数引用
1.19.9.2.自定义函数
1.19.9.2.1.准备工作
1.19.9.2.2.概述
1.19.9.2.3.开发指南
1.19.9.2.3.1.函数类
1.19.9.2.3.2.求值方法
1.19.9.2.4.标量函数
1.19.9.2.5.表值函数
1.19.9.2.6.聚合函数
1.19.9.2.7.表值聚合函数

1.19.9.函数

1.19.9.1.概览

Flink中的函数有两个划分标准。
一个划分标准是:系统(内置)函数和 Catalog 函数。系统函数没有名称空间,只能通过其名称来进行引用。 Catalog 函数属于 Catalog 和数据库,因此它们拥有 Catalog 和数据库命名空间。 用户可以通过全/部分限定名(catalog.db.func 或 db.func)或者函数名 来对 Catalog 函数进行引用。

另一个划分标准是:临时函数和持久化函数。 临时函数始终由用户创建,它容易改变并且仅在会话的生命周期内有效。 持久化函数不是由系统提供,就是存储在 Catalog 中,它在会话的整个生命周期内都有效。

这两个划分标准给Flink用户提供了4中函数:
1.临时性系统函数
2.系统函数
3.临时性Catalog函数
4.Catalog函数

请注意,系统函数始终优先于 Catalog 函数解析,临时函数始终优先于持久化函数解析, 函数解析优先级如下所述。

1.19.9.1.1.函数引用

用户在 Flink 中可以通过精确、模糊两种引用方式引用函数。

1.19.9.1.2.精确函数引用

精确函数引用允许用户跨 Catalog,跨数据库调用 Catalog 函数。例如:

select mycatalog.mydb.myfunc(x) from mytable 和 select mydb.myfunc(x) from mytable。

仅Flink 1.10以上版本支持。

1.19.9.1.3.模糊函数引用

在模糊函数引用中,用户只需在SQL查询中指定函数名,例如:

select myfunc(x) from mytable。
1.19.9.1.4.函数解析顺序

当函数名相同,函数类型不同时,函数解析顺序才有意义。 例如:当有三个都名为 “myfunc” 的临时性 Catalog 函数,Catalog 函数,和系统函数时, 如果没有命名冲突,三个函数将会被解析为一个函数。

1.19.9.1.5.精确函数引用

由于系统函数没有命名空间,Flink 中的精确函数引用必须指向临时性 Catalog 函数或 Catalog 函数。
解析顺序如下:
1.临时性catalog函数
2.Catalog函数。

1.19.9.1.6.模糊函数引用

解析顺序如下:
1.临时性系统函数
2.系统函数
3.临时性Catalog函数,在会话的当前Catalog和当前数据库中。
4.Catalog函数,在会话的当前Catalog和当前数据库中。

1.19.9.2.自定义函数
1.19.9.2.1.准备工作

定义pom.xml,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.toto.test</groupId>
    <artifactId>flink-sql-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <!--maven properties -->
        <maven.test.skip>false</maven.test.skip>
        <maven.javadoc.skip>false</maven.javadoc.skip>
        <!-- compiler settings properties -->
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <flink.version>1.12.0</flink.version>
        <commons-lang.version>2.5</commons-lang.version>
        <scala.binary.version>2.11</scala.binary.version>
    </properties>

    <distributionManagement>
        <repository>
            <id>releases</id>
            <layout>default</layout>
            <url>http://xxx.xxx.xxx/nexus/content/repositories/releases/</url>
        </repository>

        <snapshotRepository>
            <id>snapshots</id>
            <name>snapshots</name>
            <url>http://xxx.xxx.xxx/nexus/content/repositories/snapshots/</url>
        </snapshotRepository>
    </distributionManagement>

    <repositories>
        <repository>
            <id>releases</id>
            <layout>default</layout>
            <url>http://xxx.xxx.xxx/nexus/content/repositories/releases/</url>
        </repository>

        <repository>
            <id>snapshots</id>
            <name>snapshots</name>
            <url>http://xxx.xxx.xxx/nexus/content/repositories/snapshots/</url>
            <snapshots>
                <enabled>true</enabled>
                <updatePolicy>always</updatePolicy>
                <checksumPolicy>warn</checksumPolicy>
            </snapshots>
        </repository>

        <repository>
            <id>xxxx</id>
            <name>xxxx</name>
            <url>http://xxx.xxx.xxx/nexus/content/repositories/xxxx/</url>
        </repository>

        <repository>
            <id>public</id>
            <name>public</name>
            <url>http://xxx.xxx.xxx/nexus/content/groups/public/</url>
        </repository>

        <!-- 新加 -->
        <repository>
            <id>cloudera</id>
            <url>https://repository.cloudera.com/artifactory/cloudera-repos/</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients_2.11</artifactId>
            <version>${flink.version}</version>
        </dependency>


        <!-- 取决于你使用的编程语言,选择Java或者Scala API来构建你的Table API和SQL程序 -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-api-java-bridge_2.11</artifactId>
            <version>${flink.version}</version>
            <!--<scope>provided</scope>-->
        </dependency>

        <!-- 如果你想在 IDE 本地运行你的程序,你需要添加下面的模块,具体用哪个取决于你使用哪个 Planner -->
        <!-- Either... (for the old planner that was available before Flink 1.9) -->
        <!--
        如果遇到:Caused by: java.lang.ClassNotFoundException: org.apache.flink.table.api.bridge.java.internal.BatchTableEnvironmentImpl问题,解决办法是去掉:
        <scope>provided</scope>
        -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-planner_2.11</artifactId>
            <version>${flink.version}</version>
            <!--<scope>provided</scope>-->
        </dependency>

        <!-- or.. (for the new Blink planner) -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-planner-blink_2.11</artifactId>
            <version>${flink.version}</version>
            <!--<scope>provided</scope>-->
        </dependency>

        <!--
        内部实现上,部分 table 相关的代码是用 Scala 实现的。所以,下面的依赖也需要添加到你的程序里,不管是批式还是流式的程序:
        -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-scala_2.11</artifactId>
            <version>${flink.version}</version>
            <!--<scope>provided</scope>-->
        </dependency>

        <!-- 如果你想实现自定义格式来解析Kafka数据,或者自定义函数,下面的依赖就足够了,编译出来的jar文件可以直接给SQL Client使用 -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-common</artifactId>
            <version>${flink.version}</version>
            <!--<scope>provided</scope>-->
        </dependency>

        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>

        <!--***************************** scala依赖 *************************************-->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-api-scala-bridge_2.11</artifactId>
            <version>${flink.version}</version>
            <!--<scope>provided</scope>-->
        </dependency>

        <!--***************************** 用jdbc connector 的时候使用*************************-->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-jdbc_2.11</artifactId>
            <version>${flink.version}</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-kafka_2.11</artifactId>
            <version>1.12.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-json</artifactId>
            <version>1.12.0</version>
        </dependency>

    </dependencies>

    <build>
        <finalName>flink-sql-demo</finalName>
        <plugins>
            <!-- 编译插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.0</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                    <encoding>UTF-8</encoding>
                    <compilerVersion>${maven.compiler.source}</compilerVersion>
                    <showDeprecation>true</showDeprecation>
                    <showWarnings>true</showWarnings>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.12.4</version>
                <configuration>
                    <skipTests>${maven.test.skip}</skipTests>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.rat</groupId>
                <artifactId>apache-rat-plugin</artifactId>
                <version>0.12</version>
                <configuration>
                    <excludes>
                        <exclude>README.md</exclude>
                    </excludes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-javadoc-plugin</artifactId>
                <version>2.10.4</version>
                <configuration>
                    <aggregate>true</aggregate>
                    <reportOutputDirectory>javadocs</reportOutputDirectory>
                    <locale>en</locale>
                </configuration>
            </plugin>
            <!-- scala编译插件 -->
            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>3.1.6</version>
                <configuration>
                    <scalaCompatVersion>2.11</scalaCompatVersion>
                    <scalaVersion>2.11.12</scalaVersion>
                    <encoding>UTF-8</encoding>
                </configuration>
                <executions>
                    <execution>
                        <id>compile-scala</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>add-source</goal>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>test-compile-scala</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>add-source</goal>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <!-- 打jar包插件(会包含所有依赖) -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.6</version>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <!-- 可以设置jar包的入口类(可选) -->
                            <mainClass></mainClass>
                        </manifest>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <!--<plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>3.2.2</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <args>
                        <arg>-nobootcp</arg>
                    </args>
                    &lt;!&ndash; 解决Error:(55, 38) Static methods in interface require -target:jvm-1.8问题 &ndash;&gt;
                    <addScalacArgs>-target:jvm-1.8</addScalacArgs>
                </configuration>
            </plugin>-->
        </plugins>
    </build>

</project>
1.19.9.2.2.概述

当前Flink有如下几种函数:
标量函数:将标量值转换成一个新标量值。
表值函数:将标量值转换成新的行数据。
聚合函数:将多行数据里的标量值转换成一个新标量值。
表值聚合函数:将多行数据里的标量值转换成新的行数据。
异步表值函数:是异步查询外部数据系统的特殊函数。

注意 标量和表值函数已经使用了新的基于数据类型的类型系统,聚合函数仍然使用基于 TypeInformation 的旧类型系统。
以下示例展示了如何创建一个基本的标量函数,以及如何在 Table API 和 SQL 里调用这个函数。
函数用于SQL查询前要先经过注册;而在用于Table API时,函数可以先注册后调用,也可以 内联 后直接使用。

package ScalarFunctionDemo;

import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.TableEnvironment;
import org.apache.flink.table.functions.ScalarFunction;

import static org.apache.flink.table.api.Expressions.$;
import static org.apache.flink.table.api.Expressions.call;

public class SubstringFunctionDemo {

    public static class SubstringFunction extends ScalarFunction {
        public String eval(String s, Integer begin, Integer end) {
            return s.substring(begin, end);
        }
    }

    public static void main(String[] args) {
        //ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();

        EnvironmentSettings bbSettings = EnvironmentSettings.newInstance().useBlinkPlanner().inBatchMode().build();
        TableEnvironment tableEnv = TableEnvironment.create(bbSettings);

        // 1、创建表
        tableEnv.executeSql(
                "CREATE TABLE flink_stu (" +
                        "      id BIGINT, " +
                        "      name STRING, " +
                        "      speciality STRING, " +
                        "      PRIMARY KEY (id) NOT ENFORCED" +
                        ") WITH (" +
                        "'connector' = 'jdbc'," +
                        "'url'='jdbc:mysql://xxx.xxx.xxx.xxx:3306/test'," +
                        "'table-name' = 'stu'," +
                        "'username' = 'root', " +
                        "'password' = 'xxxxxx'" +
                        ")");

        //call function "inline" without registration in Table API
        Table t1 = tableEnv.from("flink_stu").select(call(SubstringFunction.class, $("name"),0,2));
        t1.as("newName");
        tableEnv.executeSql("select * from " + t1).print();

        tableEnv.createTemporarySystemFunction("SubstringFunction", SubstringFunction.class);

        //call registered function in Table API
        tableEnv.from("flink_stu").select(call("SubstringFunction",$("name"),0,2));

        //在SQL里调用注册号的函数
        tableEnv.sqlQuery("SELECT SubstringFunction(name, 0, 2) FROM flink_stu");

    }
}
1.19.9.2.3.开发指南

注意:在聚合函数使用新的类型系统之前,本节仅适用于标量和表值函数。
所有的自定义函数都遵循一些基本的实现原则。

1.19.9.2.3.1.函数类

实现类必须继承自合适的基类之一(例如 org.apache.flink.table.functions.ScalarFunction )。
该类必须声明为 public ,而不是 abstract ,并且可以被全局访问。不允许使用非静态内部类或匿名类。
为了将自定义函数存储在持久化的 catalog 中,该类必须具有默认构造器,且在运行时可实例化。

1.19.9.2.3.2.求值方法

基类提供了一组可以被重写的方法,例如 open()、 close() 或 isDeterministic() 。
但是,除了上述方法之外,作用于每条传入记录的主要逻辑还必须通过专门的 求值方法 来实现。
根据函数的种类,后台生成的运算符会在运行时调用诸如 eval()、accumulate() 或 retract() 之类的求值方法。
这些方法必须声明为public,并带有一组定义明确的参数。
常规的 JVM 方法调用语义是适用的。因此可以:
实现重载的方法,例如 eval(Integer) 和 eval(LocalDateTime);
使用变长参数,例如 eval(Integer…);
使用对象继承,例如 eval(Object) 可接受 LocalDateTime 和 Integer 作为参数;
也可组合使用,例如 eval(Object…) 可接受所有类型的参数。

以下代码片段展示了一个重载函数的示例:

package ScalarFunctionDemo;

import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.TableEnvironment;
import org.apache.flink.table.api.TableResult;
import org.apache.flink.table.functions.ScalarFunction;

import static org.apache.flink.table.api.Expressions.$;
import static org.apache.flink.table.api.Expressions.call;

public class SumFunctionDemo {

    public static class SumFunction extends ScalarFunction {
        public Integer eval(Integer a, Integer b) {
            return a + b;
        }

        public Integer eval(String a,String b) {
            return Integer.valueOf(a) + Integer.valueOf(b);
        }

        public Integer eval(Double... d) {
            double result = 0;
            for (double value : d) {
                result += value;
            }
            return (int) result;
        }
    }

    public static void main(String[] args) {
        //ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();

        EnvironmentSettings bbSettings = EnvironmentSettings.newInstance().useBlinkPlanner().inBatchMode().build();
        TableEnvironment tableEnv = TableEnvironment.create(bbSettings);

        // 1、创建表
        tableEnv.executeSql(
                "CREATE TABLE flink_stu (" +
                        "      id BIGINT, " +
                        "      name STRING, " +
                        "      speciality STRING, " +
                        "      PRIMARY KEY (id) NOT ENFORCED" +
                        ") WITH (" +
                        "'connector' = 'jdbc'," +
                        "'url'='jdbc:mysql://xxx.xxx.xxx.xxx:3306/test'," +
                        "'table-name' = 'stu'," +
                        "'username' = 'root', " +
                        "'password' = 'xxxxxx'" +
                        ")");


        System.out.println("==============================================");
        tableEnv.from("flink_stu").select(call(SumFunction.class, $("id"), 2)).execute().print();
        /**
         * 输出结果:
         * |           3 |
         * |           4 |
         * |           5 |
         * |           6 |
         * |           7 |
         * |           8 |
         * |           9 |
         * |          10 |
         * |          11 |
         * |          12 |
         * |          13 |
         * |          14 |
         * |          15 |
         * |          16 |
         */
    }
}
1.19.9.2.4.标量函数

自定义标量函数可以把0到多个标量值映射成 1 个标量值,数据类型里列出的任何数据类型都可作为求值方法的参数和返回值类型。

想要实现自定义标量函数,你需要扩展 org.apache.flink.table.functions 里面的 ScalarFunction 并且实现一个或者多个求值方法。标量函数的行为取决于你写的求值方法。求值方法必须是 public 的,而且名字必须是 eval。

下面的例子展示了如何实现一个求哈希值的函数并在查询里调用它,详情可参考开发指南:

package ScalarFunctionDemo;

import org.apache.flink.api.java.DataSet;
import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.table.annotation.DataTypeHint;
import org.apache.flink.table.annotation.InputGroup;
import org.apache.flink.table.api.bridge.java.BatchTableEnvironment;
import org.apache.flink.table.functions.ScalarFunction;

import static org.apache.flink.table.api.Expressions.$;
import static org.apache.flink.table.api.Expressions.call;

/**
 * @author tuzuoquan
 * @version 1.0
 * @ClassName HashFunctionDemo
 * @description TODO
 * @date 2021/4/12 15:46
 **/
public class HashFunctionDemo {

    public static class HashFunction extends ScalarFunction {

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

    }

    public static void main(String[] args) throws Exception {
        ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
        BatchTableEnvironment tEnv = BatchTableEnvironment.create(env);

        DataSet<WC> input = env.fromElements(new WC("Hello", 1), new WC("Ciao", 1));
        tEnv.createTemporaryView("WordCount", input, $("word"), $("frequency"));

        //在Table API里不经注册直接"内联"调用函数
        tEnv.from("WordCount").select(call(HashFunction.class, $("frequency"))).as("output");

        //注册函数
        tEnv.createTemporarySystemFunction("HashFunction",HashFunction.class);

        //在Table API里调用注册号的函数
        tEnv.from("WordCount").select(call("HashFunction", $("word")));

        //在SQL里调用注册好的函数
        tEnv.sqlQuery("SELECT HashFunction(word) FROM WordCount");
    }

    public static class WC {

        public String word;
        public Integer frequency;

        // public constructor to make it a Flink POJO
        public WC() {}

        public WC(String word, Integer frequency) {
            this.word = word;
            this.frequency = frequency;
        }

        @Override
        public String toString() {
            return "WC " + word + " " + frequency;
        }
    }

}

再如案例:

package udfdemo.scalar;

import java.util.Arrays;

import org.apache.flink.table.functions.ScalarFunction;

/**
 * 标量函数 支持0至多个标量输入 仅输出一个标量
 */
public class HashFunction extends ScalarFunction {

    public int eval() {
        return 0;
    }

    public int eval(Object o) {
        return o.hashCode();
    }

    public int eval(Object... o) {
        return Arrays.hashCode(o);
    }
}
package udfdemo.scalar;

import static org.apache.flink.table.api.Expressions.$;

import org.apache.flink.api.java.DataSet;
import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.BatchTableEnvironment;

/**
 * @author zhanglin
 * @date 2021/04/08
 */
public class HashFunctionTest {

    public static void main(String[] args) throws Exception {
        // set up execution environment
        ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
        BatchTableEnvironment tEnv = BatchTableEnvironment.create(env);

        // 注册udf
        tEnv.createTemporarySystemFunction("zl_hash", new HashFunction());

        DataSet<WC> input = env.fromElements(new WC("Hello", 1), new WC("Ciao", 1));
        tEnv.createTemporaryView("WordCount", input, $("word"), $("frequency"));
        Table table = tEnv.sqlQuery("SELECT zl_hash(word,frequency) FROM WordCount");

        DataSet<Integer> result = tEnv.toDataSet(table, Integer.class);
        result.print();

        table = tEnv.sqlQuery("SELECT zl_hash(word) FROM WordCount");
        result = tEnv.toDataSet(table, Integer.class);
        result.print();

        table = tEnv.sqlQuery("SELECT zl_hash()");
        result = tEnv.toDataSet(table, Integer.class);
        result.print();

    }

    public static class WC {

        public String word;
        public long frequency;

        // public constructor to make it a Flink POJO
        public WC() {}

        public WC(String word, long frequency) {
            this.word = word;
            this.frequency = frequency;
        }

        @Override
        public String toString() {
            return "WC " + word + " " + frequency;
        }
    }
}
1.19.9.2.5.表值函数

跟自定义标量函数一样,自定义表值函数的输入参数也可以是 0 到多个标量。但是跟标量函数只能返回一个值不同的是,它可以返回任意多行。返回的每一行可以包含 1 到多列,如果输出行只包含 1 列,会省略结构化信息并生成标量值,这个标量值在运行阶段会隐式地包装进行里。

要定义一个表值函数,你需要扩展 org.apache.flink.table.functions 下的 TableFunction,可以通过实现多个名为 eval 的方法对求值方法进行重载。像其他函数一样,输入和输出类型也可以通过反射自动提取出来。表值函数返回的表的类型取决于 TableFunction 类的泛型参数 T,不同于标量函数,表值函数的求值方法本身不包含返回类型,而是通过 collect(T) 方法来发送要输出的行。

在 Table API 中,表值函数是通过 .joinLateral(…) 或者 .leftOuterJoinLateral(…) 来使用的。joinLateral 算子会把外表(算子左侧的表)的每一行跟跟表值函数返回的所有行(位于算子右侧)进行 (cross)join。leftOuterJoinLateral 算子也是把外表(算子左侧的表)的每一行跟表值函数返回的所有行(位于算子右侧)进行(cross)join,并且如果表值函数返回 0 行也会保留外表的这一行。

在 SQL 里面用 JOIN 或者 以 ON TRUE 为条件的 LEFT JOIN 来配合 LATERAL TABLE() 的使用。

下面的例子展示了如何实现一个分隔函数并在查询里调用它,详情可参考开发指南:

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(...);
// 在 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"));
// 注册函数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");

如果你打算使用 Scala,不要把表值函数声明为 Scala object,Scala object 是单例对象,将导致并发问题。
如果你打算使用 Python 实现或调用表值函数,详情可参考 Python 表值函数。

1.19.9.2.6.聚合函数

自定义聚合函数(UDAGG)是把一个表(一行或者多行,每行可以有一列或者多列)聚合成一个标量值。
在这里插入图片描述

上面的图片展示了一个聚合的例子。假设你有一个关于饮料的表。表里面有三个字段,分别是 id、name、price,表里有 5 行数据。假设你需要找到所有饮料里最贵的饮料的价格,即执行一个 max() 聚合。你需要遍历所有 5 行数据,而结果就只有一个数值。

自定义聚合函数是通过扩展 AggregateFunction 来实现的。AggregateFunction 的工作过程如下。首先,它需要一个 accumulator,它是一个数据结构,存储了聚合的中间结果。通过调用 AggregateFunction 的 createAccumulator() 方法创建一个空的 accumulator。接下来,对于每一行数据,会调用 accumulate() 方法来更新 accumulator。当所有的数据都处理完了之后,通过调用 getValue 方法来计算和返回最终的结果。
下面几个方法是每个 AggregateFunction 必须要实现的:
createAccumulator()
accumulate()
getValue()
Flink 的类型推导在遇到复杂类型的时候可能会推导出错误的结果,比如那些非基本类型和普通的 POJO 类型的复杂类型。所以跟 ScalarFunction 和 TableFunction 一样,AggregateFunction 也提供了 AggregateFunction#getResultType() 和 AggregateFunction#getAccumulatorType() 来分别指定返回值类型和 accumulator 的类型,两个函数的返回值类型也都是 TypeInformation。

除了上面的方法,还有几个方法可以选择实现。这些方法有些可以让查询更加高效,而有些是在某些特定场景下必须要实现的。例如,如果聚合函数用在会话窗口(当两个会话窗口合并的时候需要 merge 他们的 accumulator)的话,merge() 方法就是必须要实现的。
AggregateFunction 的以下方法在某些场景下是必须实现的:
retract() 在 bounded OVER 窗口中是必须实现的。
merge() 在许多批式聚合和会话以及滚动窗口聚合中是必须实现的。除此之外,这个方法对于优化也很多帮助。例如,两阶段聚合优化就需要所有的 AggregateFunction 都实现 merge 方法。
resetAccumulator() 在许多批式聚合中是必须实现的。

AggregateFunction 的所有方法都必须是 public 的,不能是 static 的,而且名字必须跟上面写的一样。createAccumulator、getValue、getResultType 以及 getAccumulatorType 这几个函数是在抽象类 AggregateFunction 中定义的,而其他函数都是约定的方法。如果要定义一个聚合函数,你需要扩展 org.apache.flink.table.functions.AggregateFunction,并且实现一个(或者多个)accumulate 方法。accumulate 方法可以重载,每个方法的参数类型不同,并且支持变长参数。
AggregateFunction 的所有方法的详细文档如下。
前置条件(创建MySQL表):

-- UDAGG案例
CREATE TABLE `flink_udtagg` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(60) DEFAULT NULL,
  `price` bigint(20) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

CREATE TABLE `flink_avgPrice` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `avgPrice` bigint(20) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

Java代码如下:

package udaggdemo;

import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.functions.AggregateFunction;

import java.io.Serializable;

/**
 * @author tuzuoquan
 * @version 1.0
 * @ClassName UDAGGDemo
 * @description
 * 以下是聚合函数的demo
 *
 * 要添加以下依赖:
 *  <dependency>
 *      <groupId>org.apache.flink</groupId>
 *      <artifactId>flink-connector-kafka_2.11</artifactId>
 *      <version>1.12.0</version>
 *  </dependency>
 *
 *  <dependency>
 *      <groupId>org.apache.flink</groupId>
 *      <artifactId>flink-json</artifactId>
 *      <version>1.12.0</version>
 *  </dependency>
 * @date 2021/4/6 9:24
 **/
public class UDAGGDemo {

    public static class PriceAvgAccum implements Serializable {
        public long sum = 0;
        public int recordNum = 0;
    }

    /**
     * @author tuzuoquan
     * @version 1.0
     * @ClassName PriceAvg
     * @description 自定义聚合函数是通过扩展 AggregateFunction 来实现的。AggregateFunction 的工作过程如下。
     * 首先,它需要一个 accumulator,它是一个数据结构,存储了聚合的中间结果。通过调用 AggregateFunction的
     * createAccumulator() 方法创建一个空的 accumulator。接下来,对于每一行数据,会调用accumulate()
     * 方法来更新accumulator。当所有的数据都处理完了之后,通过调用 getValue 方法来计算和返回最终的结果。
     *
     * https://helpcdn.aliyun.com/document_detail/69553.html
     * https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/dev/table/functions/udfs.html
     *
     * flink程序中运行的命令:
     * $FLINK_HOME/bin/flink run -c udfdemo.PriceAvg /root/demosql/flink-sql-demo.jar
     *
     * @date 2021/4/2 16:27
     **/
    public static class PriceAvg extends AggregateFunction<Long, PriceAvgAccum> {

        /**
         * 通过调用AggregateFunction的createAccumulator()方法创建一个空的accumulator。
         * @return
         */
        @Override
        public PriceAvgAccum createAccumulator() {
            return new PriceAvgAccum();
        }

        /**
         * 当所有的数据都处理完了之后,通过调用 getValue 方法来计算和返回最终的结果。
         * @param acc
         * @return
         */
        @Override
        public Long getValue(PriceAvgAccum acc) {
            if (acc.recordNum == 0) {
                return null;
            } else {
                return acc.sum / acc.recordNum;
            }
        }

        /**
         * 接下来,对于每一行数据,会调用 accumulate() 方法来更新 accumulator。
         * @param acc
         * @param price      单个的价格
         */
        public void accumulate(PriceAvgAccum acc, long price) {
            acc.sum += price;
            acc.recordNum += 1;
        }

    }


    public static void main(String[] args) {
        ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();

        // BLINK STREAMING QUERY
        StreamExecutionEnvironment bsEnv = StreamExecutionEnvironment.getExecutionEnvironment();
        EnvironmentSettings bsSettings = EnvironmentSettings.newInstance().useBlinkPlanner().inStreamingMode().build();
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(bsEnv, bsSettings);

        /**
         创建topic
         bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic flink_udtagg
         查看topic是否存在了
         bin/kafka-topics.sh --list --zookeeper localhost:2181

         [root@flink01 flink-1.12.1]# cd $KAFKA_HOME
         [root@middleware kafka_2.12-2.6.0]# bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic flink_udtagg
         >{"id": 1,"name": "20201009","price":20}
         >{"id": 2,"name": "20201009","price":20}
         >{"id": 3,"name": "20210406","price":10}
         >{"id": 4,"name": "20201009","price":10}
         >{"id": 5,"name": "20201009","price":10}
         >{"id": 6,"name": "20210406","price":10}

         创建所需的表:
         -- UDAGG案例
         CREATE TABLE `flink_udtagg` (
         `id` bigint(20) NOT NULL AUTO_INCREMENT,
         `name` varchar(60) DEFAULT NULL,
         `price` bigint(20) DEFAULT '0',
         PRIMARY KEY (`id`)
         ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

         CREATE TABLE `flink_avgPrice` (
         `id` bigint(20) NOT NULL AUTO_INCREMENT,
         `avgPrice` bigint(20) DEFAULT '0',
         PRIMARY KEY (`id`)
         ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
         **/
        tableEnv.executeSql("create table flink_udtagg_kafkatest ( \n" +
                "  id BIGINT,\n" +
                "  name VARCHAR,\n" +
                "  price BIGINT,\n" +
                "  proctime AS PROCTIME ()\n" +
                ")\n" +
                " with (\n" +
                "  'connector' = 'kafka',\n" +
                "  'topic' = 'flink_udtagg',\n" +
                "  'properties.bootstrap.servers' = 'xxx.xxx.xxx.xxx:9092', \n" +
                "  'properties.group.id' = 'flink_gp_test1',\n" +
                "  'scan.startup.mode' = 'earliest-offset',\n" +
                "  'format' = 'json',\n" +
                "  'json.fail-on-missing-field' = 'false',\n" +
                "  'json.ignore-parse-errors' = 'true',\n" +
                "  'properties.zookeeper.connect' = 'xxx.xxx.xxx.xxx:2181/kafka'\n" +
                " )");

        // 1、创建表
        tableEnv.executeSql(
                "CREATE TABLE flink_udtagg (" +
                        "      id BIGINT, " +
                        "      name STRING, " +
                        "      price BIGINT, " +
                        "      PRIMARY KEY (id) NOT ENFORCED" +
                        ") WITH (" +
                        "'connector' = 'jdbc'," +
                        "'url'='jdbc:mysql://xxx.xxx.xxx.xxx:3306/test'," +
                        "'table-name' = 'flink_udtagg'," +
                        "'username' = 'root', " +
                        "'password' = '123456'" +
                        ")");

        //通过此句可以持续不断的将kafka中接收到的数据刷新到flink_udtagg中。这个过程不会停,只要有数据了,就看也继续执行
        tableEnv.executeSql("INSERT INTO flink_udtagg SELECT id,name,price from flink_udtagg_kafkatest");

        tableEnv.registerFunction("avgPrice", new PriceAvg());

        tableEnv.executeSql(
                "CREATE TABLE flink_avgPrice (" +
                        "      id BIGINT, " +
                        "      avgPrice FLOAT, " +
                        "      PRIMARY KEY (id) NOT ENFORCED" +
                        ") WITH (" +
                        "'connector' = 'jdbc'," +
                        "'url'='jdbc:mysql://xxx.xxx.xxx.xxx:3306/test'," +
                        "'table-name' = 'flink_avgPrice'," +
                        "'username' = 'root', " +
                        "'password' = '123456'" +
                        ")");
        tableEnv.executeSql("INSERT INTO flink_avgPrice SELECT NULL as id, avgPrice(price) as avgPrice FROM flink_udtagg_kafkatest");
    }
}

再如案例:

package udfdemo.aggregate;

import java.util.Iterator;

import org.apache.flink.table.annotation.DataTypeHint;
import org.apache.flink.table.annotation.FunctionHint;
import org.apache.flink.table.functions.AggregateFunction;

/**
 * 自定义聚合函数是通过扩展 AggregateFunction 来实现的。AggregateFunction 的工作过程如下。
 * 首先,它需要一个 accumulator,它是一个数据结构,存储了聚合的中间结果。
 * 通过调用AggregateFunction的createAccumulator()方法创建一个空的accumulator。
 * 接下来,对于每一行数据,会调用 accumulate() 方法来更新accumulator。
 * 当所有的数据都处理完了之后,通过调用 getValue 方法来计算和返回最终的结果。
 * 
 */
@FunctionHint(output = @DataTypeHint("INT"))
public class MaxAggregateFunction extends AggregateFunction<Integer, MaxAggregateFunction.MaxAccum> {

    /**
     * 获取最终结果 必须实现
     * 
     * @param maxAccum
     * @return
     */
    @Override
    public Integer getValue(MaxAccum maxAccum) {
        if (maxAccum.max != Integer.MIN_VALUE) {
            return maxAccum.max;
        }
        return null;
    }

    /**
     * 计算结果 必须实现
     * 
     * @param accumulator
     * @param value
     */
    public void accumulate(MaxAccum accumulator, Integer value) {
        if (value > accumulator.max) {
            accumulator.secondMax = accumulator.max;
            accumulator.max = value;
        }
    }

    /**
     * 创建累加器 必须实现
     *
     * @return
     */
    @Override
    public MaxAccum createAccumulator() {
        return new MaxAccum();
    }

    /**
     * 撤销累加结果 可选实现
     * 在bounded OVER窗口中是必须实现的
     * 
     * @param accumulator
     * @param value
     */
    public void retract(MaxAccum accumulator, Integer value) {
        if (value == accumulator.max) {
            accumulator.max = accumulator.secondMax;
            accumulator.secondMax = Integer.MIN_VALUE;
        }
    }

    /**
     * 合并结果 可选实现
     * 在许多批式聚合和会话以及滚动窗口聚合中是必须实现的。
     * 除此之外,这个方法对于优化也很多帮助。
     * 例如,两阶段聚合优化就需要所有的 AggregateFunction 都实现 merge 方法
     * 
     * @param accumulator
     * @param its
     */
    public void merge(MaxAccum accumulator, Iterable<MaxAccum> its) {
        Iterator<MaxAccum> iter = its.iterator();
        while (iter.hasNext()) {
            MaxAccum a = iter.next();
            if (a.max > accumulator.max) {
                accumulator.secondMax = accumulator.max;
                accumulator.max = a.max;
            }
        }
    }

    /**
     * 重置累加器 可选实现
     * 在许多批式聚合中是必须实现的
     * 
     * @param accumulator
     */
    public void resetAccumulator(MaxAccum accumulator) {
        accumulator.max = Integer.MIN_VALUE;
        accumulator.secondMax = Integer.MIN_VALUE;
    }

    /**
     * 累加器
     */
    public static class MaxAccum {

        public int max = Integer.MIN_VALUE;
        public int secondMax = Integer.MIN_VALUE;
    }
}
package udfdemo.aggregate;

import static org.apache.flink.table.api.Expressions.$;

import java.util.Arrays;

import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.TableResult;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;

public class MaxAggregateFunctionTest {

    public static void main(String[] args) throws Exception {
        // set up execution environment
        StreamExecutionEnvironment bsEnv = StreamExecutionEnvironment.getExecutionEnvironment();
        EnvironmentSettings bsSettings = EnvironmentSettings.newInstance().useBlinkPlanner().inStreamingMode().build();
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(bsEnv, bsSettings);

        // 注册udf
        tableEnv.createTemporarySystemFunction("zl_max", new MaxAggregateFunction());
        DataStream<UserScores> dataStream =
                bsEnv.fromCollection(Arrays.asList(new UserScores(1, "Lettle", 6), new UserScores(2, "Milk", 3)));
        tableEnv.createTemporaryView("UserScores", dataStream, $("id"), $("name"), $("price"));
        TableResult tableResult = tableEnv.sqlQuery("SELECT zl_max(price) FROM UserScores").execute();
        tableResult.print();
    }

    public static class UserScores {

        private int id;
        private String name;
        private int price;

        public UserScores() {}

        public UserScores(int id, String name, int price) {
            this.id = id;
            this.name = name;
            this.price = price;
        }

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getPrice() {
            return price;
        }

        public void setPrice(int price) {
            this.price = price;
        }

        @Override
        public String toString() {
            return "UserScores " + id + " " + name + " " + price;
        }
    }
}
1.19.9.2.7.表值聚合函数

自定义表值聚合函数(UDTAGG)可以把一个表(一行或者多行,每行有一列或者多列)聚合成另一张表,结果中可以有多行多列。
在这里插入图片描述

上图展示了一个表值聚合函数的例子。假设你有一个饮料的表,这个表有 3 列,分别是 id、name 和 price,一共有 5 行。假设你需要找到价格最高的两个饮料,类似于 top2() 表值聚合函数。你需要遍历所有 5 行数据,结果是有 2 行数据的一个表。

用户自定义表值聚合函数是通过扩展 TableAggregateFunction 类来实现的。一个 TableAggregateFunction 的工作过程如下。首先,它需要一个 accumulator,这个 accumulator 负责存储聚合的中间结果。 通过调用 TableAggregateFunction 的 createAccumulator 方法来构造一个空的 accumulator。接下来,对于每一行数据,会调用 accumulate 方法来更新 accumulator。当所有数据都处理完之后,调用 emitValue 方法来计算和返回最终的结果。

下面几个 TableAggregateFunction 的方法是必须要实现的:
createAccumulator()
accumulate()
Flink 的类型推导在遇到复杂类型的时候可能会推导出错误的结果,比如那些非基本类型和普通的 POJO 类型的复杂类型。所以类似于 ScalarFunction 和 TableFunction,TableAggregateFunction 也提供了 TableAggregateFunction#getResultType() 和 TableAggregateFunction#getAccumulatorType() 方法来指定返回值类型和 accumulator 的类型,这两个方法都需要返回 TypeInformation。

除了上面的方法,还有几个其他的方法可以选择性的实现。有些方法可以让查询更加高效,而有些方法对于某些特定场景是必须要实现的。比如,在会话窗口(当两个会话窗口合并时会合并两个 accumulator)中使用聚合函数时,必须要实现merge() 方法。

下面几个 TableAggregateFunction 的方法在某些特定场景下是必须要实现的:

retract() 在 bounded OVER 窗口中的聚合函数必须要实现。
merge() 在许多批式聚合和以及流式会话和滑动窗口聚合中是必须要实现的。
resetAccumulator() 在许多批式聚合中是必须要实现的。
emitValue() 在批式聚合以及窗口聚合中是必须要实现的。
下面的 TableAggregateFunction 的方法可以提升流式任务的效率:

emitUpdateWithRetract() 在 retract 模式下,该方法负责发送被更新的值。
emitValue 方法会发送所有 accumulator 给出的结果。拿 TopN 来说,emitValue 每次都会发送所有的最大的 n 个值。这在流式任务中可能会有一些性能问题。为了提升性能,用户可以实现 emitUpdateWithRetract 方法。这个方法在 retract 模式下会增量的输出结果,比如有数据更新了,我们必须要撤回老的数据,然后再发送新的数据。如果定义了 emitUpdateWithRetract 方法,那它会优先于 emitValue 方法被使用,因为一般认为 emitUpdateWithRetract 会更加高效,因为它的输出是增量的。

TableAggregateFunction 的所有方法都必须是 public 的、非 static 的,而且名字必须跟上面提到的一样。createAccumulator、getResultType 和 getAccumulatorType 这三个方法是在抽象父类 TableAggregateFunction 中定义的,而其他的方法都是约定的方法。要实现一个表值聚合函数,你必须扩展 org.apache.flink.table.functions.TableAggregateFunction,并且实现一个(或者多个)accumulate 方法。accumulate 方法可以有多个重载的方法,也可以支持变长参数。

TableAggregateFunction 的所有方法的详细文档如下。

/**
  * Base class for user-defined aggregates and table aggregates.
  *
  * @param <T>   the type of the aggregation result.
  * @param <ACC> the type of the aggregation accumulator. The accumulator is used to keep the
  *             aggregated values which are needed to compute an aggregation result.
  */
public abstract class UserDefinedAggregateFunction<T, ACC> extends UserDefinedFunction {

  /**
    * Creates and init the Accumulator for this (table)aggregate function.
    *
    * @return the accumulator with the initial value
    */
  public ACC createAccumulator(); // MANDATORY

  /**
    * Returns the TypeInformation of the (table)aggregate function's result.
    *
    * @return The TypeInformation of the (table)aggregate function's result or null if the result
    *         type should be automatically inferred.
    */
  public TypeInformation<T> getResultType = null; // PRE-DEFINED

  /**
    * Returns the TypeInformation of the (table)aggregate function's accumulator.
    *
    * @return The TypeInformation of the (table)aggregate function's accumulator or null if the
    *         accumulator type should be automatically inferred.
    */
  public TypeInformation<ACC> getAccumulatorType = null; // PRE-DEFINED
}

/**
  * Base class for table aggregation functions.
  *
  * @param <T>   the type of the aggregation result
  * @param <ACC> the type of the aggregation accumulator. The accumulator is used to keep the
  *             aggregated values which are needed to compute a table aggregation result.
  *             TableAggregateFunction represents its state using accumulator, thereby the state of
  *             the TableAggregateFunction must be put into the accumulator.
  */
public abstract class TableAggregateFunction<T, ACC> extends UserDefinedAggregateFunction<T, ACC> {

  /** Processes the input values and update the provided accumulator instance. The method
    * accumulate can be overloaded with different custom types and arguments. A TableAggregateFunction
    * requires at least one accumulate() method.
    *
    * @param accumulator           the accumulator which contains the current aggregated results
    * @param [user defined inputs] the input value (usually obtained from a new arrived data).
    */
  public void accumulate(ACC accumulator, [user defined inputs]); // MANDATORY

  /**
    * Retracts the input values from the accumulator instance. The current design assumes the
    * inputs are the values that have been previously accumulated. The method retract can be
    * overloaded with different custom types and arguments. This function must be implemented for
    * datastream bounded over aggregate.
    *
    * @param accumulator           the accumulator which contains the current aggregated results
    * @param [user defined inputs] the input value (usually obtained from a new arrived data).
    */
  public void retract(ACC accumulator, [user defined inputs]); // OPTIONAL

  /**
    * Merges a group of accumulator instances into one accumulator instance. This function must be
    * implemented for datastream session window grouping aggregate and dataset grouping aggregate.
    *
    * @param accumulator  the accumulator which will keep the merged aggregate results. It should
    *                     be noted that the accumulator may contain the previous aggregated
    *                     results. Therefore user should not replace or clean this instance in the
    *                     custom merge method.
    * @param its          an {@link java.lang.Iterable} pointed to a group of accumulators that will be
    *                     merged.
    */
  public void merge(ACC accumulator, java.lang.Iterable<ACC> its); // OPTIONAL

  /**
    * Called every time when an aggregation result should be materialized. The returned value
    * could be either an early and incomplete result  (periodically emitted as data arrive) or
    * the final result of the  aggregation.
    *
    * @param accumulator the accumulator which contains the current
    *                    aggregated results
    * @param out         the collector used to output data
    */
  public void emitValue(ACC accumulator, Collector<T> out); // OPTIONAL

  /**
    * Called every time when an aggregation result should be materialized. The returned value
    * could be either an early and incomplete result (periodically emitted as data arrive) or
    * the final result of the aggregation.
    *
    * Different from emitValue, emitUpdateWithRetract is used to emit values that have been updated.
    * This method outputs data incrementally in retract mode, i.e., once there is an update, we
    * have to retract old records before sending new updated ones. The emitUpdateWithRetract
    * method will be used in preference to the emitValue method if both methods are defined in the
    * table aggregate function, because the method is treated to be more efficient than emitValue
    * as it can outputvalues incrementally.
    *
    * @param accumulator the accumulator which contains the current
    *                    aggregated results
    * @param out         the retractable collector used to output data. Use collect method
    *                    to output(add) records and use retract method to retract(delete)
    *                    records.
    */
  public void emitUpdateWithRetract(ACC accumulator, RetractableCollector<T> out); // OPTIONAL

  /**
    * Collects a record and forwards it. The collector can output retract messages with the retract
    * method. Note: only use it in {@code emitRetractValueIncrementally}.
    */
  public interface RetractableCollector<T> extends Collector<T> {

      /**
        * Retract a record.
        *
        * @param record The record to retract.
        */
      void retract(T record);
  }
}

下面的例子展示了如何

定义一个 TableAggregateFunction 来计算给定列的最大的 2 个值,
在 TableEnvironment 中注册函数,
在 Table API 查询中使用函数(当前只在 Table API 中支持 TableAggregateFunction)。
为了计算最大的 2 个值,accumulator 需要保存当前看到的最大的 2 个值。在我们的例子中,我们定义了类 Top2Accum 来作为 accumulator。Flink 的 checkpoint 机制会自动保存 accumulator,并且在失败时进行恢复,来保证精确一次的语义。

我们的Top2 表值聚合函数(TableAggregateFunction)的 accumulate() 方法有两个输入,第一个是 Top2Accum accumulator,另一个是用户定义的输入:输入的值 v。尽管 merge() 方法在大多数聚合类型中不是必须的,我们也在样例中提供了它的实现。请注意,我们在 Scala 样例中也使用的是Java 的基础类型,并且定义了 getResultType() 和 getAccumulatorType() 方法,因为 Flink 的类型推导对于 Scala 的类型推导支持的不是很好。
案例:

package udfdemo.table_aggregate;

import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.table.functions.TableAggregateFunction;
import org.apache.flink.util.Collector;

/**
 * @author zhanglin
 * @date 2021/04/12
 */
public class Top2 extends TableAggregateFunction<Tuple2<Integer, Integer>, Top2.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 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));
        }
    }

    public void emitUpdateWithRetract(Top2Accum acc, RetractableCollector<Tuple2<Integer, Integer>> out) {
        if (!acc.first.equals(acc.oldFirst)) {
            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 (acc.oldSecond != Integer.MIN_VALUE) {
                out.retract(Tuple2.of(acc.oldSecond, 2));
            }
            out.collect(Tuple2.of(acc.second, 2));
            acc.oldSecond = acc.second;
        }
    }

    public static class Top2Accum {

        public Integer first;
        public Integer second;
        public Integer oldFirst;
        public Integer oldSecond;
    }

}
package udfdemo.table_aggregate;

import static org.apache.flink.table.api.Expressions.$;
import static org.apache.flink.table.api.Expressions.call;

import java.util.Arrays;

import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.TableResult;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;

/**
 * @author zhanglin
 * @date 2021/04/12
 */
public class Top2Test {

    public static void main(String[] args) throws Exception {
        // set up execution environment
        StreamExecutionEnvironment bsEnv = StreamExecutionEnvironment.getExecutionEnvironment();
        EnvironmentSettings bsSettings = EnvironmentSettings.newInstance().useBlinkPlanner().inStreamingMode().build();
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(bsEnv, bsSettings);

        // 注册udf
        tableEnv.createTemporarySystemFunction("top2", new Top2());
        DataStream<UserScores> dataStream = bsEnv.fromCollection(Arrays.asList(new UserScores(1, "Lettle", 6),
                new UserScores(2, "Milk", 3), new UserScores(3, "Breve", 5)));
        Table table = tableEnv.fromDataStream(dataStream, $("id"), $("name"), $("price"));
        TableResult tableResult =
                table.flatAggregate(call(Top2.class, $("price")).as("v", "rank")).select($("v"), $("rank")).execute();
        tableResult.print();
    }

    public static class UserScores {

        private int id;
        private String name;
        private int price;

        public UserScores() {}

        public UserScores(int id, String name, int price) {
            this.id = id;
            this.name = name;
            this.price = price;
        }

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getPrice() {
            return price;
        }

        public void setPrice(int price) {
            this.price = price;
        }

        @Override
        public String toString() {
            return "UserScores " + id + " " + name + " " + price;
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

涂作权的博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值