Flink SQL 语言从 DDL 到 DML,再到关键字与数据类型

官方的一句话概括:

Flink SQL 基于 Apache Calcite,遵循 SQL 标准做了扩展,是 Flink Table & SQL 生态的核心语言。

几个要点:

  • 语义上尽可能兼容标准 SQL,大多数 DDL/DML/查询语法都很熟悉;
  • 底层解析和优化依赖 Apache Calcite,因此很多保留关键字、语法规则来自 Calcite/SQL 标准;
  • 结合 Flink 自己的特性(流式表、动态表、时间属性、水位线等),做了大量扩展。

Flink 目前支持的主要 SQL 语句包括(按类别 roughly 分组):

  • 查询语言(Query)SELECT
  • DDLCREATE / DROP / ALTER / ANALYZE
  • DMLINSERT / UPDATE / DELETE
  • 元信息与调试DESCRIBE / EXPLAIN / SHOW / USE / LOAD / UNLOAD

接下来按功能逐块拆开。

2. 查询语言:SELECT(Queries)

Flink 的查询语言入口依然是最经典的 SELECT

sql 复制代码
SELECT
  user_id,
  COUNT(*) AS cnt
FROM user_events
WHERE event_type = 'click'
GROUP BY user_id;

在 Flink 里,所有 DML 的起点基本都是一个 SELECT,只不过它的目的地可能是:

  • 直接在 SQL Client 里打印;
  • 或者通过 INSERT INTO target_table 写到 Kafka / MySQL / Iceberg 等外部系统。

流批一体的特点在 SQL 层也成立:

  • 批模式SELECT 对静态数据做一次性查询;
  • 流模式SELECT 对动态表(持续变化的表)持续计算,生成一个 changelog 流。

3. DDL:CREATE / DROP / ALTER / ANALYZE 等

3.1 CREATE:建表、建库、建视图、建函数

Flink SQL 的 CREATE 家族包括:

  • CREATE TABLE
  • CREATE CATALOG
  • CREATE DATABASE
  • CREATE VIEW
  • CREATE FUNCTION
3.1.1 CREATE TABLE:最常用的 DDL

典型示例(Kafka 源表):

sql 复制代码
CREATE TABLE user_events (
  user_id      STRING,
  event_type   STRING,
  ts           BIGINT,
  row_time     AS TO_TIMESTAMP_LTZ(ts, 3),
  WATERMARK FOR row_time AS row_time - INTERVAL '5' SECOND
) WITH (
  'connector' = 'kafka',
  'topic' = 'user_events',
  'properties.bootstrap.servers' = 'kafka:9092',
  'format' = 'json'
);

要点:

  • 字段定义 + 计算列(AS ...)+ WATERMARK(事件时间属性);
  • WITH 里配置 connector、topic、格式等;
  • 这类建表是 动态表(Dynamic Table) 的入口,后续 SQL 都基于它。
3.1.2 CREATE VIEW:保存查询结果的逻辑定义

视图不会真正存储数据,它只是一个 查询的别名

sql 复制代码
CREATE VIEW active_users_10m AS
SELECT
  TUMBLE_START(row_time, INTERVAL '10' MINUTE) AS win_start,
  user_id,
  COUNT(*) AS cnt
FROM user_events
GROUP BY
  TUMBLE(row_time, INTERVAL '10' MINUTE),
  user_id;

后续你可以直接:

sql 复制代码
SELECT * FROM active_users_10m WHERE cnt > 100;
3.1.3 CREATE FUNCTION:注册 UDF/UDAF/UDTF

比如把 Java 里写好的 UDF 注册成 SQL 函数:

sql 复制代码
CREATE FUNCTION normalize_name AS 'com.example.udf.NormalizeName';

然后:

sql 复制代码
SELECT normalize_name(user_name) FROM users;

3.2 DROP:删表、删库、删视图、删函数

  • DROP TABLE
  • DROP DATABASE
  • DROP VIEW
  • DROP FUNCTION

例子:

