Flink Table API 与 SQL概念(一)

重点:

  • table api、sql api 可以与flink datastream api进行无缝切换
  • 下图是flink 所有api的抽象级别。dataSet API概念会见见淡化,开发中不要使用

Flink Table API 与 SQL概念(一)_第1张图片

  • DataStream 和 Table 之间的转换(在 StreamTableEnvironment 的情况下)

概览(Overview)

Apache Flink 具有两个关系 API - Table API 和 SQL - 用于统一流和批处理。 Table API 是用于 Java、Scala 和 Python 的语言集成查询 API,它允许以非常直观的方式组合来自关系运算符(如选择、过滤和连接)的查询。 Flink 的 SQL 支持基于实现 SQL 标准的 Apache Calcite。 无论输入是连续的(流式传输)还是有界的(批处理),任一接口中指定的查询都具有相同的语义并指定相同的结果。

Table API 和 SQL 接口与 Flink 的 DataStream API 无缝集成。 您可以轻松地在所有 API 和基于它们的库之间切换。 例如,您可以使用 MATCH_RECOGNIZE 子句从表中检测模式,然后使用 DataStream API 根据匹配的模式构建警报。

Flink Table程序的Maven依赖

Java Maven依赖:


  org.apache.flink
  flink-table-api-java-bridge_2.11
  1.14.4
  provided

此外,如果您想在 IDE 中本地运行 Table API 和 SQL 程序,则必须添加以下依赖:


  org.apache.flink
  flink-table-planner_2.11
  1.14.4
  provided


  org.apache.flink
  flink-streaming-scala_2.11
  1.14.4
  provided

扩展依赖

如果您想为(反)序列化行或一组用户定义的函数实现自定义格式或连接器,则以下依赖项就足够了,并且可用于 SQL 客户端的 JAR 文件:


  org.apache.flink
  flink-table-common
  1.14.4
  provided

概念&通用API

Table API 和 SQL 集成在一个联合 API 中。 这个 API 的中心概念是一个用作查询输入和输出的表。 本文档展示了使用 Table API 和 SQL 查询的程序的常见结构,如何注册 Table,如何查询 Table,以及如何发出 Table。

Table API 和 SQL 程序的结构 


以下代码示例显示了 Table API 和 SQL 程序的常见结构。

package org.galaxy.foundation.common.batch;

import org.apache.flink.connector.datagen.table.DataGenConnectorOptions;
import org.apache.flink.table.api.*;


/**
 * @author test
 */
public class FlinkTableTest {

    public static void main(String[] args) throws Exception {
        EnvironmentSettings settings = EnvironmentSettings.inStreamingMode();

        //创建table的执行环境
        TableEnvironment tableEnv = TableEnvironment.create(settings);


        // Create a source table
        tableEnv.createTemporaryTable("SourceTable", TableDescriptor.forConnector("datagen")
                .schema(Schema.newBuilder()
                        .column("f0", DataTypes.STRING())
                        .build())
                .option(DataGenConnectorOptions.ROWS_PER_SECOND, 2L)
                .build());


        //使用SQL DDL创建一个新的sink表
        tableEnv.executeSql("CREATE TEMPORARY TABLE SinkTable WITH ('connector' = 'blackhole') LIKE SourceTable");

        // 从Table API 中创建一个table对象
        Table table2 = tableEnv.from("SourceTable");

        // 从查询语句中创建一个table对象
        Table table3 = tableEnv.sqlQuery("SELECT * FROM SourceTable");


        //将 Table API 结果表发送到 TableSink,SQL 结果相同
        TableResult tableResult = table2.executeInsert("SinkTable");


    }
}

表 API 和 SQL 查询可以轻松地与 DataStream 程序集成并嵌入到其中。 查看 DataStream API 集成页面,了解如何将 DataStreams 转换为表,反之亦然。

创建一个 TableEnvironment 


TableEnvironment 是 Table API 和 SQL 集成的入口点,负责:

在内部catalog中注册表
注册catalogs
加载可插拔模块
执行 SQL 查询
注册用户定义的(标量、表或聚合)函数
DataStream 和 Table 之间的转换(在 StreamTableEnvironment 的情况下)
一个 Table 总是绑定到一个特定的 TableEnvironment。 不能在同一个查询中组合不同 TableEnvironments 的表,例如加入或联合它们。 通过调用静态 TableEnvironment.create() 方法创建 TableEnvironment。

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

