Flink学习笔记(十一)Table API 和 SQL

11. Table API 和 SQL

SQL 是结构化查询语言(Structured Query Language)的缩写,是对关系型数据库进 行查询和修改的通用编程语言。在关系型数据库中,数据是以表(table)的形式组织起来的, 所以也可以认为 SQL 是用来对表进行处理的工具语言。Flink 同样提供了对于“表”处理的支持,这就是更高层级的应用 API,在 Flink 中被称为 Table API 和 SQL。

Table API 和 SQL 最初并不完善,在 Flink 1.9 版本合并阿里巴巴内部版本 Blink 之后发生了非常大的改变,此后也一直处在快速开发和完善的过程中,直到 Flink 1.12 版本才基本上做到了功能上的完善。而即使是在目前最新的 1.13 版本中,Table API 和 SQL 也 依然不算稳定,接口用法还在不停调整和更新。

11.1 快速上手

11.1.1 需要依赖
<properties>
    <flink.version>1.13.0</flink.version>
    <java.version>1.8</java.version>
    <scala.binary.version>2.12</scala.binary.version>
    <slf4j.version>1.7.30</slf4j.version>
</properties>

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-table-api-java-bridge_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>

在本地的集成开发环境(IDE)里运行 Table API 和 SQL,还需要引入以下 依赖:

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-table-planner-blink_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-streaming-scala_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>
11.1.2 示例

如果使用 DataStream API,我们可以直接读取数据源后,用一个简单转换算子 map 来做字 段的提取。而这个需求直接写 SQL 的话,实现会更加简单:

select url, user from EventTable;

代码实现:

import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import static org.apache.flink.table.api.Expressions.$;

public class SimpleTableExample {
    public static void main(String[] args) throws Exception {
        // 获取流执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        // 1. 读取数据源
        SingleOutputStreamOperator<Event> eventStream = env
                .fromElements(
                        new Event("Alice", "./home", 1000L),
                        new Event("Bob", "./cart", 1000L),
                        new Event("Alice", "./prod?id=1", 5 * 1000L),
                        new Event("Cary", "./home", 60 * 1000L),
                        new Event("Bob", "./prod?id=3", 90 * 1000L),
                        new Event("Alice", "./prod?id=7", 105 * 1000L)
                );

        // 2. 获取表环境
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

        // 3. 将数据流转换成表
        Table eventTable = tableEnv.fromDataStream(eventStream);

        // 4. 用执行SQL 的方式提取数据
        Table resultTable1 = tableEnv.sqlQuery("select url, user from " + eventTable);

        // 5. 基于Table直接转换
        Table resultTable2 = eventTable.select($("user"), $("url"))
                .where($("user").isEqual("Alice"));

        // 6. 将表转换成数据流,打印输出
        tableEnv.toDataStream(resultTable1).print("result1");
        tableEnv.toDataStream(resultTable2).print("result2");

        // 执行程序
        env.execute();
    }
}

Event.java:

import java.sql.Timestamp;

public class Event {
    public String user;
    public String url;
    public Long timestamp;

    public Event() {
    }

    public Event(String user, String url, Long timestamp) {
        this.user = user;
        this.url = url;
        this.timestamp = timestamp;
    }

    @Override
    public String toString() {
        return "Event{" +
                "user='" + user + '\'' +
                ", url='" + url + '\'' +
                ", timestamp=" + new Timestamp(timestamp) +
                '}';
    }
}

执行结果:
在这里插入图片描述
程序将原始的 Event 数据转换成了(url,user)这样类似二元组的类型。每行输 出前面有一个“+I”标志,这是表示每条数据都是“插入”(Insert)到表中的新增数据。

11.2 基本 API

11.2.1 程序架构

程序的整体处理流程与 DataStream API 非常相似,也可以分为读取数据 源(Source)、转换(Transform)、输出数据(Sink)三部分;只不过这里的输入输出操作不需 要额外定义,只需要将用于输入和输出的表定义出来,然后进行转换查询就可以了。

程序基本架构如下:

// 创建表环境
TableEnvironment tableEnv = ...;
// 创建输入表,连接外部系统读取数据
tableEnv.executeSql("CREATE TEMPORARY TABLE inputTable ... WITH ( 'connector' = ... )");
// 注册一个表,连接到外部系统,用于输出
tableEnv.executeSql("CREATE TEMPORARY TABLE outputTable ... WITH ( 'connector' = ... )");
// 执行 SQL 对表进行查询转换,得到一个新的表
Table table1 = tableEnv.sqlQuery("SELECT ... FROM inputTable... ");
// 使用 Table API 对表进行查询转换,得到一个新的表
Table table2 = tableEnv.from("inputTable").select(...);
// 将得到的结果写入输出表
TableResult tableResult = table1.executeInsert("outputTable");
11.2.2 创建表环境

表环境主要负责:

(1)注册 Catalog 和表;

(2)执行 SQL 查询;

(3)注册用户自定义函数(UDF);

(4)DataStream 和表之间的转换。

Catalog 就是“目录”,与标准 SQL 中的概念是一致的,主要用来管理所有数据库 (database)和表(table)的元数据(metadata)。

每个表和 SQL 的执行,都必须绑定在一个表环境(TableEnvironment)中。TableEnvironment 是 Table API 中提供的基本接口类,可以通过调用静态的 create()方法来创建一个表环境实例。 方法需要传入一个环境的配置参数 EnvironmentSettings,它可以指定当前表环境的执行模式和 计划器(planner)。执行模式有批处理和流处理两种选择,默认是流处理模式;计划器默认使 用 blink planner。

import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.TableEnvironment;

EnvironmentSettings settings = EnvironmentSettings
    .newInstance()
    .inStreamingMode() // 使用流处理模式
    .build();
TableEnvironment tableEnv = TableEnvironment.create(settings);

也可以用另一种更加简单的方式 来创建表环境:

import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
11.2.3 创建表

为了方便地查询表,表环境中会维护一个目录(Catalog)和表的对应关系。所以表都是 通过 Catalog 来进行注册创建的。表在环境中有一个唯一的 ID,由三部分组成:目录(catalog) 名,数据库(database)名,以及表名。在默认情况下,目录名为 default_catalog,数据库名为 default_database。所以如果我们直接创建一个叫作 MyTable 的表,它的 ID 就是:

default_catalog.default_database.MyTable

具体创建表的方式,有通过连接器(connector)和虚拟表(virtual tables)两种。

  1. 连接器表(Connector Tables)

