4. Flink SQL 核心基础:环境搭建、Table & SQL API 入门、动态表与连续查询

好,咱们进入 Flink SQL 的世界。这一章是地基,我建议你认真过一遍。很多同学上来就写 SQL,结果环境配不对、表概念搞混,后面排查问题特别痛苦。我自己带团队时,踩过不少这种坑。

4.1 开发环境搭建:别让环境卡住你

先说环境。Flink 支持本地开发、集群部署,咱们课程里主要用本地模式跑例子。你只需要三样东西:JDK 8/11、Maven 3.6+、IDE(我习惯用 IntelliJ IDEA)。

创建一个 Maven 项目,引入核心依赖:

<properties>
    <flink.version>1.17.0</flink.version>
    <scala.binary.version>2.12</scala.binary.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-table-api-java-bridge_${scala.binary.version}</artifactId>
        <version>${flink.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-table-planner-loader</artifactId>
        <version>${flink.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-streaming-java</artifactId>
        <version>${flink.version}</version>
    </dependency>
</dependencies>
小提示: 如果你用 Scala 版 API,记得把 flink-table-api-java-bridge 换成 flink-table-api-scala-bridge。我个人更推荐 Java,团队协作时类型更安全。

依赖配好后,写个最简单的入口验证环境:

import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.TableEnvironment;

public class FlinkSQLDemo {
    public static void main(String[] args) {
        // 创建流处理环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        // 创建表环境
        EnvironmentSettings settings = EnvironmentSettings.newInstance()
                .inStreamingMode()
                .build();
        TableEnvironment tableEnv = TableEnvironment.create(settings);

        System.out.println("Flink SQL 环境搭建成功!");
    }
}

跑一下,控制台没报错,说明环境通了。嗯,这里要注意:inStreamingMode() 表示我们工作在流模式下,这是实时宽表的核心场景。

4.2 Table & SQL API 入门:两种姿势,一个内核

Flink 提供了两套 API:Table API 和 SQL API。说白了,Table API 是 Java/Scala 的链式调用,SQL API 就是写标准 SQL。底层引擎是一样的。

我刚开始用的时候,总觉得 Table API 有点多余。后来发现,在复杂 ETL 场景下,Table API 的算子组合比 SQL 字符串拼接更灵活。举个例子:

4.2.1 用 Table API 注册表

// 从 DataStream 创建 Table
DataStream<Order> orderStream = env.addSource(...);
Table orderTable = tableEnv.fromDataStream(orderStream,
        $("orderId"),
        $("userId"),
        $("amount"),
        $("orderTime").rowtime());

// 注册为临时视图
tableEnv.createTemporaryView("orders", orderTable);

4.2.2 用 SQL 查询

Table result = tableEnv.sqlQuery(
    "SELECT userId, SUM(amount) AS totalAmount " +
    "FROM orders " +
    "GROUP BY userId"
);

你看,两种方式可以混用。我个人习惯:简单查询用 SQL,复杂多步骤用 Table API。为什么?SQL 可读性好,但调试起来麻烦;Table API 可以在 IDE 里一步步断点。

核心概念: Table 和 DataStream 可以互相转换。用 tableEnv.fromDataStream() 把流变成表,用 tableEnv.toDataStream() 把表变回流。这是实时宽表构建的基石。

4.3 动态表与连续查询:流式 SQL 的灵魂

好,重点来了。传统数据库里,表是静态的——数据插入后就不变了。但 Flink SQL 里,表是动态的。什么意思?

你想想看,一个 Kafka 消息流源源不断进来,每来一条数据,表的内容就变了。Flink 把这种随时间变化的表叫做动态表(Dynamic Table)。而我们对动态表执行的 SQL,就是连续查询(Continuous Query)——它永远不会终止,每来一条数据,结果就更新一次。

我记得第一次理解这个概念时,脑子有点绕。画个图就清楚了:

动态表与连续查询核心流程 数据流 Kafka / Socket / 文件 动态表 随时间不断变化 追加 / 更新 / 删除行 连续查询 SQL 持续执行 结果实时更新 结果表 关键理解 1. 输入是无限的流,不是有限的批 2. 动态表在查询过程中持续追加/更新行 3. 连续查询的结果也是动态表,可以继续被下游消费 4. 最终结果可以输出到 Kafka、MySQL、HBase 等外部系统

4.4 动态表的三种更新模式

动态表不是随便变的,它有明确的更新模式。我整理了一张表,你对照着看:

更新模式 说明 典型场景 对应 SQL
Append 只追加,不修改已有行 日志采集、点击流 SELECT ... FROM source
Retract 先发送旧值的删除消息,再发送新值 聚合结果更新 GROUP BY、JOIN
Upsert 根据 Key 直接覆盖,不发送删除 维表关联、去重 ROW_NUMBER() OVER
避坑指南: 我曾经在项目中用 GROUP BY 做实时统计,结果下游 Kafka Sink 报错。查了半天才发现,GROUP BY 产生的是 Retract 流,而 Kafka 默认只支持 Append。解决方案是改用 UPSERT 模式的 Kafka Sink 或者用 changelog-json 格式。

4.5 时间属性:流式 SQL 的命脉

动态表离不开时间。Flink SQL 支持三种时间语义:

  • 事件时间(Event Time):数据本身携带的时间戳。最准确,但需要处理乱序。
  • 处理时间(Processing Time):Flink 算子所在机器的当前时间。简单,但不准确。
  • 摄入时间(Ingestion Time):数据进入 Flink 的时间。介于两者之间。

我建议你优先使用事件时间。为什么?实时宽表要保证数据一致性,处理时间在重算或回溯时结果会变。举个例子:

// 定义事件时间
Table orders = tableEnv.fromDataStream(orderStream,
    $("orderId"),
    $("userId"),
    $("amount"),
    $("orderTime").rowtime());  // 指定事件时间

// 基于事件时间的滚动窗口
Table result = tableEnv.sqlQuery(
    "SELECT " +
    "  userId, " +
    "  TUMBLE_END(orderTime, INTERVAL '1' MINUTE) AS windowEnd, " +
    "  SUM(amount) AS totalAmount " +
    "FROM orders " +
    "GROUP BY userId, TUMBLE(orderTime, INTERVAL '1' MINUTE)"
);
小技巧: 定义事件时间时,记得设置 watermark。我习惯用 FOR SYSTEM_TIME AS OF 配合 rowtime(),这样 Flink 会自动处理乱序数据。水位线设置太短容易丢数据,太长延迟高,一般设 5-10 秒。

4.6 本章小结

这一章我们搭好了 Flink SQL 的开发环境,理解了 Table API 和 SQL API 的关系,最重要的是搞懂了动态表和连续查询这两个核心概念。说白了,实时宽表就是在一张不断变化的动态表上,跑一个永不停止的 SQL 查询。