Flink SQL CREATE 语句从建表到 CTAS/RTAS,一次讲清 - 详解

1. CREATE 语句能干嘛

CREATE 系列语句的目标只有一个:把对象注册到当前或指定 Catalog 中,让它们能被后续 SQL 直接引用(查询、写入、函数调用、模型推理等)。

当前 Flink SQL 常见支持:

  1. CREATE TABLE
  2. [CREATE OR] REPLACE TABLE
  3. CREATE CATALOG
  4. CREATE DATABASE
  5. CREATE VIEW
  6. CREATE FUNCTION
  7. CREATE MODEL

2. 如何执行 CREATE:Java TableEnvironment 一把梭

在 Java 里,CREATE 语句就是一条 SQL 字符串,直接丢给 TableEnvironment.executeSql()

TableEnvironment tableEnv = TableEnvironment.create(...);
// 1) 注册源表
tableEnv.executeSql(
"CREATE TABLE Orders (`user` BIGINT, product STRING, amount INT) WITH (...)"
);
// 2) 注册 sink 表
tableEnv.executeSql(
"CREATE TABLE RubberOrders(product STRING, amount INT) WITH (...)"
);
// 3) 查询
Table result = tableEnv.sqlQuery(
"SELECT product, amount FROM Orders WHERE product LIKE '%Rubber%'"
);
// 4) 写入
tableEnv.executeSql(
"INSERT INTO RubberOrders " +
"SELECT product, amount FROM Orders WHERE product LIKE '%Rubber%'"
);

SQL Client 场景通常更简单:直接在 CLI 里执行 CREATE 即可(依赖通常已带齐)。

3. CREATE TABLE:Flink 建表的“核心玩法”

3.1 总体语法骨架(你可以把它当模板)

你在生产里 90% 的建表,都可以从这个骨架改出来:

CREATE TABLE [IF NOT EXISTS] [catalog.][db.]table_name (
-- 1) 物理列 / 元数据列 / 计算列(三选一或混合)
...
-- 2) watermark(可选,但事件时间流基本必备)
[ WATERMARK FOR rowtime AS ... ]
-- 3) 主键/约束(可选,但 upsert/更新流强烈推荐)
[ PRIMARY KEY (...) NOT ENFORCED ]
)
[COMMENT '...']
[ DISTRIBUTED BY ... | DISTRIBUTED INTO ... ]   -- 桶/分布(可选)
[ PARTITIONED BY (...) ]                        -- 分区(可选,filesystem 常用)
WITH (
'connector'='...',
...
)
[ LIKE source_table (...) | AS select_query ];

4. 列的三种形态:物理列 / 元数据列 / 计算列

4.1 物理列(Physical Columns)

最普通的列:定义字段名、类型、顺序。connector/format 读取与写出基本都依赖物理列

CREATE TABLE MyTable (
user_id BIGINT,
name STRING
) WITH (...);

4.2 元数据列(Metadata Columns)

用于把 connector/format 的“行级元信息”显式暴露出来(比如 Kafka 的 timestamp、offset 等)。

CREATE TABLE MyTable (
user_id BIGINT,
name STRING,
record_time TIMESTAMP_LTZ(3) METADATA FROM 'timestamp'
) WITH (
'connector'='kafka',
...
);

常见要点:

  1. FROM 'xxx' 可以省略:列名就当 metadata key
  2. 类型允许兼容 cast(比如把 timestamp 映射成 BIGINT)
  3. VIRTUAL:只参与读(source-to-query),不参与写出(query-to-sink)
CREATE TABLE MyTable (
`timestamp` BIGINT METADATA,
`offset` BIGINT METADATA VIRTUAL,
user_id BIGINT,
name STRING
) WITH ('connector'='kafka', ...);

4.3 计算列(Computed Columns)