最直观的创建表的方式,就是通过连接器(connector)连接到一个外部系统,然后定义出 对应的表结构。例如可以连接到 Kafka 或者文件系统,将存储在这些外部系统的数据以“表” 的形式定义出来,这样对表的读写就可以通过连接器转换成对外部系统的读写了。

调用表环境的 executeSql()方法,可以传入一个 DDL 作为参数执行 SQL 操作。这里我们传入一个 CREATE 语句进行表的创建,并通过 WITH 关键字指定连接到 外部系统的连接器:

tableEnv.executeSql("CREATE [TEMPORARY] TABLE MyTable ... WITH ( 'connector' = ... )");

如果希望使用自定义的目录名和库名,可以在环境中 进行设置:

tEnv.useCatalog("xxxx_catalog");
tEnv.useDatabase("xxxx_database");
  1. 虚拟表(Virtual Tables)

在环境中注册之后,我们就可以在 SQL 中直接使用这张表进行查询转换了。

Table newTable = tableEnv.sqlQuery("SELECT ... FROM MyTable... ");

由于 newTable 是一个 Table 对象,并没有在表环境中注册;所以我们还需要将这 个中间结果表注册到环境中,才能在 SQL 中使用:

tableEnv.createTemporaryView("NewTable", newTable);

这里的注册其实是创建了一个“虚拟表”(Virtual Table)。这个概念与 SQL 语 法中的视图(View)非常类似,所以调用的方法也叫作创建“虚拟视图”(createTemporaryView)。视图之所以是“虚拟”的,是因为我们并不会直接保存这个表的内容,并没有“实体”;只是 在用到这张表的时候,会将它对应的查询语句嵌入到 SQL 中。

11.2.4 表的查询

对一个表的查询(Query)操作,就 对应着流数据的转换(Transform)处理。Flink 提供了两种查询方式:SQL 和 Table API。

  1. 执行 SQL 进行查询

在代码中,只要调用表环境的 sqlQuery()方法,传入一个字符串形式的 SQL 查询语句 就可以了。执行得到的结果,是一个 Table 对象。

// 创建表环境
TableEnvironment tableEnv = ...; 
// 创建表
tableEnv.executeSql("CREATE TABLE EventTable ... WITH ( 'connector' = ... )");
// 查询用户 Alice 的点击事件,并提取表中前两个字段
Table aliceVisitTable = tableEnv.sqlQuery(
    "SELECT user, url " +
    "FROM EventTable " +
    "WHERE user = 'Alice' "
);

目前 Flink 支持标准 SQL 中的绝大部分用法,并提供了丰富的计算函数。这样我们就可 以把已有的技术迁移过来,像在 MySQL、Hive 中那样直接通过编写 SQL 实现自己的处理需求。

Table urlCountTable = tableEnv.sqlQuery(
    "SELECT user, COUNT(url) " +
    "FROM EventTable " +
    "GROUP BY user "
);

得到的是一个新的 Table 对象,我们可以再次将它注册为虚拟表继续在 SQL 中调用。另外,我们也可以直接将查询的结果写入到已经注册的表中,这需要调用表环境的 executeSql()方法来执行 DDL,传入的是一个 INSERT 语句:

// 注册表
tableEnv.executeSql("CREATE TABLE EventTable ... WITH ( 'connector' = ... )");
tableEnv.executeSql("CREATE TABLE OutputTable ... WITH ( 'connector' = ... )");

// 将查询结果输出到 OutputTable 中
tableEnv.executeSql (
"INSERT INTO OutputTable " +
 "SELECT user, url " +
 "FROM EventTable " +
 "WHERE user = 'Alice' "
 );
  1. 调用 Table API 进行查询

基于环境中已注册的表,可以通过表环境的 from()方法非常容易地得到一个 Table 对象:

Table eventTable = tableEnv.from("EventTable");

传入的参数就是注册好的表名。注意这里 eventTable 是一个 Table 对象,而 EventTable 是 在环境中注册的表名。得到 Table 对象之后,就可以调用 API 进行各种转换操作了,得到的是 一个新的 Table 对象:

Table maryClickTable = eventTable
    .where($("user").isEqual("Alice"))
    .select($("url"), $("user"));

主要还是以使用 SQL 为主。

11.2.5 输出表

表的创建和查询,就对应着流处理中的读取数据源(Source)和转换(Transform);而最后一个步骤 Sink,也就是将结果数据输出到外部系统,就对应着表的输出操作。

输出一张表最直接的方法,就是调用 Table 的方法 executeInsert()方法将一个 Table 写入到注册过的表中,方法传入的参数就是注册的表名。

// 注册表,用于输出数据到外部系统
tableEnv.executeSql("CREATE TABLE OutputTable ... WITH ( 'connector' = ... )");
// 经过查询转换,得到结果表
Table result = ...
// 将结果表写入已注册的输出表中
result.executeInsert("OutputTable");
11.2.6 表和流的转换

测试业务逻辑一般不会直接将结果直接 写入到外部系统,而是在本地控制台打印输出。对于 DataStream 这非常容易,直接调用 print() 方法就可以看到结果数据流的内容了,但是 Table 没有提供print()方法,所以在 Flink 中我们可以将 Table 再转换成 DataStream,然后进行打印输出。

  1. 将表(Table)转换成流(DataStream)

将一个 Table 对象转换成 DataStream ,只要直接调用表环境的方法 toDataStream() 就可以了。

Table aliceVisitTable = tableEnv.sqlQuery(
    "SELECT user, url " +
    "FROM EventTable " +
    "WHERE user = 'Alice' "
);
// 将表转换成数据流
tableEnv.toDataStream(aliceVisitTable).print();
  1. 将流(DataStream)转换成表(Table)

想要将一个 DataStream 转换成表,可以通过调用表环境的 fromDataStream()方法 来实现,返回的就是一个 Table 对象。

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 获取表环境
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 读取数据源
SingleOutputStreamOperator<Event> eventStream = env.addSource(...)
// 将数据流转换成表
Table eventTable = tableEnv.fromDataStream(eventStream);

由于流中的数据本身就是定义好的 POJO 类型 Event,所以我们将流转换成表之后,每一 行数据就对应着一个 Event,而表中的列名就对应着 Event 中的属性。

调用 fromDataStream()方法简单直观,可以直接实现 DataStream 到 Table 的转换;不过如 果我们希望直接在 SQL 中引用这张表,就还需要调用表环境的 createTemporaryView()方法来创建虚拟视图了。可以直接调用 createTemporaryView() 方法创建虚拟表,传入的两个参数,第一个依然是注册的表名,而第二个可以直接就是 DataStream。之后仍旧可以传入多个参数,用来指定表中的字段。