EnvironmentSettings settings = EnvironmentSettings
    .newInstance()
    .inStreamingMode()
    //.inBatchMode()
    .build();

TableEnvironment tEnv = TableEnvironment.create(settings);

或者,用户可以从现有的 StreamExecutionEnvironment 创建 StreamTableEnvironment 以与 DataStream API 进行互操作。

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 tEnv = StreamTableEnvironment.create(env);

在目录中创建表 


TableEnvironment 维护使用标识符创建的表catalogs的映射。 每个标识符由 3 部分组成:目录名称、数据库名称和对象名称。 如果未指定目录或数据库,则将使用当前默认值(请参阅表标识符扩展部分中的示例)。

表可以是虚拟的 (VIEWS) 或常规的 (TABLES)。 可以从现有的 Table 对象创建 VIEWS,通常是 Table API 或 SQL 查询的结果。 TABLES 描述外部数据,例如文件、数据库表或消息队列。

临时表与永久表


表可以是临时的,并且与单个 Flink 会话的生命周期相关联,也可以是永久的,并且在多个 Flink 会话和集群中可见。

永久表需要一个目录(例如 Hive Metastore)来维护有关表的元数据。 一旦创建了永久表,它对连接到目录的任何 Flink 会话都是可见的,并且将继续存在,直到表被显式删除。

另一方面,临时表始终存储在内存中,并且仅在它们创建的 Flink 会话期间存在。 这些表对其他会话不可见。 它们不绑定到任何目录或数据库,但可以在其中一个的命名空间中创建。 如果删除了相应的数据库(这里应该指的是永久表的数据库),则不会删除临时表。

阴影(是一个开发阶段的好手段)
可以使用与现有永久表相同的标识符注册临时表。 临时表会影响永久表,只要临时表存在,就无法访问永久表。 所有具有该标识符的查询都将针对临时表执行。

这可能对实验有用。 它允许首先针对临时表运行完全相同的查询,例如 只有一个数据子集,或者数据被混淆了。 一旦验证查询是正确的,它就可以针对真实的生产表运行。

创建表


虚拟表 


Table API 对象对应于 SQL 术语中的 VIEW(虚拟表)。 它封装了一个逻辑查询计划。 它可以在目录中创建,如下所示:

// get a TableEnvironment
TableEnvironment tableEnv = ...; // see "Create a TableEnvironment" section

// table is the result of a simple projection query 
Table projTable = tableEnv.from("X").select(...);

// register the Table projTable as table "projectedTable"
tableEnv.createTemporaryView("projectedTable", projTable);

注意:表对象类似于关系数据库系统中的 VIEW,即定义表的查询未优化,但当另一个查询引用已注册的表时将被内联。 如果多个查询引用同一个注册表,每个引用查询都会被内联并执行多次,即注册表的结果不会被共享。(michael:是不是表示,如果再次创建一个createTemporaryView,上面的select还要执行一次?)

连接器表 #
也可以从连接器声明中创建从关系数据库已知的 TABLE。 连接器描述了存储表数据的外部系统。 可以在此处声明存储系统,例如 Apache Kafka 或常规文件系统。

此类表可以直接使用 Table API 创建,也可以通过切换到 SQL DDL 创建。

// Using table descriptors
final TableDescriptor sourceDescriptor = TableDescriptor.forConnector("datagen")
    .schema(Schema.newBuilder()
    .column("f0", DataTypes.STRING())
    .build())
    .option(DataGenOptions.ROWS_PER_SECOND, 100)
    .build();

tableEnv.createTable("SourceTableA", sourceDescriptor);
tableEnv.createTemporaryTable("SourceTableB", sourceDescriptor);

// Using SQL DDL
tableEnv.executeSql("CREATE [TEMPORARY] TABLE MyTable (...) WITH (...)")

扩展表标识符 


表始终使用由目录、数据库和表名组成的 3 部分标识符进行注册。

用户可以将其中的一个目录和一个数据库设置为“当前目录”和“当前数据库”。 有了它们,上面提到的 3 部分标识符中的前两部分可以是可选的 - 如果未提供它们,则将引用当前目录和当前数据库。 用户可以通过 Table API 或 SQL 切换当前目录和当前数据库。

