Flink中Table API和SQL(一)

目录

十一:Table API和SQL

11.1 快速上手

11.1.1 需要引入的依赖

11.1.2 一个简单示例

11.2 基本 API

11.2.1 程序架构

11.2.2 创建表环境

11.2.3 创建表

11.2.4 表的查询


十一:Table API和SQL

在 Flink 提供的多层级 API 中,核心是 DataStream API,这是我们开发流 处理应用的基本途径;底层则是所谓的处理函数(process function),可以访问事件的时间信 息、注册定时器、自定义状态,进行有状态的流处理。DataStream API 和处理函数比较通用, 有了这些 API,理论上我们就可以实现所有场景的需求了。

不过在企业实际应用中,往往会面对大量类似的处理逻辑,所以一般会将底层 API 包装 成更加具体的应用级接口。怎样的接口风格最容易让大家接收呢?作为大数据工程师,我们最 为熟悉的数据统计方式,当然就是写 SQL 了。 SQL 是结构化查询语言(Structured Query Language)的缩写,是我们对关系型数据库进 行查询和修改的通用编程语言。在关系型数据库中,数据是以表(table)的形式组织起来的, 所以也可以认为 SQL 是用来对表进行处理的工具语言。无论是传统架构中进行数据存储的MySQL、PostgreSQL,还是大数据应用中的 Hive,都少不了 SQL 的身影;而 Spark 作为大数 据处理引擎,为了更好地支持在 Hive 中的 SQL 查询,也提供了 Spark SQL 作为入口。 Flink 同样提供了对于“表”处理的支持,这就是更高层级的应用 API,在 Flink 中被称为Table API 和 SQL。

Table API 顾名思义,就是基于“表”(Table)的一套 API,它是内嵌在 Java、Scala 等语言中的一种声明式领域特定语言(DSL),也就是专门为处理表而设计的;在此基础 上,Flink 还基于 Apache Calcite 实现了对 SQL 的支持。这样一来,我们就可以在 Flink 程序中 直接写 SQL 来实现处理需求了

11.1 快速上手

如果我们对关系型数据库和 SQL 非常熟悉,那么 Table API 和 SQL 的使用其实非常简单: 只要得到一个“表”(Table),然后对它调用 Table API,或者直接写 SQL 就可以了。接下来我 们就以一个非常简单的例子上手,初步了解一下这种高层级 API 的使用方法。

11.1.1 需要引入的依赖

我们想要在代码中使用 Table API,必须引入相关的依赖。

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

这里的依赖是一个 Java 的“桥接器”(bridge),主要就是负责 Table API 和下层 DataStream API 的连接支持,按照不同的语言分为 Java 版和 Scala 版。

如果我们希望在本地的集成开发环境(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> 

这里主要添加的依赖是一个“计划器”(planner),它是 Table API 的核心组件,负责提供 运行时环境,并生成程序的执行计划。这里我们用到的是新版的 blink planner。由于 Flink 安 装包的 lib 目录下会自带 planner,所以在生产集群环境中提交的作业不需要打包这个依赖。而在 Table API 的内部实现上,部分相关的代码是用 Scala 实现的,所以还需要额外添加 一个 Scala 版流处理的相关依赖。

另外,如果想实现自定义的数据格式来做序列化,可以引入下面的依赖:

<dependency> 
 <groupId>org.apache.flink</groupId> 
 <artifactId>flink-table-common</artifactId> 
 <version>${flink.version}</version> 
</dependency> 

11.1.2 一个简单示例

提取Event类型数据源中的user和url

代码实现:

package com.atguigu.chapter11;

import com.atguigu.chapter05.ClickSource;
import com.atguigu.chapter05.Event;
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 java.time.Duration;

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

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

        //2.读取数据源
        SingleOutputStreamOperator<Event> eventStream = env.addSource(new ClickSource())
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event event, long l) {
                                return event.timestamp;
                            }
                        }));
        //3.获取表环境
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

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

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

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

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

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

代码执行的结果:

+I[./home, Alice] 
+I[./cart, Bob] 
+I[./prod?id=1, Alice] 
+I[./home, Cary] 
+I[./prod?id=3, Bob] 
+I[./prod?id=7, Alice] 

可以看到,我们将原始的 Event 数据转换成了(url,user)这样类似二元组的类型。每行输出前面有一个“+I”标志,这是表示每条数据都是“插入”(Insert)到表中的新增数据。 Table 是 Table API 中的核心接口类,对应着我们熟悉的“表”的概念。基于 Table 我们也 可以调用一系列查询方法直接进行转换,这就是所谓 Table API 的处理方式:

// 用 Table API 方式提取数据 
Table clickTable2 = eventTable.select($("url"), $("user")); 

