1. Flink 的 DataType 是什么?
在 Flink Table / SQL 里,DataType 代表的是"表生态中的逻辑类型(logical type)"。
典型例子:
text
INT
INT NOT NULL
INTERVAL DAY TO SECOND(3)
ROW<myField ARRAY<BOOLEAN>, myOtherField TIMESTAMP(3)>
只要你在用:
- Table API 定义 schema;
- 自定义 Connector / Catalog;
- 自定义 UDF 的输入输出类型;
你都会和 org.apache.flink.table.types.DataType 打交道。
1.1 逻辑类型 vs 物理表示
一个 DataType 有两层含义:
-
逻辑类型
- 表示在 SQL 世界里,这个字段是什么类型(INT、TIMESTAMP(3)、ARRAY 等);
- 自带 nullability 信息(
INTvsINT NOT NULL)。
-
可选的物理"提示(physical hint)"
-
告诉 Flink Planner:这个逻辑类型在 JVM/Python 世界里,希望用什么类来承载;
-
比如:
javaDataType t1 = DataTypes.TIMESTAMP(3).bridgedTo(java.sql.Timestamp.class); DataType t2 = DataTypes.ARRAY(DataTypes.INT().notNull()).bridgedTo(int[].class); -
Planner/runtime 会负责在内部格式与这些类之间做转换。
-
一般业务开发(用内置 Connector、内置函数),不用管 physical hints ;
只有你在写自定义 Connector 或高级 UDF 框架时才会用到。
2. 标量类型:字符串、数字、布尔、日期时间
2.1 字符串类型:CHAR / VARCHAR / STRING
- CHAR(n):定长字符,空格填充;
- VARCHAR(n):变长字符,最长 n 个 code point;
- STRING :等价于
VARCHAR(2147483647),即"几乎无限长"。
SQL 写法:
sql
name CHAR(10)
desc VARCHAR(255)
comment STRING
实战建议:
- 绝大多数场景直接用
STRING即可(省心); - 只有需要兼容传统数据库 schema、或者做严格长度控制(比如对接老系统)时,才用 CHAR/VARCHAR。
2.2 二进制类型:BINARY / VARBINARY / BYTES
- BINARY(n):定长字节数组;
- VARBINARY(n):变长字节数组;
- BYTES :等价于
VARBINARY(2147483647)。
用于:
- 原始协议数据;
- 图片/文件内容的直接存储;
- 各种编码后的 payload。
2.3 精确数值:DECIMAL、TINYINT、SMALLINT、INT、BIGINT
DECIMAL(p, s)
- 固定精度小数,p 为总位数,s 为小数位数;
1 <= p <= 38,0 <= s <= p;DECIMAL/NUMERIC/DEC都是同义词。
金额、费率、工资这种都用 DECIMAL,不要用 DOUBLE。
整数
TINYINT:1 字节,-128 ~ 127SMALLINT:2 字节,-32768 ~ 32767INT/INTEGER:4 字节BIGINT:8 字节(Javalong)
实战建议:
- 主键 / 自增 ID 通常用 BIGINT;
- 计数值不特别大时用 INT 即可。
2.4 近似数值:FLOAT / DOUBLE
FLOAT:4 字节单精度;DOUBLE:8 字节双精度;
适合:
- 统计指标(如平均值、方差);
- 不要求精确小数的场景。
金额、汇率之类仍然用 DECIMAL。
2.5 日期与时间:DATE / TIME / TIMESTAMP / TIMESTAMP_LTZ
DATE
- 年月日:
0000-01-01~9999-12-31 - 无时区信息。
TIME§
- 时分秒(带小数)
hh:mm:ss[.fractional],0 <= p <= 9; - 默认
TIME(0),只到秒。
TIMESTAMP§
- 时间戳(无时区),
yyyy-MM-dd HH:mm:ss[.fractional]; - 默认精度
p = 6,支持到微秒。
TIMESTAMP WITH TIME ZONE
- 每条记录里都存偏移量(+08:00 之类),真正"带时区"的时间戳;
- 对外系统交互时更直观,但存储开销更大。
TIMESTAMP_LTZ§(推荐重点关注)
- "带本地时区的时间戳":内部语义是
java.time.Instant,统一用 UTC 存; - 但在计算和显示时,会用会话的本地时区来解释;
- 兼顾了跨时区统一 & 业务展示友好,是现在最常用的类型之一。
实战经验总结:
- 事件时间(Event Time)优选
TIMESTAMP_LTZ; - 只需要逻辑时间、不牵扯时区的场景,
TIMESTAMP即可; - 如果你要和某些严格的外部系统对接(要求
TIMESTAMP WITH TIME ZONE),再考虑用后者。
3. INTERVAL:时间间隔类型
3.1 INTERVAL YEAR TO MONTH
"年-月"粒度的时间间隔:
sql
INTERVAL YEAR
INTERVAL YEAR(4)
INTERVAL YEAR TO MONTH
INTERVAL MONTH
内部统一表现为 +years-months,例如:
INTERVAL '50' MONTH会被表示为+04-02(4 年 2 个月)。
3.2 INTERVAL DAY TO SECOND
"天-秒"粒度的时间间隔,支持到纳秒级:
sql
INTERVAL DAY
INTERVAL DAY(6) TO SECOND(3)
INTERVAL HOUR TO SECOND(9)
INTERVAL MINUTE
INTERVAL SECOND(9)
...
内部表现为:+DD HH:MM:SS.fffffffff,例如:
INTERVAL '70' SECOND→+00 00:01:10.000000。
常见用途:
- 窗口大小、滑动步长(
INTERVAL '10' MINUTE); - TTL、超时等业务间隔。
4. 结构化与集合类型:ARRAY / MAP / MULTISET / ROW / STRUCTURED / VARIANT
4.1 ARRAY
同类型元素的有序数组:
sql
ARRAY<INT>
INT ARRAY -- 等价写法
最大长度 2,147,483,647,内部没有限制子类型,完全看你业务需求。
4.2 MAP
键值对映射:
sql
MAP<STRING, INT>
- key 和 value 都可以为 NULL;
- 不允许重复 key(逻辑上);
- SQL 标准没有这个类型,这是 Flink 自己加的扩展。
4.3 MULTISET(袋 / 多重集合)
可以有重复元素的集合:
sql
MULTISET<INT>
INT MULTISET -- 等价写法
用于表达:某个元素出现多少次的场景(比较偏统计/理论,实际业务中用得少)。
4.4 ROW
匿名"结构体"类型,一行中多个字段:
sql
ROW<id INT, name STRING>
ROW<myField INT '描述', myOtherField BOOLEAN '是否有效'>
等价写法:
sql
ROW(id INT, name STRING)
适合:
- 某些复杂嵌套结构(例如 JSON 内部有对象);
- UDF 返回多值时;
- 内部处理中间结果的 schema 表达。
4.5 STRUCTURED:用户自定义对象类型
和 ROW 相似,但它是命名类型(带有类名),哪怕字段完全一致,也被认为是不同类型:
sql
STRUCTURED<'com.myorg.Customer', id INT, name STRING, active BOOLEAN>
Java 里一般有两种方式定义:
-
用 POJO 让 Flink 反射推断:
javaclass Customer { public int id; public String name; public Map<String, String> properties; public boolean active; } -
显式声明:
javaDataTypes.STRUCTURED( Customer.class, DataTypes.FIELD("id", DataTypes.INT().notNull()), DataTypes.FIELD("name", DataTypes.STRING()) );
适合:
- 想在类型系统中区分不同"业务对象"(即使字段长得一样);
- 在 UDF / Connector 里用 POJO 来表示复杂结构。
4.6 VARIANT:半结构化数据(JSON 风格)
VARIANT 是用来存半结构化数据的类型:
- 可以存 ARRAY、MAP(key 为 STRING)、各种标量类型;
- 字段的 schema 是"跟着数据走"的,类似 JSON;
- 非常适合嵌套深 / schema 频繁演化的场景。
生成方式一般是:
sql
SELECT PARSE_JSON('{"a":1,"b":["a","b","c"]}') AS v;
-- v 的类型就是 VARIANT
适合的典型场景:
- 日志型 JSON,字段经常加减;
- 某些配置、埋点、扩展字段,结构不稳定。
5. RAW 与 NULL:黑盒类型与无类型 NULL
5.1 RAW:黑盒类型
RAW 能保存任意序列化后的对象,对 Flink 来说是一个"黑盒":
- 只有在边缘(Source/Sink、UDF)才会反序列化;
- Planner 不会对 RAW 里的内容做任何优化。
一般写法(SQL):
sql
RAW('class', 'serializerSnapshotBase64')
在 API 中:
java
// 提供 Class + TypeSerializer
DataTypes.RAW(MyPojo.class, mySerializer);
适合:
- 完全交给上层/下层系统处理的二进制数据;
- 自带序列化逻辑的复杂对象(但注意可维护性)。
5.2 NULL 类型
NULL 是一种只包含 NULL 值的类型:
sql
SELECT CAST(NULL AS NULL); -- 没太大用,仅用于桥接不确定类型
作用主要是:
- 表示"目前类型未知"的 NULL;
- 在某些 JSON/Avro 场景里充当占位型。
实际业务里用到的机会非常少。
6. Casting:CAST vs TRY_CAST
Flink 提供两种显式类型转换函数:
-
CAST(expr AS type)
- SQL 标准的普通 CAST;
- 如果转换失败,会抛异常,作业失败;
- 返回类型的 nullability 和输入保持一致。
-
TRY_CAST(expr AS type)
- Flink 扩展的"安全版 CAST";
- 如果转换失败,返回 NULL;
- 返回类型总是可 NULL。
例子:
sql
CAST('42' AS INT) -- 42, 类型为 INT NOT NULL
CAST(NULL AS VARCHAR) -- NULL, 类型为 VARCHAR
CAST('non-number' AS INT) -- 抛异常,作业失败
TRY_CAST('42' AS INT) -- 42, 类型为 INT
TRY_CAST(NULL AS VARCHAR) -- NULL, 类型为 VARCHAR
TRY_CAST('non-number' AS INT) -- NULL, 类型为 INT
-- 配合 COALESCE 做兜底:
COALESCE(TRY_CAST('non-number' AS INT), 0) -- 0, 类型为 INT NOT NULL
实战建议:
- 数据质量有保障(比如明确定义好的 schema)时用
CAST; - 面对脏数据、无结构 JSON 时用
TRY_CAST+COALESCE,避免作业被一条坏数据打挂。
另外还有一个配置:
table.exec.legacy-cast-behaviour= enabled
会启用旧版 CAST 行为(CAST 不抛异常,只返回 NULL),官方已经不推荐,新项目不要开。
7. 类型自动推断 & @DataTypeHint
在很多 API 场景(尤其是 Java/Scala UDF)里,Flink 会尝试根据你的类自动推断 DataType,比如:
| Java 类型 | Flink DataType |
|---|---|
java.lang.String |
STRING |
int / Integer |
INT / INT NOT NULL |
long / Long |
BIGINT / BIGINT NOT NULL |
java.sql.Date |
DATE |
java.time.LocalDate |
DATE |
java.sql.Timestamp |
TIMESTAMP(9) |
java.time.Instant |
TIMESTAMP_LTZ(9) |
byte[] |
BYTES |
T[] |
ARRAY |
Map<K,V> |
MAP<K,V> |
但自动推断并不是万能的:
- 有时缺少精度、scale 信息(DECIMAL);
- 有时你想强制用 RAW、或者 TIMESTAMP_LTZ,而不是默认类型。
这时可以用 @DataTypeHint 做"辅助注解"。
示例:
java
import org.apache.flink.table.annotation.DataTypeHint;
class User {
// 强制定义为 INT,而不是根据 Object 推断
public @DataTypeHint("INT") Object o;
// 指定 TIMESTAMP(3) 且用 java.sql.Timestamp 作为 bridging class
public @DataTypeHint(
value = "TIMESTAMP(3)",
bridgedTo = java.sql.Timestamp.class
) Object ts;
// 强制某个字段用 RAW
public @DataTypeHint("RAW") Class<?> modelClass;
// 统一指定 BigDecimal 的精度与小数位
public @DataTypeHint(
defaultDecimalPrecision = 12,
defaultDecimalScale = 2
) AccountStatement stmt;
// 遇到无法映射的类型时一律当 RAW 处理,而不是抛异常
public @DataTypeHint(allowRawGlobally = HintFlag.TRUE) ComplexModel model;
}
实战经验:
- 自定义 UDF / Connector 时,最好对关键字段显式加上
@DataTypeHint,避免因推断差异导致线上行为不一致; - Scala 中建议使用 boxed 类型 (
Integer而不是Int),避免 nullability 被推断成 NOT NULL 带来隐性问题。
8. 一些常用的类型选型建议(可直接当 checklist 用)
最后给一份可以直接落地的 mini checklist:
-
金额 / 工资 / 费率 / 单价
- 用
DECIMAL(p, s),推荐DECIMAL(18, 2)或更大; - 所有计算保持在 DECIMAL 里完成再输出。
- 用
-
事件时间(Event Time)
- 优选
TIMESTAMP_LTZ(3)+WATERMARK; - 源如果是 epoch 毫秒:
BIGINT ts+ 计算列TO_TIMESTAMP_LTZ(ts, 3)。
- 优选
-
日志 / JSON / 埋点
- 结构稳定:拆成多个独立字段 + ROW;
- 结构经常变:用
VARIANT(配合PARSE_JSON)或STRING+JSON_VALUE/JSON_QUERY。
-
复杂对象作为中间结果或 UDF 参数
- 能表达为 ROW/STRUCTURED 就尽量用 ROW/STRUCTURED;
- 实在没办法时再用 RAW,但要评估调试难度与演进成本。
-
类型转换
- 高质量数据 →
CAST; - 可能混着脏数据 →
TRY_CAST + COALESCE,防止作业挂。
- 高质量数据 →
结语
Flink SQL 的类型系统看起来"很像普通 SQL",但实际上:
- 多了
TIMESTAMP_LTZ、VARIANT、RAW、STRUCTURED等一堆现代场景必备的类型; - 把 nullability 和 physical hints 也纳入了统一的 DataType 模型;
- 再加上
CAST/TRY_CAST、@DataTypeHint等工具,能够完整打通 SQL 世界 ↔ JVM/Python 世界。
真正的最佳实践不是"类型越复杂越好",而是:
在业务语义和技术实现之间,选一个最合适的类型表达。