标识符遵循 SQL 要求,这意味着它们可以使用反引号字符 (`) 进行转义。

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

Table table = ...;

// register the view named 'exampleView' in the catalog named 'custom_catalog'
// in the database named 'custom_database' 
tableEnv.createTemporaryView("exampleView", table);

// register the view named 'exampleView' in the catalog named 'custom_catalog'
// in the database named 'other_database' 
tableEnv.createTemporaryView("other_database.exampleView", table);

// register the view named 'example.View' in the catalog named 'custom_catalog'
// in the database named 'custom_database' 
tableEnv.createTemporaryView("`example.View`", table);

// register the view named 'exampleView' in the catalog named 'other_catalog'
// in the database named 'other_database' 
tableEnv.createTemporaryView("other_catalog.other_database.exampleView", table);

查询表


表 API 


Table API 是用于 Scala 和 Java 的语言集成查询 API。 与 SQL 相比,查询不指定为字符串,而是使用宿主语言逐步组成。

API 基于代表表(流式或批处理)的 Table 类,并提供应用关系操作的方法。 这些方法返回一个新的 Table 对象,它表示对输入 Table 应用关系操作的结果。 一些关系操作是由多个方法调用组成的,例如 table.groupBy(...).select(),其中 groupBy(...) 指定 table 的分组,而 select(...) 在分组上的投影。

Table API 文档描述了流和批处理表支持的所有 Table API 操作。

以下示例显示了一个简单的 Table API 聚合查询:

// get a TableEnvironment
TableEnvironment tableEnv = ...; // see "Create a TableEnvironment" section

// register Orders table

// scan registered Orders table
Table orders = tableEnv.from("Orders");
// compute revenue for all customers from France
Table revenue = orders
  .filter($("cCountry").isEqual("FRANCE"))
  .groupBy($("cID"), $("cName"))
  .select($("cID"), $("cName"), $("revenue").sum().as("revSum"));

// emit or convert Table
// execute query

SQL 


Flink 的 SQL 集成基于 Apache Calcite,它实现了 SQL 标准。 SQL 查询被指定为常规字符串。

SQL 文档描述了 Flink 对流表和批处理表的 SQL 支持。

以下示例显示如何指定查询并将结果作为表返回。

// get a TableEnvironment
TableEnvironment tableEnv = ...; // see "Create a TableEnvironment" section

// register Orders table

// compute revenue for all customers from France
Table revenue = tableEnv.sqlQuery(
    "SELECT cID, cName, SUM(revenue) AS revSum " +
    "FROM Orders " +
    "WHERE cCountry = 'FRANCE' " +
    "GROUP BY cID, cName"
  );

// emit or convert Table
// execute query

以下示例显示如何指定将其结果插入已注册表的更新查询。

混合表 API 和 SQL 


Table API 和 SQL 查询很容易混合使用,因为它们都返回 Table 对象:

可以在 SQL 查询返回的 Table 对象上定义 Table API 查询。
通过在 TableEnvironment 中注册结果表并在 SQL 查询的 FROM 子句中引用它,可以在 Table API 查询的结果上定义 SQL 查询。

发出一个表


通过将 Table 写入 TableSink 来发出 Table。 TableSink 是一个通用接口,支持多种文件格式(例如 CSV、Apache Parquet、Apache Avro)、存储系统(例如 JDBC、Apache HBase、Apache Cassandra、Elasticsearch)或消息系统(例如 Apache Kafka、 兔MQ)。

批处理表只能写入 BatchTableSink,而流表需要 AppendStreamTableSink、RetractStreamTableSink 或 UpsertStreamTableSink。

有关可用接收器的详细信息以及如何实现自定义 DynamicTableSink 的说明,请参阅有关表源和接收器的文档。

Table.executeInsert(String tableName) 方法将 Table 发送到已注册的 TableSink。 该方法通过名称从目录中查找 TableSink,并验证 Table 的架构与 TableSink 的架构相同。

// get a TableEnvironment
TableEnvironment tableEnv = ...; // see "Create a TableEnvironment" section

// create an output Table
final Schema schema = Schema.newBuilder()
    .column("a", DataTypes.INT())
    .column("b", DataTypes.STRING())
    .column("c", DataTypes.BIGINT())
    .build();

tableEnv.createTemporaryTable("CsvSinkTable", TableDescriptor.forConnector("filesystem")
    .schema(schema)
    .option("path", "/path/to/file")
    .format(FormatDescriptor.forFormat("csv")
        .option("field-delimiter", "|")
        .build())
    .build());

// compute a result Table using Table API operators and/or SQL queries
Table result = ...

// emit the result Table to the registered TableSink
result.executeInsert("CsvSinkTable");

翻译并执行查询


表 API 和 SQL 查询被转换为 DataStream 程序,无论它们的输入是流式还是批处理。查询在内部表示为逻辑查询计划,并分两个阶段进行转换:

  1. 优化逻辑计划,
  2. 翻译成 DataStream 程序

在以下情况下会翻译 Table API 或 SQL 查询:

  • 调用 TableEnvironment.executeSql()。该方法用于执行给定的语句,一旦调用该方法,就会立即翻译 sql 查询。
  • 调用 Table.executeInsert()。该方法用于将表格内容插入给定的接收器路径,一旦调用该方法,就会立即翻译表格 API。
  • 调用 Table.execute()。该方法用于将表格内容采集到本地客户端,调用该方法后立即翻译表格API。
  • 调用 StatementSet.execute()。表(通过 StatementSet.addInsert() 发送到接收器)或 INSERT 语句(通过 StatementSet.addInsertSql() 指定)将首先在 StatementSet 中缓冲。一旦 StatementSet.execute() 被调用,它们就会被翻译。所有接收器都将优化为一个 DAG。
  • 表在转换为 DataStream 时被转换(请参阅与 DataStream 集成)。翻译后,它是一个常规的 DataStream 程序,并在调用 StreamExecutionEnvironment.execute() 时执行。

查询优化


Apache Flink 利用和扩展 Apache Calcite 来执行复杂的查询优化。这包括一系列基于规则和成本的优化,例如:

基于 Apache Calcite 的子查询去相关

  • 项目修剪
  • 分区修剪
  • 过滤器下推
  • 子计划去重,避免重复计算
  • 特殊子查询重写,包括两部分:

  将 IN 和 EXISTS 转换为左半联接
  将 NOT IN 和 NOT EXISTS 转换为左反连接

  • 可选的连接重新排序

       通过 table.optimizer.join-reorder-enabled 启用


注意:IN/EXISTS/NOT IN/NOT EXISTS 目前仅在子查询重写的连接条件中支持。

优化器不仅基于计划,还基于数据源提供的丰富统计数据和每个算子(如 io、cpu、网络和内存)的细粒度成本做出智能决策。

高级用户可以通过调用 TableEnvironment#getConfig#setPlannerConfig 提供给表环境的 CalciteConfig 对象提供自定义优化。

解释表 


Table API 提供了一种机制来解释计算表的逻辑和优化查询计划。 这是通过 Table.explain() 方法或 StatementSet.explain() 方法完成的。 Table.explain() 返回一个表的计划。 StatementSet.explain() 返回多个接收器的计划。 它返回一个描述三个计划的字符串:

  1. 关系查询的抽象语法树,即未优化的逻辑查询计划
  2. 优化的逻辑查询计划,以及
  3. 物理执行计划

TableEnvironment.explainSql() 和 TableEnvironment.executeSql() 支持执行 EXPLAIN 语句来获取计划,请参考 EXPLAIN 页面。

以下代码使用 Table.explain() 方法显示了给定 Table 的示例和相应输出:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);

DataStream> stream1 = env.fromElements(new Tuple2<>(1, "hello"));
DataStream> stream2 = env.fromElements(new Tuple2<>(1, "hello"));

// explain Table API
Table table1 = tEnv.fromDataStream(stream1, $("count"), $("word"));
Table table2 = tEnv.fromDataStream(stream2, $("count"), $("word"));
Table table = table1
  .where($("word").like("F%"))
  .unionAll(table2);

System.out.println(table.explain());

上面例子的结果是:


== Abstract Syntax Tree ==
LogicalUnion(all=[true])
:- LogicalFilter(condition=[LIKE($1, _UTF-16LE'F%')])
:  +- LogicalTableScan(table=[[Unregistered_DataStream_1]])
+- LogicalTableScan(table=[[Unregistered_DataStream_2]])

== Optimized Physical Plan ==
Union(all=[true], union=[count, word])
:- Calc(select=[count, word], where=[LIKE(word, _UTF-16LE'F%')])
:  +- DataStreamScan(table=[[Unregistered_DataStream_1]], fields=[count, word])
+- DataStreamScan(table=[[Unregistered_DataStream_2]], fields=[count, word])

== Optimized Execution Plan ==
Union(all=[true], union=[count, word])
:- Calc(select=[count, word], where=[LIKE(word, _UTF-16LE'F%')])
:  +- DataStreamScan(table=[[Unregistered_DataStream_1]], fields=[count, word])
+- DataStreamScan(table=[[Unregistered_DataStream_2]], fields=[count, word])

以下代码显示了使用 StatementSet.explain() 方法的多接收器计划的示例和相应输出:

EnvironmentSettings settings = EnvironmentSettings.inStreamingMode();
TableEnvironment tEnv = TableEnvironment.create(settings);

final Schema schema = Schema.newBuilder()
    .column("count", DataTypes.INT())
    .column("word", DataTypes.STRING())
    .build();

tEnv.createTemporaryTable("MySource1", TableDescriptor.forConnector("filesystem")
    .schema(schema)
    .option("path", "/source/path1")
    .format("csv")
    .build());
tEnv.createTemporaryTable("MySource2", TableDescriptor.forConnector("filesystem")
    .schema(schema)
    .option("path", "/source/path2")
    .format("csv")
    .build());
tEnv.createTemporaryTable("MySink1", TableDescriptor.forConnector("filesystem")
    .schema(schema)
    .option("path", "/sink/path1")
    .format("csv")
    .build());
tEnv.createTemporaryTable("MySink2", TableDescriptor.forConnector("filesystem")
    .schema(schema)
    .option("path", "/sink/path2")
    .format("csv")
    .build());

StatementSet stmtSet = tEnv.createStatementSet();

Table table1 = tEnv.from("MySource1").where($("word").like("F%"));
stmtSet.addInsert("MySink1", table1);

Table table2 = table1.unionAll(tEnv.from("MySource2"));
stmtSet.addInsert("MySink2", table2);

String explanation = stmtSet.explain();
System.out.println(explanation);

多sink计划的结果是:

== Abstract Syntax Tree ==
LogicalLegacySink(name=[`default_catalog`.`default_database`.`MySink1`], fields=[count, word])
+- LogicalFilter(condition=[LIKE($1, _UTF-16LE'F%')])
   +- LogicalTableScan(table=[[default_catalog, default_database, MySource1, source: [CsvTableSource(read fields: count, word)]]])

LogicalLegacySink(name=[`default_catalog`.`default_database`.`MySink2`], fields=[count, word])
+- LogicalUnion(all=[true])
   :- LogicalFilter(condition=[LIKE($1, _UTF-16LE'F%')])
   :  +- LogicalTableScan(table=[[default_catalog, default_database, MySource1, source: [CsvTableSource(read fields: count, word)]]])
   +- LogicalTableScan(table=[[default_catalog, default_database, MySource2, source: [CsvTableSource(read fields: count, word)]]])

== Optimized Physical Plan ==
LegacySink(name=[`default_catalog`.`default_database`.`MySink1`], fields=[count, word])
+- Calc(select=[count, word], where=[LIKE(word, _UTF-16LE'F%')])
   +- LegacyTableSourceScan(table=[[default_catalog, default_database, MySource1, source: [CsvTableSource(read fields: count, word)]]], fields=[count, word])

LegacySink(name=[`default_catalog`.`default_database`.`MySink2`], fields=[count, word])
+- Union(all=[true], union=[count, word])
   :- Calc(select=[count, word], where=[LIKE(word, _UTF-16LE'F%')])
   :  +- LegacyTableSourceScan(table=[[default_catalog, default_database, MySource1, source: [CsvTableSource(read fields: count, word)]]], fields=[count, word])
   +- LegacyTableSourceScan(table=[[default_catalog, default_database, MySource2, source: [CsvTableSource(read fields: count, word)]]], fields=[count, word])

== Optimized Execution Plan ==
Calc(select=[count, word], where=[LIKE(word, _UTF-16LE'F%')])(reuse_id=[1])
+- LegacyTableSourceScan(table=[[default_catalog, default_database, MySource1, source: [CsvTableSource(read fields: count, word)]]], fields=[count, word])

LegacySink(name=[`default_catalog`.`default_database`.`MySink1`], fields=[count, word])
+- Reused(reference_id=[1])

LegacySink(name=[`default_catalog`.`default_database`.`MySink2`], fields=[count, word])
+- Union(all=[true], union=[count, word])
   :- Reused(reference_id=[1])
   +- LegacyTableSourceScan(table=[[default_catalog, default_database, MySource2, source: [CsvTableSource(read fields: count, word)]]], fields=[count, word])

参考:

Overview | Apache Flink(Application Development  -- Table API & SQL -- Overview)

Concepts & Common API | Apache Flink(Application Development  -- Table API & SQL -- Concepts & Common API)

你可能感兴趣的:(Flink,flink)