这里的$符号是 Table API 中定义的“表达式”类 Expressions 中的一个方法,传入一个字段名称,就可以指代数据中对应字段。将得到的表转换成流打印输出,会发现结果与直接执行SQL 完全一样。

11.2 基本 API

11.2.1 程序架构

在 Flink 中,Table API 和 SQL 可以看作联结在一起的一套 API,这套 API 的核心概念就 是“表”(Table)。在我们的程序中,输入数据可以定义成一张表;然后对这张表进行查询, 就可以得到新的表,这相当于就是流数据的转换操作;最后还可以定义一张用于输出的表,负责将处理结果写入到外部系统。

我们可以看到,程序的整体处理流程与 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"); 

与上一节中不同,这里不是从一个 DataStream 转换成 Table,而是通过执行 DDL 来直接创建一个表。这里执行的 CREATE 语句中用 WITH 指定了外部系统的连接器,于是就可以连 接外部系统读取数据了。这其实是更加一般化的程序架构,因为这样我们就可以完全抛开DataStream API,直接用 SQL 语句实现全部的流处理过程。

而后面对于输出表的定义是完全一样的。可以发现,在创建表的过程中,其实并不区分“输入”还是“输出”,只需要将这个表“注册”进来、连接到外部系统就可以了;这里的 inputTable、outputTable 只是注册的表名,并不代表处理逻辑,可以随意更换。至于表的具体作用,则要 等到执行后面的查询转换操作时才能明确。我们直接从 inputTable 中查询数据,那么 inputTable就是输入表;而 outputTable 会接收另外表的结果进行写入,那么就是输出表

11.2.2 创建表环境

对于 Flink 这样的流处理框架来说,数据流和表在结构上还是有所区别的。所以使用 Table API 和 SQL 需要一个特别的运行时环境,这就是所谓的“表环境”(TableEnvironment)。它主要负责:

(1)注册 Catalog 和表;

(2)执行 SQL 查询;

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

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

这里的 Catalog 就是“目录”,与标准 SQL 中的概念是一致的,主要用来管理所有数据库 (database)和表(table)的元数据(metadata)。通过 Catalog 可以方便地对数据库和表进行 查询的管理,所以可以认为我们所定义的表都会“挂靠”在某个目录下,这样就可以快速检索。 在表环境中可以由用户自定义 Catalog,并在其中注册表和自定义函数(UDF)。默认的 Catalog就叫作 default_catalog。

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

/*
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

         */

        //1.定义环境配置来创建表执行环境,这样创建的话,不需要依赖流执行环境
        
        //1.1 基于blink版本planner进行流处理
        EnvironmentSettings settings = EnvironmentSettings.newInstance()
                .inStreamingMode()
                .useBlinkPlanner()
                .build();

        TableEnvironment tableEnv = TableEnvironment.create(settings);
        
        //1.1 基于老版本计划器planner进行流处理
        EnvironmentSettings settings1 = EnvironmentSettings.newInstance()
                .inStreamingMode()
                .useOldPlanner()
                .build();

        TableEnvironment tableEnv1 = TableEnvironment.create(settings);
        
        //1.2 基于老版本计划器planner进行批处理
        ExecutionEnvironment batchEnv=ExecutionEnvironment.getExecutionEnvironment();
        BatchTableEnvironment tableEnv2 = BatchTableEnvironment.create(batchEnv);

        //1.3 基于blink版本planner进行批处理
        EnvironmentSettings settings3 = EnvironmentSettings.newInstance()
                .inBatchMode()
                .useBlinkPlanner()
                .build();
        TableEnvironment tableEnv3=TableEnvironment.create(settings3);

11.2.3 创建表

表(Table)是我们非常熟悉的一个概念,它是关系型数据库中数据存储的基本形式,也是SQL 执行的基本对象。Flink 中的表概念也并不特殊,是由多个“行”数据构成的,每个行(Row) 又可以有定义好的多个列(Column)字段;整体来看,表就是固定类型的数据组成的二维矩阵

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

default_catalog.default_database.MyTable 

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

1. 连接器表(Connector Tables)

最直观的创建表的方式,就是通过连接器(connector)连接到一个外部系统,然后定义出对应的表结构。例如我们可以连接到 Kafka 或者文件系统,将存储在这些外部系统的数据以“表” 的形式定义出来,这样对表的读写就可以通过连接器转换成对外部系统的读写了。当我们在表环境中读取这张表,连接器就会从外部系统读取数据并进行转换;而当我们向这张表写入数据, 连接器就会将数据输出(Sink)到外部系统中

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

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

这里没有定义 Catalog 和 Database , 所 以 都 是 默 认 的 , 表 的 完 整 ID 就 是default_catalog.default_database.MyTable。如果希望使用自定义的目录名和库名,可以在环境中 进行设置:

tEnv.useCatalog("custom_catalog"); 
tEnv.useDatabase("custom_database");