sql 复制代码
DROP TABLE IF EXISTS tmp_user_events;
DROP VIEW active_users_10m;
DROP FUNCTION normalize_name;

注意和外部存储的绑定关系:

Flink 的 DROP TABLE 通常只删掉 catalog 中的元信息,是否真正删掉底层存储(如 Kafka topic / HDFS 目录),要看具体 connector/catal og 的实现。

3.3 ALTER:修改表、库、函数属性

Flink 支持的 ALTER 能力相对有限,大致包括:

  • 修改表的属性(如 connector 选项);
  • (部分版本)支持列的增删改、主键标记调整;
  • 修改函数类型(TEMPORARY 与否等);
  • 修改数据库属性。

示意:

sql 复制代码
ALTER TABLE user_events SET (
  'scan.startup.mode' = 'latest-offset'
);

3.4 ANALYZE TABLE:统计表的统计信息(接近优化器提示)

ANALYZE TABLE 会收集表的行数、列的基数等统计信息,帮助优化器做更好的 Join 顺序、执行计划选择:

sql 复制代码
ANALYZE TABLE user_events COMPUTE STATISTICS;

在大规模复杂查询(尤其是 Batch 或 Hybrid)的场景里,这个会对计划质量有帮助。

4. DML:INSERT / UPDATE / DELETE

4.1 INSERT:最常用的写入语句

Flink SQL 中最常见的生产语句就是:

sql 复制代码
INSERT INTO sink_table
SELECT ... FROM source_table ...;

例子:

sql 复制代码
INSERT INTO big_screen_metrics
SELECT
  TUMBLE_START(row_time, INTERVAL '1' MINUTE) AS window_start,
  app_id,
  COUNT(*) AS pv,
  COUNT(DISTINCT user_id) AS uv
FROM user_events
GROUP BY
  TUMBLE(row_time, INTERVAL '1' MINUTE),
  app_id;

在 SQL Client 或应用中,这个 INSERT 一旦提交:

  • 流模式:就会变成一个持续运行的流任务;
  • 批模式:就是一条离线任务,跑完即止。

4.2 UPDATE / DELETE:基于主键的更新与删除

在 Flink SQL 里,UPDATE / DELETE 通常依赖:

  • 表上存在主键(PRIMARY KEY NOT ENFORCED)
  • 底层 connector 支持 upsert / delete 语义(如 Upsert-Kafka、一些数据库 sink)。

简单例子(伪示意,实际支持要看版本和 connector):

sql 复制代码
UPDATE user_status
SET state = 'disabled'
WHERE user_id = 'u_001';

DELETE FROM user_status
WHERE user_id = 'u_002';

在流式语义下,这些会被转成 changelog(UPDATE_BEFORE / UPDATE_AFTER / DELETE)写入 sink。

5. 元信息与运维相关语句:DESCRIBE / EXPLAIN / USE / SHOW / LOAD / UNLOAD

5.1 DESCRIBE:查看表结构与时间属性

sql 复制代码
DESCRIBE user_events;

输出中包括:

  • 字段名、类型、是否 nullable;
  • 是否是 *ROWTIME* / *PROCTIME*
  • Wartermark 相关信息。

在调 flink-sql 或排查时间属性/水位线问题时非常好用。

5.2 EXPLAIN:查看执行计划

sql 复制代码
EXPLAIN
SELECT
  user_id,
  COUNT(*) AS cnt
FROM user_events
GROUP BY user_id;

会展示:

  • 逻辑计划;
  • 优化后的物理计划;
  • 各种算子链路、shuffle、并行度信息(视配置和版本而定)。

调性能时必看。

5.3 USE:切换 catalog / database

类似于 MySQL 里的 USE db;

sql 复制代码
USE CATALOG my_catalog;
USE my_database;

5.4 SHOW:列出资源

常见用法:

sql 复制代码
SHOW CATALOGS;
SHOW DATABASES;
SHOW TABLES;
SHOW FUNCTIONS;

在交互式地探索环境时非常好用。