tableEnv.createTemporaryView("EventTable", eventStream, $("timestamp").as("ts"),$("url"));
  1. 综合应用示例
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;

public class TableToStreamExample {
    public static void main(String[] args) throws Exception {
        // 获取流环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        // 读取数据源
        SingleOutputStreamOperator<Event> eventStream = env
            .fromElements(
            new Event("Alice", "./home", 1000L),
            new Event("Bob", "./cart", 1000L),
            new Event("Alice", "./prod?id=1", 5 * 1000L),
            new Event("Cary", "./home", 60 * 1000L),
            new Event("Bob", "./prod?id=3", 90 * 1000L),
            new Event("Alice", "./prod?id=7", 105 * 1000L)
        );

        // 获取表环境
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

        // 将数据流转换成表
        tableEnv.createTemporaryView("EventTable", eventStream);


        // 查询Alice的访问url列表
        Table aliceVisitTable = tableEnv.sqlQuery("SELECT url, user FROM EventTable WHERE user = 'Alice'");

        // 统计每个用户的点击次数
        Table urlCountTable = tableEnv.sqlQuery("SELECT user, COUNT(url) FROM EventTable GROUP BY user");

        // 将表转换成数据流,在控制台打印输出
        tableEnv.toDataStream(aliceVisitTable).print("alice visit");
        tableEnv.toChangelogStream(urlCountTable).print("count");

        // 执行程序
        env.execute();
    }
}

数据的前缀出现了+I、-U 和+U 三种 RowKind,分别表示 INSERT(插入)、 UPDATE_BEFORE(更新前)和 UPDATE_AFTER(更新后)。

11.3 流处理中的表

关系型表/SQL 与流处理的对比:

关系型表/SQL流处理
处理的数据对象字段元组的有界集合字段元组的无限序列
查询(Query)可以访问到完整的数据输入无法访问到所有数据,必须“持续”等待流式输入
查询终止条件生成固定大小的结果集后终止永不停止,根据持续收到的数据不断更新查询结果

可以看到,关系型表和 SQL,主要就是针对批处理设计的,这和流处理有着天生的隔阂。 而Flink 中的 Table API 和 SQL 针对流处理有自己的处理方式。

11.3.1 动态表和持续查询
  1. 动态表(Dynamic Tables)

当流中有新数据到来,初始的表中会插入一行;而基于这个表定义的 SQL 查询,就应该 在之前的基础上更新结果。这样得到的表就会不断地动态变化,被称为“动态表”(Dynamic Tables)。

动态表是Flink在Table API和SQL中的核心概念,它为流数据处理提供了表和SQL支持。 我们所熟悉的表一般用来做批处理,面向的是固定的数据集,可以认为是“静态表”;而动态 表则完全不同,它里面的数据会随时间变化。

  1. 持续查询(Continuous Query)

动态表可以像静态的批处理表一样进行查询操作。由于数据在不断变化,因此基于它定义 的 SQL 查询也不可能执行一次就得到最终结果。这样一来,我们对动态表的查询也就永远不 会停止,一直在随着新数据的到来而继续执行。这样的查询就被称作“持续查询”(Continuous Query)。对动态表定义的查询操作,都是持续查询;而持续查询的结果也会是一个动态表。

流、动态表和持续 查询的关系:

11.3.2 将流转换成动态表

如果把流看作一张表,那么流中每个数据的到来,都应该看作是对表的一次插入(Insert) 操作,会在表的末尾添加一行数据。因为流是连续不断的,而且之前的输出结果无法改变、只 能在后面追加;所以我们其实是通过一个只有插入操作(insert-only)的更新日志(changelog) 流,来构建一个表。

11.2 综合示例中流转换成动态表的过程

11.3.2 用 SQL 持续查询
  1. 更新(Update)查询

在代码中定义了一个 SQL 查询。

Table urlCountTable = tableEnv.sqlQuery("SELECT user, COUNT(url) as cnt FROM EventTable GROUP BY user");

  1. 追加(Append)查询

上面的例子中,查询过程用到了分组聚合,结果表中就会产生更新操作。如果我们执行一 个简单的条件查询,结果表中就会像原始表 EventTable 一样,只有插入(Insert)操作了。

Table aliceVisitTable = tableEnv.sqlQuery("SELECT url, user FROM EventTable WHERE user = 'Cary'");

这样的持续查询,就被称为追加查询(Append Query),它定义的结果表的更新日志 (changelog)流中只有 INSERT 操作。只要用到了聚合,在之前的结果上有叠加,就会产生更新操作,就是一个更新查询。但事实上,更新查询的判断标准是结果表中的数据是否会 有 UPDATE 操作,如果聚合的结果不再改变,那么同样也不是更新查询。

11.3.3 将动态表转换为流

与关系型数据库中的表一样,动态表也可以通过插入(Insert)、更新(Update)和删除(Delete) 操作,进行持续的更改。将动态表转换为流或将其写入外部系统时,就需要对这些更改操作进 行编码,通过发送编码消息的方式告诉外部系统要执行的操作。在 Flink 中,Table API 和 SQL 支持三种编码方式:

  • 仅追加(Append-only)流
  • 撤回(Retract)流

用+代表 add 消息(对应插入 INSERT 操作),用-代表 retract 消息(对应删除 DELETE 操作)。

  • 更新插入(Upsert)流

所谓的“upsert”其实是“update”和“insert”的合成词,所以对于更新插入流来说,INSERT 插 入操作和UPDATE更新操作,统一被编码为upsert消息;而DELETE删除操作则被编码为delete 消息。

要动态表中必须有唯一的键(key)。通过这个 key 进行查询,如果存在对应的数据 就做更新(update),如果不存在就直接插入(insert)。这是一个动态表可以转换为更新插入流 的必要条件。

11.4 时间属性和窗口

11.4.1 事件时间

事件时间语义最大的用途就是处理乱序事件或者延迟事件的场景。我们通过设置水位线 (watermark)来表示事件时间的进展,而水位线可以根据数据的最大时间戳设置一个延迟时 间。这样即使在出现乱序的情况下,对数据的处理也可以获得正确的结果。

事件时间属性可以在创建表 DDL 中定义,也可以在数据流和表的转换中定义。

  1. 在创建表的 DDL 中定义

在创建表的 DDL(CREATE TABLE 语句)中,可以增加一个字段,通过 WATERMARK 语句来定义事件时间属性。