这样我们创建的表完整 ID就变成了 custom_catalog.custom_database.MyTable。

之后在表环境中创建的所有表,ID也会都以 custom_catalog.custom_database 作为前缀。

2. 虚拟表(Virtual Tables)

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

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

这里调用了表环境的 sqlQuery()方法,直接传入一条 SQL 语句作为参数执行查询,得到的结果是一个 Table 对象。Table 是 Table API 中提供的核心接口类,就代表了一个 Java 中定义的 表实例。

得到的 newTable 是一个中间转换结果,如果之后又希望直接使用这个表执行 SQL,又该 怎么做呢?由于 newTable 是一个 Table 对象,并没有在表环境中注册;所以我们还需要将这 个中间结果表注册到环境中,才能在 SQL 中使用:

tableEnv.createTemporaryView("NewTable", newTable);

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

注册为虚拟表之后,我们就又可以在 SQL 中直接使用 NewTable 进行查询转换了。不难看 到,通过虚拟表可以非常方便地让 SQL 分步骤执行得到中间结果,这为代码编写提供了很大 的便利.

另外,虚拟表也可以让我们在 Table API 和 SQL 之间进行自由切换。一个 Java 中的 Table对象可以直接调用 Table API 中定义好的查询转换方法,得到一个中间结果表;这跟对注册好的表直接执行 SQL 结果是一样的。

11.2.4 表的查询

创建好了表,接下来自然就是对表进行查询转换了。对一个表的查询(Query)操作,就 对应着流数据的转换(Transform)处理。 Flink 为我们提供了两种查询方式:SQL 和 Table API。

1. 执行 SQL 进行查询

基于表执行 SQL 语句,是我们最为熟悉的查询方式。Flink 基于 Apache Calcite 来提供对SQL 的支持,Calcite 是一个为不同的计算平台提供标准 SQL 查询的底层工具,很多大数据框 架比如 Apache Hive、Apache Kylin 中的 SQL 支持都是通过集成 Calcite 来实现的。

在代码中,我们只要调用表环境的 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 实现自己的处理需 求,从而大大降低了 Flink 上手的难度。

例如,我们也可以通过 GROUP BY 关键字定义分组聚合,调用 COUNT()、SUM()这样的 函数来进行统计计算:

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' " 
 ); 

2. 调用 Table API 进行查询

另外一种查询方式就是调用 Table API。这是嵌入在 Java 和 Scala 语言内的查询 API,核 心就是 Table 接口类,通过一步步链式调用 Table 的方法,就可以定义出所有的查询转换操作。 每一步方法调用的返回结果,都是一个 Table。

由于Table API是基于Table的Java实例进行调用的,因此我们首先要得到表的Java对象。 基于环境中已注册的表,可以通过表环境的 from()方法非常容易地得到一个 Table 对象:

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

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

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

这里每个方法的参数都是一个“表达式”(Expression),用方法调用的形式直观地说明了 想要表达的内容;“$”符号用来指定表中的一个字段。上面的代码和直接执行 SQL 是等效的。 Table API 是嵌入编程语言中的 DSL,SQL 中的很多特性和功能必须要有对应的实现才可 以使用,因此跟直接写 SQL 比起来肯定就要麻烦一些。目前 Table API 支持的功能相对更少, 可以预见未来 Flink 社区也会以扩展 SQL 为主,为大家提供更加通用的接口方式;所以我们 接下来也会以介绍 SQL 为主,简略地提及 Table API。

3. 两种 API 的结合使用

可以发现,无论是调用 Table API 还是执行 SQL,得到的结果都是一个 Table 对象;所以 这两种 API 的查询可以很方便地结合在一起。

(1)无论是那种方式得到的 Table 对象,都可以继续调用 Table API 进行查询转换;

(2)如果想要对一个表执行 SQL 操作(用 FROM 关键字引用),必须先在环境中对它进 行注册。

所以我们可以通过创建虚拟表的方式实现两者的转换:

tableEnv.createTemporaryView("MyTable", myTable); 

注意:这里的第一个参数"MyTable"是注册的表名,而第二个参数 myTable 是 Java 中的Table 对象。

另外要说明的是,我们并没有将 Table 对象注册为虚拟表就直接在 SQL 中使用了:

Table clickTable = tableEnvironment.sqlQuery("select url, user from " + 
eventTable); 

这其实是一种简略的写法,我们将 Table 对象名 eventTable 直接以字符串拼接的形式添加 到 SQL 语句中,在解析时会自动注册一个同名的虚拟表到环境中,这样就省略了创建虚拟视 图的步骤。

两种 API 殊途同归,实际应用中可以按照自己的习惯任意选择。不过由于结合使用容易 引起混淆,而 Table API 功能相对较少、通用性较差,所以企业项目中往往会直接选择 SQL 的 方式来实现需求。

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值