用表达式生成的“虚拟列”,不会物理落盘/落 Kafka。非常常用来:

  • 补齐处理时间:proctime AS PROCTIME()
  • 从字符串/JSON 转换事件时间
  • 做派生指标:cost AS price * quantity
CREATE TABLE MyTable (
user_id BIGINT,
price DOUBLE,
quantity DOUBLE,
cost AS price * quantity
) WITH (...);

计算列不能作为 INSERT INTO 的目标列(因为不持久化)。

5. WATERMARK:事件时间语义的“开关”

水位线声明形式:

WATERMARK FOR rowtime_column AS watermark_expression

关键规则(记住这三条,基本不踩坑):

  1. rowtime_column 必须是表中已存在的列
  2. 事件时间列通常要求是 TIMESTAMP(3)(可来自计算列)
  3. watermark 表达式返回值也必须是 TIMESTAMP(3)

常用策略:

5.1 严格递增(几乎不允许乱序)

WATERMARK FOR rowtime AS rowtime

5.2 允许 5 秒乱序(生产最常见)

WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND

完整例子:

CREATE TABLE Orders (
`user` BIGINT,
product STRING,
order_time TIMESTAMP(3),
WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND
) WITH (...);

6. PRIMARY KEY:不是约束检查,而是“优化提示”

Flink 的主键语义要记住一句话:

Flink 只支持 NOT ENFORCED,不会帮你检查数据是否真的唯一;你要自己保证。

但主键仍然非常重要,因为它会影响:

  • Planner 的优化(例如 upsert、changelog 推导)
  • 下游 sink 的写入语义(比如 upsert-kafka/jdbc upsert)

写法两种:

6.1 列级主键

id BIGINT PRIMARY KEY NOT ENFORCED

6.2 表级主键

PRIMARY KEY (id, biz_date) NOT ENFORCED

注意:主键列不可为空;Flink 会据此调整列的 nullability 假设。

7. PARTITIONED BY 与 DISTRIBUTED:分区 vs 桶(别搞混)

7.1 PARTITIONED BY:典型用于 filesystem sink

按分区列生成目录层级(Hive/Filesystem 场景最常见):

PARTITIONED BY (dt, region)

7.2 DISTRIBUTED / BUCKET:典型用于负载均衡与并行写

桶(buckets)把“可能无限的 keyspace”切成更可管理的小片,利于并行处理与下游存储的写放大控制。

常见写法:

-- 指定 HASH + 桶数
CREATE TABLE MyTable (uid BIGINT, name STRING)
DISTRIBUTED BY HASH(uid) INTO 4 BUCKETS;
-- 让 connector 自选算法,只给桶键与桶数
CREATE TABLE MyTable (uid BIGINT, name STRING)
DISTRIBUTED BY (uid) INTO 4 BUCKETS;
-- 只给桶键,桶数交给 connector
CREATE TABLE MyTable (uid BIGINT, name STRING)
DISTRIBUTED BY (uid);
-- 只给桶数
CREATE TABLE MyTable (uid BIGINT, name STRING)
DISTRIBUTED INTO 4 BUCKETS;

8. WITH Options:连接器与格式的“入口”

WITH ('k1'='v1', 'k2'='v2') 用来描述 connector/format 等属性。要点:

  1. key/value 都必须是字符串字面量
  2. 表一旦注册,既可以作为 source 也可以作为 sink(直到被 DML 引用时才确定角色)
  3. 表名可以是:catalog.db.table / db.table / table

9. LIKE:复用表定义,做“继承 + 覆盖”

LIKE 可以基于已有表复用定义,并选择:

  • INCLUDING / EXCLUDING 哪些部分(约束、分布、分区、watermark、options、generated、metadata…)
  • 是否 OVERWRITING(遇到冲突 key 时用新表覆盖)

典型场景:复用 schema,把 connector 从 filesystem 换成 kafka,同时补 watermark