CREATE TABLE EventTable(
    user STRING,
    url STRING,
    ts TIMESTAMP(3),
    WATERMARK FOR ts AS ts - INTERVAL '5' SECOND
) WITH (
    ...
);
  1. 在数据流转换为表时定义

事件时间属性也可以在将 DataStream 转换为表的时候来定义。我们调用 fromDataStream() 方法创建表时,可以追加参数来定义表中的字段结构;这时可以给某个字段加上.rowtime() 后 缀,就表示将当前字段指定为事件时间属性。

// 方法一:
// 流中数据类型为二元组 Tuple2,包含两个字段;需要自定义提取时间戳并生成水位线
DataStream<Tuple2<String, String>> stream = inputStream.assignTimestampsAndWatermarks(...);
// 声明一个额外的逻辑字段作为事件时间属性
Table table = tEnv.fromDataStream(stream, $("user"), $("url"), $("ts").rowtime());
// 方法二:
// 流中数据类型为三元组 Tuple3,最后一个字段就是事件时间戳
DataStream<Tuple3<String, String, Long>> stream = inputStream.assignTimestampsAndWatermarks(...);
// 不再声明额外字段,直接用最后一个字段作为事件时间属性
Table table = tEnv.fromDataStream(stream, $("user"), $("url"), $("ts").rowtime());
11.4.2 处理时间
  1. 在创建表的 DDL 中定义

创建表的 DDL(CREATE TABLE 语句)中,可以增加一个额外的字段,通过调用系统 内置的 PROCTIME()函数来指定当前的处理时间属性,返回的类型是 TIMESTAMP_LTZ。

CREATE TABLE EventTable(
 user STRING,
 url STRING,
 ts AS PROCTIME()
) WITH (
 ...
);
  1. 在数据流转换为表时定义
DataStream<Tuple2<String, String>> stream = ...;
// 声明一个额外的字段作为处理时间属性字段
Table table = tEnv.fromDataStream(stream, $("user"), $("url"), 
$("ts").proctime());
11.4.3 窗口(Window)
  1. 分组窗口(Group Window,老版本)
  2. 窗口表值函数(Windowing TVFs,新版本)

目前 Flink (1.13版本)提供了以下几个窗口 TVF:

  • 滚动窗口(Tumbling Windows);

  • 滑动窗口(Hop Windows,跳跃窗口);

  • 累积窗口(Cumulate Windows);

  • 会话窗口(Session Windows,目前尚未完全支持)。

在 SQL 中的声明方式,与以前的分组窗口是类似的,直接调用 TUMBLE()、HOP()、 CUMULATE()就可以实现滚动、滑动和累积窗口,不过传入的参数会有所不同。

(1)滚动窗口(TUMBLE)

在 SQL 中通过调用 TUMBLE()函数就可以声明一个滚动窗口,只有一个核心参数就是窗 口大小(size)。

TUMBLE(TABLE EventTable, DESCRIPTOR(ts), INTERVAL '1' HOUR);

(2)滑动窗口(HOP)

在 SQL 中通过调用 HOP()来声明滑动窗口;除了也要传入表名、时间属性外,还需要传入窗口大小(size) 和滑动步长(slide)两个参数。

HOP(TABLE EventTable, DESCRIPTOR(ts), INTERVAL '5' MINUTES, INTERVAL '1' HOURS));

(3)累积窗口(CUMULATE)

累积窗口中有两个核心的参数:最大窗口长度(max window size)和累积步长(step)。所谓的最 大窗口长度其实就是我们所说的“统计周期”,最终目的就是统计这段时间内的数据。

CUMULATE(TABLE EventTable, DESCRIPTOR(ts), INTERVAL '1' HOURS, INTERVAL '1' DAYS))

11.5 聚合(Aggregation)查询

在 SQL 中,一个很常见的功能就是对某一列的多条数据做一个合并统计,得到一个或多 个结果值;比如求和、最大最小值、平均值等等,这种操作叫作聚合(Aggregation)查询。

11.5.1 分组聚合

从概念上讲,SQL 中的分组聚 合可以对应 DataStream API 中 keyBy 之后的聚合转换,它们都是按照某个 key 对数据进行了 划分,各自维护状态来进行聚合统计的。

SELECT user, COUNT(url) as cnt FROM EventTable GROUP BY user

在持续查询的过程中,由于用于分组的 key 可能会不断增加,因此计算结果所需要 维护的状态也会持续增长。为了防止状态无限增长耗尽资源,Flink Table API 和 SQL 可以在表 环境中配置状态的生存时间(TTL):

TableEnvironment tableEnv = ...
// 获取表环境的配置
TableConfig tableConfig = tableEnv.getConfig();
// 配置状态保持时间
tableConfig.setIdleStateRetention(Duration.ofMinutes(60));

或者也可以直接设置配置项 table.exec.state.ttl:

TableEnvironment tableEnv = ...
Configuration configuration = tableEnv.getConfig().getConfiguration();
configuration.setString("table.exec.state.ttl", "60 min");
11.5.2 窗口聚合

在 Flink 的 Table API 和 SQL 中,窗口的计算是通过“窗口聚合”(window aggregation) 来实现的。不过窗口聚合时,需要将窗口信息作 为分组 key 的一部分定义出来。在 Flink 1.12 版本之前,是直接把窗口自身作为分组 key 放在 GROUP BY 之后的,所以也叫“分组窗口聚合”;而 1.13 版本开始使用了 “窗口表值函数”(Windowing TVF),窗口本身返回的是就是一个表,所以窗口会出现在 FROM 后面,GROUP BY 后面的则是窗口新增的字段 window_start 和 window_end。

Table result = tableEnv.sqlQuery(
    "SELECT " +
    	"user, " +
    	"window_end AS endT, " +
   		"COUNT(url) AS cnt " +
    "FROM TABLE( " +
    	"TUMBLE( TABLE EventTable, " +
   		"DESCRIPTOR(ts), " +
    	"INTERVAL '1' HOUR)) " +
    "GROUP BY user, window_start, window_end "
);
11.5.3 开窗(Over)聚合

在标准 SQL 中还有另外一类比较特殊的聚合方式,可以针对每一行计算一个聚合值。比 如说,我们可以以每一行数据为基准,计算它之前 1 小时内所有数据的平均值;也可以计算它 之前 10 个数的平均值。就好像是在每一行上打开了一扇窗户、收集数据进行统计一样,这就 是所谓的“开窗函数”。

