Flink SQL 数据类型从 INT 到 VARIANT 的完整实战指南

在 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 有两层含义:

  1. 逻辑类型

    • 表示在 SQL 世界里,这个字段是什么类型(INT、TIMESTAMP(3)、ARRAY 等);
    • 自带 nullability 信息(INT vs INT NOT NULL)。
  2. 可选的物理"提示(physical hint)"

    • 告诉 Flink Planner:这个逻辑类型在 JVM/Python 世界里,希望用什么类来承载

    • 比如:

      java 复制代码
      DataType 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 <= 380 <= s <= p
  • DECIMAL / NUMERIC / DEC 都是同义词。

金额、费率、工资这种都用 DECIMAL,不要用 DOUBLE。

整数

  • TINYINT:1 字节,-128 ~ 127
  • SMALLINT:2 字节,-32768 ~ 32767
  • INT/INTEGER:4 字节
  • BIGINT:8 字节(Java long

实战建议:

  • 主键 / 自增 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 里一般有两种方式定义:

  1. 用 POJO 让 Flink 反射推断:

    java 复制代码
    class Customer {
      public int id;
      public String name;
      public Map<String, String> properties;
      public boolean active;
    }
  2. 显式声明:

    java 复制代码
    DataTypes.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 提供两种显式类型转换函数:

  1. CAST(expr AS type)

    • SQL 标准的普通 CAST;
    • 如果转换失败,会抛异常,作业失败
    • 返回类型的 nullability 和输入保持一致。
  2. 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:

  1. 金额 / 工资 / 费率 / 单价

    • DECIMAL(p, s),推荐 DECIMAL(18, 2) 或更大;
    • 所有计算保持在 DECIMAL 里完成再输出。
  2. 事件时间(Event Time)

    • 优选 TIMESTAMP_LTZ(3) + WATERMARK
    • 源如果是 epoch 毫秒:BIGINT ts + 计算列 TO_TIMESTAMP_LTZ(ts, 3)
  3. 日志 / JSON / 埋点

    • 结构稳定:拆成多个独立字段 + ROW;
    • 结构经常变:用 VARIANT(配合 PARSE_JSON)或 STRING + JSON_VALUE/JSON_QUERY
  4. 复杂对象作为中间结果或 UDF 参数

    • 能表达为 ROW/STRUCTURED 就尽量用 ROW/STRUCTURED;
    • 实在没办法时再用 RAW,但要评估调试难度与演进成本。
  5. 类型转换

    • 高质量数据 → CAST
    • 可能混着脏数据 → TRY_CAST + COALESCE,防止作业挂。

结语

Flink SQL 的类型系统看起来"很像普通 SQL",但实际上:

  • 多了 TIMESTAMP_LTZVARIANTRAWSTRUCTURED 等一堆现代场景必备的类型;
  • nullabilityphysical hints 也纳入了统一的 DataType 模型;
  • 再加上 CAST/TRY_CAST@DataTypeHint 等工具,能够完整打通 SQL 世界 ↔ JVM/Python 世界

真正的最佳实践不是"类型越复杂越好",而是:

在业务语义和技术实现之间,选一个最合适的类型表达。

相关推荐
vi121231 小时前
ENVI 地形量化与植被指数反演
开发语言·python
rising start1 小时前
一、FastAPI入门
python·fastapi·端口
红树林071 小时前
渗透测试之sql注入--盲注
数据库·sql·安全·web安全
闲人编程1 小时前
Flask应用工厂模式:构建可扩展的大型应用
后端·python·flask·工厂模式·codecapsule·应用工厂
LitchiCheng1 小时前
Mujoco 检验 KDL 和 Pinocchio 运动学 FK 是否一致
人工智能·python
ZhengEnCi1 小时前
P3H1-Python-sys模块完全指南-系统参数与命令行参数处理利器
python
猫头虎1 小时前
如何解决pip install网络报错SSLError: TLSV1_ALERT_PROTOCOL_VERSION(OpenSSL过旧)问题
网络·python·scrapy·pycharm·beautifulsoup·pip·scipy
帮帮志1 小时前
05【AI大模型对话/创建项目】通过pycharm创建大模型项目,关联Anaconda环境
ide·人工智能·python·语言模型·pycharm
海边夕阳20061 小时前
【每天一个AI小知识】:什么是目标检测?
人工智能·python·深度学习·目标检测·机器学习·计算机视觉·目标跟踪