5.5 LOAD / UNLOAD

  • LOAD / UNLOAD 用于加载/卸载一些资源,会因 catalog/方言不同而略有差异;
  • 通常在需要动态加载 jar/模块、或存算分离场景下使用。

在日常业务开发中,LOAD/UNLOAD 出场频率相对较低。

6. SQL 中的数据类型:与 Data Types 文档的关系

在 DDL 语句里,Flink SQL 支持完整的数据类型体系(前文我们已经专门写过一篇)。

简单回顾几个要点:

  • 标准标量类型:BOOLEAN / TINYINT / SMALLINT / INT / BIGINT / FLOAT / DOUBLE / DECIMAL(p, s) 等;
  • 字符类型:CHAR(n) / VARCHAR(n) / STRING
  • 二进制:BINARY(n) / VARBINARY(n) / BYTES
  • 日期时间:DATE / TIME(p) / TIMESTAMP(p) / TIMESTAMP_LTZ(p) / 各种 INTERVAL
  • 复合类型:ARRAY<T> / MAP<K, V> / MULTISET<T> / ROW<...>
  • 特殊类型:RAW / VARIANT / STRUCTURED 等。

6.1 注意:有些类型在「查询表达式」里还不能完全使用

文档中特别提醒:

有些数据类型在 SQL 查询里(比如 cast 表达式、字面量)支持不完整。

例如在某些版本中,以下类型可能在表达式里受限制:

  • STRING
  • BYTES
  • RAW
  • TIME(p) WITHOUT TIME ZONE
  • TIME(p) WITH LOCAL TIME ZONE
  • TIMESTAMP(p) WITHOUT TIME ZONE
  • TIMESTAMP(p) WITH LOCAL TIME ZONE
  • ARRAY
  • MULTISET
  • ROW

这意味着:

  • 在 DDL 定义里你可以用这些类型;
  • 但在 SELECT 的表达式里,有的类型暂时不能直接写字面量,或者 CAST 不支持所有目标类型。

实战建议:

  • DDL 尽量用 Flink Data Types 文档中推荐的类型
  • 遇到 cast 报错时,优先简化为基础类型或用 UDF 做一些自定义转换。

7. 保留关键字(Reserved Keywords):为啥会突然报语法错?

文档最后给了一大长串保留关键字,比如:

SELECT, GROUP, ORDER, WINDOW, VIEW, TABLE, STREAM, OFFSET, VALUE, COUNT, MAP, RAW, STRING, INTERVAL, TIMESTAMP, TOP, YEAR, MONTH ......(共几百个)

这些关键字来自:

  • SQL 标准;
  • Calcite;
  • Flink 自己的扩展(如 STREAMSTRING 等)。

7.1 问题:我在建表时用了关键字当字段名

非常常见的坑,例如:

sql 复制代码
CREATE TABLE logs (
  timestamp TIMESTAMP(3),
  offset BIGINT,
  value STRING
) WITH (...);

结果建表就报语法错,或者后续 SELECT 时报错。