开窗聚合也叫作“OVER 聚合”(Over Aggregation)。基本语法如下:

SELECT
 <聚合函数> OVER (
     [PARTITION BY <字段 1>[, <字段 2>, ...]]
     ORDER BY <时间属性字段>
     <开窗范围>),
 ...
FROM ...
11.5.4 应用实例 —— Top N
  1. 普通 Top N

统计每个用户的访问事件中,按照字符长度排序的前两个 url:

SELECT user, url, ts, row_num
FROM (
    SELECT *,
    ROW_NUMBER() OVER (
        PARTITION BY user
        ORDER BY CHAR_LENGTH(url) desc 
    ) AS row_num
    FROM EventTable)
WHERE row_num <= 2
  1. 窗口 Top N

统计一段时间内的热门商品。这就需要先开窗口,在窗口中统计每个商品的点击量;然后将统计数据收集起来,按窗口进行分组,并按 点击量大小降序排序,选取前 N 个作为结果返回。

具体来说,可以先做一个窗口聚合,将窗口信息 window_start、window_end 连同每个商 品的点击量一并返回,这样就得到了聚合的结果表,包含了窗口信息、商品和统计的点击量。 接下来就可以像一般的 Top N 那样定义 OVER 窗口了,按窗口分组,按点击量排序,用 ROW_NUMBER()统计行号并筛选前 N 行就可以得到结果。所以窗口 Top N 的实现就是窗口 聚合与 OVER 聚合的结合使用。

下面是一个具体案例的代码实现。由于用户访问事件 Event 中没有商品相关信息,因此统计的是每小时内有最多访问行为的用户,取前两名,相当于是一个每小时活跃用户的查询。

import java.sql.Timestamp;

public class Event {
    public String user;
    public String url;
    public Long timestamp;

    public Event() {
    }

    public Event(String user, String url, Long timestamp) {
        this.user = user;
        this.url = url;
        this.timestamp = timestamp;
    }

    @Override
    public String toString() {
        return "Event{" +
                "user='" + user + '\'' +
                ", url='" + url + '\'' +
                ", timestamp=" + new Timestamp(timestamp) +
                '}';
    }
}
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import static org.apache.flink.table.api.Expressions.$;

public class WindowTopNExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        // 读取数据源,并分配时间戳、生成水位线
        SingleOutputStreamOperator<Event> eventStream = env
            .fromElements(
            new Event("Alice", "./home", 1000L),
            new Event("Bob", "./cart", 1000L),
            new Event("Alice", "./prod?id=1",  25 * 60 * 1000L),
            new Event("Alice", "./prod?id=4", 55 * 60 * 1000L),
            new Event("Bob", "./prod?id=5", 3600 * 1000L + 60 * 1000L),
            new Event("Cary", "./home", 3600 * 1000L + 30 * 60 * 1000L),
            new Event("Cary", "./prod?id=7", 3600 * 1000L + 59 * 60 * 1000L)
        )
            .assignTimestampsAndWatermarks(
            WatermarkStrategy.<Event>forMonotonousTimestamps()
            .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                @Override
                public long extractTimestamp(Event element, long recordTimestamp) {
                    return element.timestamp;
                }
            })
        );

        // 创建表环境
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

        // 将数据流转换成表,并指定时间属性
        Table eventTable = tableEnv.fromDataStream(
            eventStream,
            $("user"),
            $("url"),
            $("timestamp").rowtime().as("ts")
            // 将timestamp指定为事件时间,并命名为ts
        );

        // 为方便在SQL中引用,在环境中注册表EventTable
        tableEnv.createTemporaryView("EventTable", eventTable);

        // 定义子查询,进行窗口聚合,得到包含窗口信息、用户以及访问次数的结果表
        String subQuery =
            "SELECT window_start, window_end, user, COUNT(url) as cnt " +
            "FROM TABLE ( " +
            "TUMBLE( TABLE EventTable, DESCRIPTOR(ts), INTERVAL '1' HOUR )) " +
            "GROUP BY window_start, window_end, user ";

        // 定义Top N的外层查询
        String topNQuery =
            "SELECT * " +
            "FROM (" +
            "SELECT *, " +
            "ROW_NUMBER() OVER ( " +
            "PARTITION BY window_start, window_end " +
            "ORDER BY cnt desc " +
            ") AS row_num " +
            "FROM (" + subQuery + ")) " +
            "WHERE row_num <= 2";

        // 执行SQL得到结果表
        Table result = tableEnv.sqlQuery(topNQuery);

        tableEnv.toDataStream(result).print();

        env.execute();
    }
}

执行结果:

11.6 联结(Join)查询

在标准 SQL 中,可以将多个表连接合并起来,从中查询出想要的信息;这种操作就是表 的联结(Join)。在 Flink SQL 中,同样支持各种灵活的联结(Join)查询,操作的对象是动态表。

在流处理中,动态表的 Join 对应着两条数据流的 Join 操作。Flink SQL 中的联结查询大体上可以分为两类:SQL 原生的联结查询方式,和流处理中特有的联结查询。

11.6.1 常规联结查询
  1. 等值内联结(INNER Equi-JOIN)

内联结用 INNER JOIN 来定义,会返回两表中符合联接条件的所有行的组合,也就是所谓 的笛卡尔积(Cartesian product)。

SELECT *
FROM Order
INNER JOIN Product
ON Order.product_id = Product.id
  1. 等值外联结(OUTER Equi-JOIN)

与内联结类似,外联结也会返回符合联结条件的所有行的笛卡尔积;另外,还可以将某一 侧表中找不到任何匹配的行也单独返回。

SELECT *
FROM Order
LEFT JOIN Product
ON Order.product_id = Product.id
SELECT *
FROM Order
RIGHT JOIN Product
ON Order.product_id = Product.id
SELECT *
FROM Order
FULL OUTER JOIN Product
ON Order.product_id = Product.id
11.6.2 间隔联结查询

间隔联结(Interval Join)返回的,同样是符合约束条件的两条中数据的笛卡尔积。只不 过这里的“约束条件”除了常规的联结条件外,还多了一个时间间隔的限制。

间隔联结不需要用 JOIN 关键字,直接在 FROM 后将要联结的两表列出来就可以,用逗 号分隔。联结条件用 WHERE 子句来定义,用一个等值表达式描述。在 WHERE 子句中,联结条件后用 AND 追加一个时间间隔的限制条件。

SELECT *
FROM Order o, Shipment s
WHERE o.id = s.order_id
AND o.order_time BETWEEN s.ship_time - INTERVAL '4' HOUR AND s.ship_time

