1. 查询的核心入口:TableEnvironment.sqlQuery()
在 Flink 里,你不会直接用 Statement stmt = ... 这种 JDBC 风格,而是通过 TableEnvironment 来执行 SQL。
对于 SELECT / VALUES 这类"有结果集"的查询,入口是:
java
Table result = tableEnv.sqlQuery("SELECT ...");
这里有几个关键点:
1.1 查询的输入:必须是「可见的表」
SQL 语句里的表名,必须先在 TableEnvironment 中"可见",才可以被引用。常见几种方式:
- 通过
CREATE TABLEDDL 创建表(典型:Kafka、文件、JDBC) - 通过
createTemporaryView把 DataStream / Table 注册成视图 - 通过 Catalog 管理多数据源
- 直接从 Table API 创建 Table(然后用
toString()内联)
比如从 DataStream 创建一个临时视图 Orders:
java
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 假设外部数据源产生三列 (user, product, amount)
DataStream<Tuple3<Long, String, Integer>> ds = env.addSource(...);
// 注册成临时视图 "Orders"
tableEnv.createTemporaryView(
"Orders",
ds,
$("user"), $("product"), $("amount")
);
// 现在 SQL 就可以直接用 Orders 了
Table result = tableEnv.sqlQuery(
"SELECT product, amount FROM Orders WHERE product LIKE '%Rubber%'"
);
1.2 内联 Table:Table.toString() 小技巧
有时候你已经拿到一个 Table,不想再注册个名字,Flink 提供了一个"小彩蛋":
Table.toString()会自动给表起一个唯一的名字并注册到当前 TableEnvironment,- 然后返回这个名字。
所以你可以把 Table 直接拼进 SQL 里用,比如:
java
Table table = tableEnv.fromDataStream(ds, $("user"), $("product"), $("amount"));
Table result = tableEnv.sqlQuery(
"SELECT SUM(amount) FROM " + table + " WHERE product LIKE '%Rubber%'"
);
这段代码里:
table被隐式注册成一个匿名表;- SQL 里直接引用这个匿名表名即可。
这种方式在你做复杂逻辑、Table API 和 SQL 混用时非常方便。
2. 查询如何真正执行:executeSql() 与 Table.execute()
刚才说的 sqlQuery() 只构建查询计划,不会真正执行任务 ,它返回的是一个 Table。
要让查询真的"跑起来",需要用到以下两个入口:
TableEnvironment.executeSql(sql)Table.execute()
2.1 执行 SELECT / VALUES:返回 TableResult
对于 SELECT / VALUES 语句,你可以直接:
java
TableResult tableResult = tableEnv.executeSql("SELECT * FROM Orders");
或者把 SQL 写成 sqlQuery 再执行:
java
Table table = tableEnv.sqlQuery("SELECT * FROM Orders");
TableResult tableResult = table.execute(); // 等价于上面
TableResult 提供两种典型用法:
① collect():迭代拿结果(需要手动关闭)
java
try (CloseableIterator<Row> it = tableResult.collect()) {
while (it.hasNext()) {
Row row = it.next();
// 处理每一行
}
} // try-with-resources 会自动调用 it.close()
注意:
- 这是一个流式迭代器,流任务下它可以一直输出;
- 不主动关闭迭代器,对长时间运行的作业可能会资源泄露;
- 一旦调用了
collect(),结果只能消费一次,不能再调用print()。
② print():直接打印到控制台(调试神器)
java
TableResult result = tableEnv.sqlQuery("SELECT * FROM Orders").execute();
result.print();
Flink 会自动把结果以表格形式输出,非常适合开发调试。
⚠️ 注意:
collect()和print()都是单次消费 ,不能对同一个TableResult都调用一次,否则会报错。
2.2 不同 checkpoint 策略下的语义差异
Flink 是流批一体的,TableResult.collect()/print() 在不同模式下语义略有差异:
批任务 或 无 checkpoint 的流任务
- 结果一生产出来就可以被客户端看到;
- 不保证 exactly-once 或 at-least-once;
- 如果作业失败重启,客户端这边会抛异常。
有 exactly-once checkpoint 的流任务
- 提供 端到端 exactly-once 交付保证;
- 某条结果要等它所在的 checkpoint 完成后,才会暴露给客户端;
- 通俗点:输出稍有延迟,但不会重复、不会丢。
有 at-least-once checkpoint 的流任务
- 提供 端到端至少一次 语义;
- 结果会尽快输出,但在故障重启时可能重复。
一般实战建议:
- 日志调试、临时查询:可以不开 checkpoint,低延迟看结果就行;
- 对结果一致性有要求:推荐开启 exactly-once + 外部支持事务的 Sink;
3. 执行 INSERT:把结果写到外部系统
对于:
sql
INSERT INTO RubberOrders
SELECT product, amount
FROM Orders
WHERE product LIKE '%Rubber%';
这类 DML,使用方式依然是 executeSql:
java
tableEnv.executeSql(
"INSERT INTO RubberOrders " +
"SELECT product, amount FROM Orders WHERE product LIKE '%Rubber%'"
);
这里的 RubberOrders 是一个预先通过 DDL 定义好的表,比如:
java
final Schema schema = Schema.newBuilder()
.column("product", DataTypes.STRING())
.column("amount", DataTypes.INT())
.build();
final TableDescriptor sinkDescriptor = TableDescriptor.forConnector("filesystem")
.schema(schema)
.option("path", "/path/to/file")
.format(FormatDescriptor.forFormat("csv")
.option("field-delimiter", ",")
.build())
.build();
tableEnv.createTemporaryTable("RubberOrders", sinkDescriptor);
之后所有 INSERT INTO RubberOrders ... 都会持续写入这个 Sink 表(Kafka / 文件 / JDBC 等)。
小结:
- SELECT / VALUES :
executeSql(...)或table.execute()→TableResult→collect()/print()- INSERT / UPDATE / DELETE :
executeSql(...)即可,通常不用再 collect 结果,而是看作业和 Sink 是否正常即可。
4. SQL 语法 & 字面量规则(和 Java 很像)
Flink SQL 基于 Apache Calcite,整体遵循 ANSI SQL 标准,但有几条特别容易踩坑的规则值得记一下。
4.1 标识符(表名、字段名、函数名)大小写规则
规则类似 Java:
- 无论是否加引号,原始大小写会被保留;
- 匹配时是大小写敏感的。
通常我们会全部用小写,避免和 Calcite 的各种大小写处理搞混。
如果字段名中有特殊字符(例如空格、关键字),可以用反引号包起来:
sql
SELECT a AS `my field` FROM t;
SELECT `value`, `count` FROM my_table;
很多 SQL 实现是用双引号 ",Flink 更推荐 反引号 ,尤其当你字段名里有关键字时(比如 value、count 等)。
4.2 字符串字面量 & 转义
- 字符串统一用 单引号
';
sql
SELECT 'Hello World';
- 单引号内部如果再需要单引号,用 两个单引号 转义:
sql
SELECT 'It''s me';
-- 输出:It's me
你在 SQL Client 里就能看到类似:
text
Flink SQL> SELECT 'Hello World', 'It''s me';
+-------------+---------+
| EXPR$0 | EXPR$1 |
+-------------+---------+
| Hello World | It's me |
+-------------+---------+
4.3 Unicode & C 风格转义(Flink 2.0 起)
如果你需要精确写 Unicode 字符,有两种方式:
① 使用 U&'' 语法
sql
-- 默认用反斜杠作为转义符
SELECT U&'\263A'; -- ☺
SELECT U&'#263A' UESCAPE '#'; -- 自定义转义符为 #
② C 风格转义(Flink 2.0)
以下是常见的转义序列:
| 写法 | 含义 |
|---|---|
\b |
backspace |
\f |
form feed |
\n |
newline(换行) |
\r |
carriage return |
\t |
tab |
\o...\ooo |
8 进制字节 |
\xh...\xhh |
16 进制字节 |
\uxxxx / \Uxxxxxxxx |
16/32 位 Unicode |
示例:
sql
SELECT e'\u0061\x61\141' AS c;
-- 或
SELECT E'\u0061\x61\141' AS c;
这三段都代表字符 a,最后输出 aaa。
5. 一个完整小示例:从 inlined Table 到文件 Sink
把上文的所有概念串起来,我们做一个完整例子:
- 从 DataStream 读取
user, product, amount; - 用
sqlQuery()过滤product包含 "Rubber" 的记录; - 把结果写到文件 Sink。
java
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.*;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import static org.apache.flink.table.api.Expressions.$;
public class FlinkSqlQueriesDemo {
public static void main(String[] args) throws Exception {
// 1. 环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 2. DataStream -> 内联 Table
DataStream<Tuple3<Long, String, Integer>> ds = env.addSource(...); // 自己实现 Source
Table ordersTable = tableEnv.fromDataStream(
ds,
$("user"),
$("product"),
$("amount")
);
// 3. 用 sqlQuery 在内联表上做查询
Table rubberOrders = tableEnv.sqlQuery(
"SELECT product, amount FROM " + ordersTable +
" WHERE product LIKE '%Rubber%'"
);
// 4. 定义文件 Sink 表
Schema schema = Schema.newBuilder()
.column("product", DataTypes.STRING())
.column("amount", DataTypes.INT())
.build();
TableDescriptor sinkDescriptor = TableDescriptor.forConnector("filesystem")
.schema(schema)
.option("path", "/tmp/rubber_orders.csv")
.format("csv")
.build();
tableEnv.createTemporaryTable("RubberOrders", sinkDescriptor);
// 5. 把查询结果 INSERT INTO Sink
rubberOrders.executeInsert("RubberOrders");
env.execute("Flink SQL Queries Demo");
}
}
这里用的是 Table.executeInsert("RubberOrders")(不同版本 API 名字略有差异,本质等价于做一条 INSERT INTO)。
6. 总结 & 实战建议
最后用几条要点再捋一遍:
-
构建查询:
sqlQuery()只返回Table,不触发执行;- SQL 里的表必须先在 TableEnvironment 中"可见"(DDL / View / Catalog / 内联 Table)。
-
执行查询:
executeSql("SELECT ...")或table.execute()→TableResult;- 用
collect()流式迭代结果,记得关闭迭代器; - 用
print()快速在控制台看结果(只能用一次)。
-
写入外部系统:
- 用
INSERT INTO sink SELECT ...; - Sink 表通过 DDL / TableDescriptor 预先定义好即可。
- 用
-
流批语义:
- 批任务 / 无 checkpoint:快速看到结果,但不保证一致性;
- exactly-once:结果稍延迟,保证端到端精确一次;
- at-least-once:可能有重复,需要下游幂等处理。
-
语法注意事项:
- 标识符大小写敏感,关键字要用反引号包起来;
- 字符串用单引号,内部单引号要写成两个;
- Unicode 和 C 风格转义可以帮你精确控制字符。