CREATE TABLE Orders (
`user` BIGINT,
product STRING,
order_time TIMESTAMP(3)
) WITH (
'connector'='kafka',
'scan.startup.mode'='earliest-offset'
);
CREATE TABLE Orders_with_watermark (
WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND
) WITH (
'scan.startup.mode'='latest-offset'
)
LIKE Orders;

10. CTAS:Create Table As Select(建表 + 插入一条命令完成)

CTAS 适合“快速从查询结果生成一张新表”:

CREATE TABLE my_ctas_table
WITH ('connector'='kafka', ...)
AS
SELECT id, name, age
FROM source_table
WHERE MOD(id, 10) = 0;

你也可以在 CREATE 部分显式加:

  • computed columns
  • watermark
  • primary key
  • distributed

但注意两类限制(很容易踩):

  1. 暂不支持创建 temporary table
  2. 暂不支持创建 partitioned table
  3. 默认 非原子:插入失败不会自动回滚删除已创建的表

10.1 想要“原子 CTAS”怎么办?

两件事同时满足:

  1. sink 侧实现了 CTAS 原子语义(通常依赖 staging 能力)
  2. 设置:
SET 'table.rtas-ctas.atomicity-enabled' = 'true';

11. RTAS / REPLACE TABLE:一条语句“替换并回填”

REPLACE TABLE AS SELECT / CREATE OR REPLACE TABLE AS SELECT 用于:

  • 表存在:直接替换(语义上像 drop + create + insert)
  • CREATE OR REPLACE:不存在就创建,存在就替换
REPLACE TABLE my_rtas_table
WITH ('connector'='kafka', ...)
AS
SELECT id, name, age
FROM source_table
WHERE MOD(id, 10) = 0;

RTAS 同样默认 非原子,并且语义上先 drop 再建再写;如果在 in-memory catalog 下,drop 只是从 catalog 移除,不一定删除物理数据(这点生产上要特别注意)。

想要原子 RTAS:同 CTAS,依赖 sink 支持 + table.rtas-ctas.atomicity-enabled=true

12. 其它 CREATE:Catalog / Database / View / Function / Model

12.1 CREATE CATALOG

CREATE CATALOG [IF NOT EXISTS] my_catalog
WITH ('type'='...', ...);

12.2 CREATE DATABASE

CREATE DATABASE [IF NOT EXISTS] my_db
WITH ('k'='v', ...);

12.3 CREATE VIEW(支持 TEMPORARY)

CREATE [TEMPORARY] VIEW [IF NOT EXISTS] my_view AS
SELECT ...

12.4 CREATE FUNCTION(JAVA/SCALA/PYTHON)

支持临时函数与系统临时函数:

CREATE TEMPORARY FUNCTION my_udf
AS 'com.xxx.MyUdf'
LANGUAGE JAVA;

USING JAR 当前只支持 JAVA/SCALA(把实现与依赖 jar 引入)。

12.5 CREATE MODEL(把“模型推理”注册成对象)

CREATE MODEL sentiment_analysis_model
INPUT (text STRING)
OUTPUT (sentiment STRING)
WITH (
'provider' = 'openai',
'endpoint' = 'https://api.openai.com/v1/chat/completions',
'api-key' = '<YOUR KEY>',
  'model' = 'gpt-3.5-turbo',
  'system-prompt' = '...'
  );

13. 最佳实践速查(建议你写进博客结尾)

  1. Kafka/Upsert/JDBC 这类更新流:优先声明 PRIMARY KEY ... NOT ENFORCED
  2. 事件时间作业:一定写 WATERMARK(并用合理乱序区间)
  3. 元数据列:offset 这类“只读”字段用 VIRTUAL,避免写出 schema 不一致
  4. 用 LIKE 做“复用定义 + 覆盖 options”,比复制粘贴更稳
  5. CTAS/RTAS 上生产前先确认:是否需要原子性?sink 是否支持 staging?
posted @ 2026-01-15 08:47  clnchanpin  阅读(33)  评论(0)    收藏  举报