11.7 函数

在 SQL 中,我们可以把一些数据的转换操作包装起来,嵌入到 SQL 查询中统一调用,这 就是“函数”(functions)。

Table API 中 的函数是通过数据对象的方法调用来实现的;而 SQL 则是直接引用函数名称,传入数据作为 参数。

Flink SQL 中的函数可以分为两类:一类是 SQL 中内置的系统函数,而另一类函数则是用户自定义的函数(UDF),需要在表环境中注册才能使用。

11.7.1 系统函数

Flink SQL 中的系统函数又主要可以分为两大类:标量函数(Scalar Functions)和聚合函 数(Aggregate Functions)。

  1. 标量函数(Scalar Functions)
  • 比较函数(Comparison Functions)
  • 逻辑函数(Logical Functions)
  • 算术函数(Arithmetic Functions)
  • 字符串函数(String Functions)
  • 时间函数(Temporal Functions)
  1. 聚合函数(Aggregate Functions)

聚合函数是以表中多个行作为输入,提取字段进行聚合操作的函数,会将唯一的聚合值作 为结果返回。

  • COUNT(*) 返回所有行的数量,统计个数
  • SUM([ ALL | DISTINCT ] expression) 对某个字段进行求和操作。默认情况 下省略了关键字 ALL,表示对所有行求和;如果指定 DISTINCT,则会对数据进行去 重,每个值只叠加一次。
  • RANK() 返回当前值在一组值中的排名
  • ROW_NUMBER() 对一组值排序后,返回当前值的行号。与 RANK()的 功能相似
11.7.2 自定义函数(UDF)

Flink 的 Table API 和 SQL 提供了多种自定义函数的接口,以抽象类的形式定义。当前 UDF 主要有以下几类:

  • 标量函数(Scalar Functions):将输入的标量值转换成一个新的标量值;
  • 表函数(Table Functions):将标量值转换成一个或多个新的行数据,也就是扩展成一个表;
  • 聚合函数(Aggregate Functions):将多行数据里的标量值转换成一个新的标量值;
  • 表聚合函数(Table Aggregate Functions):将多行数据里的标量值转换成一 个或多个新的行数据。

整体调用流程:

要想在代码中使用自定义的函数,我们需要首先自定义对应 UDF 抽象类的实现,并在表 环境中注册这个函数,然后就可以在 Table API 和 SQL 中调用了。

(1)注册函数

注册函数时需要调用表环境的 createTemporarySystemFunction()方法,传入注册的函数名 以及 UDF 类的 Class 对象:

// 注册函数
tableEnv.createTemporarySystemFunction("MyFunction", MyFunction.class);

(2)使用 Table API 调用函数

在 Table API 中,需要使用 call()方法来调用自定义函数:

tableEnv.from("MyTable").select(call("MyFunction", $("myField")));

(3)在 SQL 中调用函数

当我们将函数注册为系统函数之后,在 SQL 中的调用就与内置系统函数完全一样了:

tableEnv.sqlQuery("SELECT MyFunction(myField) FROM MyTable");

11.8 SQL 客户端

有了 Table API 和 SQL,就可以使用熟悉的 SQL 来编写查询语句进行流处理了。不过,这种方式还是将 SQL 语句嵌入到 Java/Scala 代码中进行的;另外,写完的代码后想要提 交作业还需要使用工具进行打包。

基于这样的考虑,Flink 为我们提供了一个工具来进行 Flink 程序的编写、测试和提交,这 工具叫作“SQL 客户端”。SQL 客户端提供了一个命令行交互界面(CLI),我们可以在里面非 常容易地编写 SQL 进行查询,就像使用 MySQL 一样;整个 Flink 应用编写、提交的过程全变 成了写 SQL,不需要写一行 Java/Scala 代码。

具体使用流程如下:

(1)首先启动本地集群

./bin/start-cluster.sh

(2)启动 Flink SQL 客户端

./bin/sql-client.sh

SQL 客户端的启动脚本同样位于 Flink 的 bin 目录下。默认的启动模式是 embedded,也就 是说客户端是一个嵌入在本地的进程,这是目前唯一支持的模式。

(3)设置运行模式

首先是表环境的运行时模式,有流处理和批处理两个选项。默认为流处理:

Flink SQL> SET 'execution.runtime-mode' = 'streaming';

其次是 SQL 客户端的“执行结果模式”,主要有 table、changelog、tableau 三种,默认为 table 模式:

Flink SQL> SET 'sql-client.execution.result-mode' = 'table';

还可以做一些其它可选的设置,比如空闲状态生存时间(TTL):

Flink SQL> SET 'table.exec.state.ttl' = '1000';

(4)执行 SQL 查询

聚合:

Flink SQL> CREATE TABLE EventTable(
> user STRING,
> url STRING,
> `timestamp` BIGINT
> ) WITH (
> 'connector' = 'filesystem',
> 'path' = 'events.csv',
> 'format' = 'csv'
> );
Flink SQL> CREATE TABLE ResultTable (
> user STRING,
361
> cnt BIGINT
> ) WITH (
> 'connector' = 'print'
> );
Flink SQL> INSERT INTO ResultTable SELECT user, COUNT(url) as cnt FROM EventTable
GROUP BY user;

11.9 连接到外部系统

在 Table API 和 SQL 编写的 Flink 程序中,可以在创建表的时候用 WITH 子句指定连接器 (connector),这样就可以连接到外部系统进行数据交互了。

11.9.1 Kafka

Kafka 的 SQL 连接器可以从 Kafka 的主题(topic)读取数据转换成表,也可以将表数据 写入 Kafka 的主题。换句话说,创建表的时候指定连接器为 Kafka,则这个表既可以作为输入 表,也可以作为输出表。

  1. 引入依赖
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-kafka_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>

如果想在 SQL 客户端里使用 Kafka 连接器,还需要下载对应的 jar 包放到 lib 目录下。

另外,Flink 为各种连接器提供了一系列的“表格式”(table formats),比如 CSV、JSON、 Avro、Parquet 等等。根据 Kafka 连接器中配置的格式,我们可能需要引入对应的依赖支持。以 CSV 为例:

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-csv</artifactId>
    <version>${flink.version}</version>
</dependency>
  1. 创建连接到 Kafka 的表

创建一个连接到 Kafka 表,需要在 CREATE TABLE 的 DDL 中在 WITH 子句里指定连接 器为 Kafka,并定义必要的配置参数。