7.2 解决办法:用反引号 ````` 转义

文档明确说:

如果你想把一个保留关键字当作字段名使用,需要用反引号包起来,例如:
value, count, group

改写建表:

sql 复制代码
CREATE TABLE logs (
  `timestamp` TIMESTAMP(3),
  `offset` BIGINT,
  `value` STRING
) WITH (...);

查询时也一样:

sql 复制代码
SELECT `timestamp`, `offset`, `value` FROM logs;

7.3 实战命名建议

为了少踩坑,建议:

  • 字段名尽量 避免 与关键字重名,如 user, group, value, offset, timestamp, order 等;

  • 如果是从已有 MySQL/OLTP 库同步过来的 schema,可以:

    • 在 Flink 层用 不同名字 (如 ts 替代 timestamp);
    • 或者固定习惯:所有列名都不用关键字(新设计的库尽量遵守)。

8. 一个小型综合示例:从 DDL 到 Query 再到 Insert

假设我们要做一个简单的"用户行为 → 实时指标"的任务,可以用 Flink SQL 写成这样:

8.1 1)创建源表(Kafka)

sql 复制代码
CREATE TABLE user_events (
  user_id    STRING,
  event_type STRING,
  ts         BIGINT,
  row_time   AS TO_TIMESTAMP_LTZ(ts, 3),
  WATERMARK FOR row_time AS row_time - INTERVAL '5' SECOND
) WITH (
  'connector' = 'kafka',
  'topic' = 'user_events',
  'properties.bootstrap.servers' = 'kafka:9092',
  'format' = 'json'
);

8.2 2)创建结果表(还是 Kafka)

sql 复制代码
CREATE TABLE user_metrics_1m (
  window_start TIMESTAMP(3),
  window_end   TIMESTAMP(3),
  user_id      STRING,
  pv           BIGINT,
  uv           BIGINT
) WITH (
  'connector' = 'kafka',
  'topic' = 'user_metrics_1m',
  'properties.bootstrap.servers' = 'kafka:9092',
  'format' = 'json'
);

8.3 3)分析表结构,检查时间属性是否正确

sql 复制代码
DESCRIBE user_events;

看一下 row_time 是否被标记为 *ROWTIME*,watermark 是否生效。

8.4 4)先用 SELECT 做交互式调试

sql 复制代码
SELECT
  TUMBLE_START(row_time, INTERVAL '1' MINUTE) AS window_start,
  TUMBLE_END(row_time, INTERVAL '1' MINUTE)   AS window_end,
  user_id,
  COUNT(*) AS pv,
  COUNT(DISTINCT session_id) AS uv  -- 假设有 session_id
FROM user_events
WHERE event_type = 'page_view'
GROUP BY
  TUMBLE(row_time, INTERVAL '1' MINUTE),
  user_id;

8.5 5)用 EXPLAIN 看一下执行计划

sql 复制代码
EXPLAIN
INSERT INTO user_metrics_1m
SELECT
  TUMBLE_START(row_time, INTERVAL '1' MINUTE) AS window_start,
  TUMBLE_END(row_time, INTERVAL '1' MINUTE)   AS window_end,
  user_id,
  COUNT(*) AS pv,
  COUNT(DISTINCT session_id) AS uv
FROM user_events
WHERE event_type = 'page_view'
GROUP BY
  TUMBLE(row_time, INTERVAL '1' MINUTE),
  user_id;

确认:

  • 窗口算子;
  • groupBy key;
  • 是否有 shuffle;
  • 是否有不必要的 exchange。

8.6 6)正式提交 INSERT,启动任务

sql 复制代码
INSERT INTO user_metrics_1m
SELECT
  TUMBLE_START(row_time, INTERVAL '1' MINUTE) AS window_start,
  TUMBLE_END(row_time, INTERVAL '1' MINUTE)   AS window_end,
  user_id,
  COUNT(*) AS pv,
  COUNT(DISTINCT session_id) AS uv
FROM user_events
WHERE event_type = 'page_view'
GROUP BY
  TUMBLE(row_time, INTERVAL '1' MINUTE),
  user_id;

至此,一个完整的 Flink SQL 作业就串起来了:从 DDL → Query → DML → 运维调试

相关推荐
Mxsoft61943 分钟前
某次实时分析延迟高,Flink事件时间窗口对齐救场!
大数据·flink
mn_kw1 小时前
Spark SQL CBO(基于成本的优化器)参数深度解析
前端·sql·spark
5***E6859 小时前
【SQL】写SQL查询时,常用到的日期函数
数据库·sql
yaoxin52112312 小时前
为什么 IRIS SQL 会比 Spring JDBC 更快?
数据库·sql·spring
M***Z21012 小时前
SQL中如何添加数据
数据库·sql
n***265613 小时前
Python连接SQL SEVER数据库全流程
数据库·python·sql
r***l76614 小时前
sql中COALESCE函数详解
数据库·sql
1***357715 小时前
SQL之CASE WHEN用法详解
数据库·python·sql
前进的李工16 小时前
SQL入门:从零掌握数据库查询语言
数据库·sql·mysql