1. Flink SQL 是什么?和标准 SQL 有啥关系?
官方的一句话概括:
Flink SQL 基于 Apache Calcite,遵循 SQL 标准做了扩展,是 Flink Table & SQL 生态的核心语言。
几个要点:
- 语义上尽可能兼容标准 SQL,大多数 DDL/DML/查询语法都很熟悉;
- 底层解析和优化依赖 Apache Calcite,因此很多保留关键字、语法规则来自 Calcite/SQL 标准;
- 结合 Flink 自己的特性(流式表、动态表、时间属性、水位线等),做了大量扩展。
Flink 目前支持的主要 SQL 语句包括(按类别 roughly 分组):
- 查询语言(Query) :
SELECT - DDL :
CREATE/DROP/ALTER/ANALYZE等 - DML :
INSERT/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 TABLECREATE CATALOGCREATE DATABASECREATE VIEWCREATE 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 TABLEDROP DATABASEDROP VIEWDROP 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 表达式、字面量)支持不完整。
例如在某些版本中,以下类型可能在表达式里受限制:
STRINGBYTESRAWTIME(p) WITHOUT TIME ZONETIME(p) WITH LOCAL TIME ZONETIMESTAMP(p) WITHOUT TIME ZONETIMESTAMP(p) WITH LOCAL TIME ZONEARRAYMULTISETROW
这意味着:
- 在 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 自己的扩展(如
STREAM、STRING等)。
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); - 或者固定习惯:所有列名都不用关键字(新设计的库尽量遵守)。
- 在 Flink 层用 不同名字 (如
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 → 运维调试。