CREATE TABLE KafkaTable (
    `user` STRING,
    `url` STRING,
    `ts` TIMESTAMP(3) METADATA FROM 'timestamp'
) WITH (
    'connector' = 'kafka',
    'topic' = 'events',
    'properties.bootstrap.servers' = 'localhost:9092',
    'properties.group.id' = 'testGroup',
    'scan.startup.mode' = 'earliest-offset',
    'format' = 'csv'
)

这里定义了 Kafka 连接器对应的主题(topic),Kafka 服务器,消费者组 ID,消费者起始模式以及表格式。在 KafkaTable 的字段中有一个 ts,它的声明中用到了 METADATA FROM,这是表示一个“元数据列”(metadata column),它是由 Kafka 连接器的 元数据“timestamp”生成的。这里的 timestamp 其实就是 Kafka 中数据自带的时间戳,我们把 它直接作为元数据提取出来,转换成一个新的字段 ts。

  1. Upsert Kafka

Flink 专门增加了一个“更新插入 Kafka”(Upsert Kafka)连接器。这 个连接器支持以更新插入(UPSERT)的方式向 Kafka 的 topic 中读写数据。

CREATE TABLE pageviews_per_region (
    user_region STRING,
    pv BIGINT,
    uv BIGINT,
    PRIMARY KEY (user_region) NOT ENFORCED
) WITH (
    'connector' = 'upsert-kafka',
    'topic' = 'pageviews_per_region',
    'properties.bootstrap.servers' = '...',
    'key.format' = 'avro',
    'value.format' = 'avro'
);
CREATE TABLE pageviews (
    user_id BIGINT,
    page_id BIGINT,
    viewtime TIMESTAMP,
    user_region STRING,
    WATERMARK FOR viewtime AS viewtime - INTERVAL '2' SECOND
) WITH (
    'connector' = 'kafka',
    'topic' = 'pageviews',
    'properties.bootstrap.servers' = '...',
    'format' = 'json'
);
-- 计算 pv、uv 并插入到 upsert-kafka 表中
INSERT INTO pageviews_per_region
SELECT
 user_region,
 COUNT(*),
 COUNT(DISTINCT user_id)
FROM pageviews
GROUP BY user_region;
11.9.2 文件系统

Flink 提供了文件系统的连 接器,支持从本地或者分布式的文件系统中读写数据。这个连接器是内置在 Flink 中的,所以 使用它并不需要额外引入依赖。

CREATE TABLE MyTable (
    column_name1 INT,
    column_name2 STRING,
    ...
    part_name1 INT,
    part_name2 STRING
) PARTITIONED BY (part_name1, part_name2) WITH (
    'connector' = 'filesystem', -- 连接器类型
    'path' = '...', -- 文件路径
    'format' = '...' -- 文件格式
)
11.9.3 JDBC

Flink 提供的 JDBC 连接器可以通过 JDBC 驱动程序(driver)向任意的关系型 数据库读写数据,比如 MySQL、PostgreSQL、Derby 等。

为 TableSink 向数据库写入数据时,运行的模式取决于创建表的 DDL 是否定义了主键 (primary key)。如果有主键,那么 JDBC 连接器就将以更新插入(Upsert)模式运行,可以向 外部数据库发送按照指定键(key)的更新(UPDATE)和删除(DELETE)操作;如果没有 定义主键,那么就将在追加(Append)模式下运行,不支持更新和删除操作。

  1. 引入依赖
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-jdbc_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>

此外,为了连接到特定的数据库,还应引入相关的驱动器依赖,比如 MySQL:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.38</version>
</dependency>
  1. 创建 JDBC 表
-- 创建一张连接到 MySQL 的 表
CREATE TABLE MyTable (
    id BIGINT,
    name STRING,
    age INT,
    status BOOLEAN,
    PRIMARY KEY (id) NOT ENFORCED
) WITH (
    'connector' = 'jdbc',
    'url' = 'jdbc:mysql://localhost:3306/mydatabase',
    'table-name' = 'users'
);
-- 将另一张表 T 的数据写入到 MyTable 表中
INSERT INTO MyTable
SELECT id, name, age, status FROM T;
11.9.4 Elasticsearch

Flink 提供的 Elasticsearch的SQL连接器只能作为TableSink,可以将表数据写入Elasticsearch的索引(index)。 Elasticsearch 连接器的使用与 JDBC 连接器非常相似,写入数据的模式同样是由创建表的 DDL 中是否有主键定义决定的。

  1. 引入依赖

具体的依赖与 Elasticsearch 服务器的版本有关,对于 6.x 版本引入依赖如下:

<dependency>
    <groupId>org.apache.flink</groupId> 
    <artifactId>flink-connector-elasticsearch6_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>

对于 Elasticsearch 7 以上的版本,引入的依赖则是:

<dependency>
    <groupId>org.apache.flink</groupId> 
    <artifactId>flink-connector-elasticsearch7_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>
  1. 创建连接到 Elasticsearch 的表
-- 创建一张连接到 Elasticsearch 的 表
CREATE TABLE MyTable (
    user_id STRING,
    user_name STRING
    uv BIGINT,
    pv BIGINT,
    PRIMARY KEY (user_id) NOT ENFORCED
) WITH (
    'connector' = 'elasticsearch-7',
    'hosts' = 'http://localhost:9200',
    'index' = 'users'
);
11.9.5 HBase

在流处理场景下,连接器作为 TableSink 向 HBase 写入数据时,采用的始终是更新插入 (Upsert)模式。也就是说,HBase 要求连接器必须通过定义的主键(primary key)来发送更 新日志(changelog)。所以在创建表的 DDL 中,我们必须要定义行键(rowkey)字段,并将 它声明为主键;如果没有用 PRIMARY KEY 子句声明主键,连接器会默认把 rowkey 作为主键。

  1. 引入依赖

目前 Flink 只对 HBase 的 1.4.x 和 2.2.x 版本提供了连接器支持,而引入的依赖也应该与具体的 HBase 版本有关。对于 1.4 版本引入依赖如下:

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-hbase-1.4_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>

对于 HBase 2.2 版本,引入的依赖则是:

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-hbase-2.2_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>
  1. 创建连接到 HBase 的表

在 DDL 创建出的 HBase 表中,所有的列族(column family)都必须声明为 ROW 类型,在表中占据一 个字段;而每个 family 中的列(column qualifier)则对应着 ROW 里的嵌套字段。

-- 创建一张连接到 HBase 的 表
CREATE TABLE MyTable (
    rowkey INT,
    family1 ROW<q1 INT>,
    family2 ROW<q2 STRING, q3 BIGINT>,
    family3 ROW<q4 DOUBLE, q5 BOOLEAN, q6 STRING>,
    PRIMARY KEY (rowkey) NOT ENFORCED
) WITH (
    'connector' = 'hbase-1.4',
    'table-name' = 'mytable',
    'zookeeper.quorum' = 'localhost:2181'
);
368
-- 假设表 T 的字段结构是 [rowkey, f1q1, f2q2, f2q3, f3q4, f3q5, f3q6]
INSERT INTO MyTable
SELECT rowkey, ROW(f1q1), ROW(f2q2, f2q3), ROW(f3q4, f3q5, f3q6) FROM T;
11.9.6 Hive

Flink 提供了“Hive 目录”(HiveCatalog)功能,允许使用 Hive 的“元存储”(Metastore)来管理 Flink 的元数据。这带来的好处体现在两个方面:

(1)Metastore 可以作为一个持久化的目录,因此使用 HiveCatalog 可以跨会话存储 Flink 特定的元数据。这样一来,我们在 HiveCatalog 中执行执行创建 Kafka 表或者 ElasticSearch 表, 就可以把它们的元数据持久化存储在 Hive 的 Metastore 中;对于不同的作业会话就不需要重复 创建了,直接在 SQL 查询中重用就可以。

(2)使用 HiveCatalog,Flink 可以作为读写 Hive 表的替代分析引擎。这样一来,在 Hive 中进行批处理会更加高效;与此同时,也有了连续在 Hive 中读写数据、进行流处理的能力, 这也使得“实时数仓”(real-time data warehouse)成为了可能。

只有 Blink 的计划器(planner)提供了 Hive 集成的支持, 所以需要在使用 Flink SQL时选择Blink planner。

  1. 引入依赖

目前 Flink 支持的 Hive 版本包括:

  • Hive 1.x:1.0.0~1.2.2;
  • Hive 2.x:2.0.02.2.0,2.3.02.3.6;
  • Hive 3.x:3.0.0~3.1.2;
<!-- Flink 的 Hive 连接器-->
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-hive_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>
<!-- Hive 依赖 -->
<dependency>
    <groupId>org.apache.hive</groupId>
    <artifactId>hive-exec</artifactId>
    <version>${hive.version}</version>
</dependency
  1. 连接到 Hive

在 Flink 中连接 Hive,是通过在表环境中配置 HiveCatalog 来实现的。需要说明的是,配 置 HiveCatalog 本身并不需要限定使用哪个 planner,不过对 Hive 表的读写操作只有 Blink 的 planner 才支持。所以一般需要将表环境的 planner 设置为 Blink。

EnvironmentSettings settings = EnvironmentSettings.newInstance().useBlinkPlanner().build();
TableEnvironment tableEnv = TableEnvironment.create(settings);
String name = "myhive";
String defaultDatabase = "mydatabase";
String hiveConfDir = "/opt/hive-conf";
// 创建一个 HiveCatalog,并在表环境中注册
HiveCatalog hive = new HiveCatalog(name, defaultDatabase, hiveConfDir);
tableEnv.registerCatalog("myhive", hive);
// 使用 HiveCatalog 作为当前会话的 catalog
tableEnv.useCatalog("myhive");

也可以直接启动 SQL 客户端,用 CREATE CATALOG 语句直接创建 HiveCatalog:

Flink SQL> create catalog myhive with ('type' = 'hive', 'hive-conf-dir' = '/opt/hive-conf');
Flink SQL> use catalog myhive;
  1. 设置 SQL 方言

Hive内部提供了类SQL的查询语言,不过语法细节与标准SQL会有一些出入, 相当于是 SQL 的一种“方言”(dialect)。为了提高与 Hive 集成时的兼容性,Flink SQL 提供了 一个非常有趣而强大的功能:可以使用方言来编写 SQL 语句。换句话说,我们可以直接在 Flink 中写 Hive SQL 来操作 Hive 表。

Flink 目前支持两种 SQL 方言的配置:default 和 hive。所谓的 default 就是 Flink SQL 默认 的 SQL 语法了。我们需要先切换到 hive 方言,然后才能使用 Hive SQL 的语法。具体设置可 以分为 SQL 和 Table API 两种方式。

(1)SQL 中设置

可以通过配置 table.sql-dialect 属性来设置 SQL 方言:

set table.sql-dialect=hive;

如 果使用 SQL 客户端,我们还可以在配置文件 sql-cli-defaults.yaml 中通过“configuration”模块来 设置:

execution:
	planner: blink
	type: batch
	result-mode: table
configuration:
table.sql-dialect: hiv

(2)Table API 中设置

// 配置 hive 方言
tableEnv.getConfig().setSqlDialect(SqlDialect.HIVE);
// 配置 default 方言
tableEnv.getConfig().setSqlDialect(SqlDialect.DEFAULT);
  1. 读写 Hive 表

有了 SQL 方言的设置,我们就可以很方便的在 Flink 中创建 Hive 表并进行读写操作了。 Flink 支持以批处理和流处理模式向 Hive 中读写数据。更灵活的是,我们可以随时切换 SQL 方言,从其它数据源(例如 Kafka)读取数据、经 转换后再写入Hive。下面是以纯SQL形式编写的一个示例:

-- 设置 SQL 方言为 hive,创建 Hive 表
SET table.sql-dialect=hive;
CREATE TABLE hive_table (
    user_id STRING,
    order_amount DOUBLE
) PARTITIONED BY (dt STRING, hr STRING) STORED AS parquet TBLPROPERTIES (
    'partition.time-extractor.timestamp-pattern'='$dt $hr:00:00',
    'sink.partition-commit.trigger'='partition-time',
    'sink.partition-commit.delay'='1 h',
    'sink.partition-commit.policy.kind'='metastore,success-file'
);
-- 设置 SQL 方言为 default,创建 Kafka 表
SET table.sql-dialect=default;
CREATE TABLE kafka_table (
    user_id STRING,
    order_amount DOUBLE,
    log_ts TIMESTAMP(3),
    WATERMARK FOR log_ts AS log_ts - INTERVAL '5' SECOND – 定义水位线
) WITH (...);
-- 将 Kafka 中读取的数据经转换后写入 Hive 
INSERT INTO TABLE hive_table 
SELECT user_id, order_amount, DATE_FORMAT(log_ts, 'yyyy-MM-dd'), 
DATE_FORMAT(log_ts, 'HH')
FROM kafka_table;
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

半岛铁子_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值