4. Flink SQL 创建 Iceberg 表:语法、分区与主键

这一节我们直接上手干活。用 Flink SQL 在 Iceberg 上建表,说白了就是把 Hive 那套 DDL 搬到 Flink 里,但加了不少 Iceberg 自己的东西。我个人习惯先把表建出来跑通,再回头调优,这样心里有底。

4.1 基础 CREATE TABLE 语法

先看一个最简版本。你打开 Flink SQL Client,敲下面这段:

CREATE TABLE iceberg_catalog.my_db.log_events (
    event_id     STRING,
    event_time   TIMESTAMP(3),
    user_id      STRING,
    event_type   STRING,
    event_data   STRING
) WITH (
    'connector' = 'iceberg',
    'catalog-type' = 'hadoop',
    'warehouse' = 'hdfs://namenode:8020/warehouse'
);

嗯,这里要注意:catalog-type 我推荐用 hadoop 或 hive。如果你公司已经有 Hive Metastore,直接用 hive 类型,省事。我在项目中遇到过有人用 hadoop 类型但没配 HDFS 权限,结果建表成功但写数据报错——后来发现是 warehouse 路径没写对。

小技巧: 建表前先用 SHOW CATALOGS 确认你的 Iceberg catalog 已经注册到 Flink 里了。我经常看到新人直接建表,然后报错说 catalog 不存在。

4.2 分区策略:怎么分才不后悔

分区是 Iceberg 的核心能力之一。它跟 Hive 分区最大的区别是:Iceberg 隐藏了分区目录,你不需要在 SQL 里写 WHERE partition_col = xxx,Iceberg 会自动做分区裁剪。

语法上,分区定义写在 PARTITIONED BY 里:

CREATE TABLE iceberg_catalog.my_db.user_actions (
    action_id    STRING,
    user_id      STRING,
    action_time  TIMESTAMP(3),
    action_type  STRING,
    action_data  STRING
) PARTITIONED BY (
    days(action_time),
    action_type
) WITH (
    'connector' = 'iceberg',
    'catalog-type' = 'hadoop',
    'warehouse' = 'hdfs://namenode:8020/warehouse'
);

这里 days(action_time) 是 Iceberg 的隐藏分区转换——它不会真的在表里加一列,而是按 action_time 的天做分区。你想想看,如果按原始时间戳分区,每个毫秒一个分区,那还得了?

常用的分区转换有这些:

转换函数 适用类型 说明
years(ts) 时间戳/日期 按年分区,适合历史归档
months(ts) 时间戳/日期 按月分区,我常用在日志表
days(ts) 时间戳/日期 按天分区,最常用
hours(ts) 时间戳 按小时分区,数据量极大时用
bucket(n, col) 任意类型 哈希分桶,解决数据倾斜
truncate(len, col) 字符串 截断前 N 个字符做分区
避坑指南: 我曾经在一个项目里对 user_id 直接做分区,结果用户量一大,分区数爆炸,元数据查询慢得离谱。后来改成 bucket(100, user_id),问题解决。记住:分区数控制在几百到几千最合适,别搞出几十万个分区。

4.3 主键定义:UPSERT 的关键

Iceberg 从 v2 开始支持主键。主键的作用不是强制唯一约束,而是告诉 Iceberg:当数据进来时,按这些字段去重或更新。

语法很简单,在列定义后面加 PRIMARY KEY:

CREATE TABLE iceberg_catalog.my_db.user_profile (
    user_id      STRING,
    user_name    STRING,
    email        STRING,
    update_time  TIMESTAMP(3),
    status       STRING,
    PRIMARY KEY (user_id) NOT ENFORCED
) WITH (
    'connector' = 'iceberg',
    'catalog-type' = 'hadoop',
    'warehouse' = 'hdfs://namenode:8020/warehouse',
    'write.upsert.enabled' = 'true'
);

注意 NOT ENFORCED 这个关键字。它表示 Iceberg 不会在写入时检查主键唯一性——检查成本太高了。你想想看,如果每次写入都要全表扫描去重,那还怎么玩?所以 Iceberg 选择信任上游,由 Flink 或业务层保证数据不重复。

另外,write.upsert.enabled 必须设为 true,否则主键不会生效。我见过有人配了主键但忘了开这个参数,结果数据一直追加,没有去重——排查了半天才发现。

核心要点: 主键 + UPSERT 模式 = 流式 CDC 入湖的标准姿势。Flink 从 Kafka 读变更数据,按主键写入 Iceberg,天然支持更新和删除。

4.4 完整示例:一张实战表

把上面三个知识点串起来,给你一个我在生产环境用过的表结构:

CREATE TABLE iceberg_catalog.ods.order_events (
    order_id      STRING,
    user_id       STRING,
    product_id    STRING,
    amount        DECIMAL(10, 2),
    order_status  STRING,
    event_time    TIMESTAMP(3),
    proc_time     AS PROCTIME(),
    PRIMARY KEY (order_id) NOT ENFORCED
) PARTITIONED BY (
    days(event_time)
) WITH (
    'connector' = 'iceberg',
    'catalog-type' = 'hive',
    'warehouse' = 'hdfs://namenode:8020/warehouse/ods',
    'write.upsert.enabled' = 'true',
    'write.format.default' = 'parquet',
    'write.target-file-size-bytes' = '134217728'
);

这里我加了几个生产参数:

  • write.format.default 设成 parquet,压缩比高,查询快。
  • write.target-file-size-bytes 设成 128MB,避免小文件问题。这个值我调过很多次,128MB 是性价比比较高的。
  • proc_time 是处理时间,用于 Flink 的窗口计算,跟 Iceberg 本身无关,但经常一起用。

4.5 知识体系总览

下面这张图帮你理清 Flink SQL 创建 Iceberg 表的核心逻辑:

Flink SQL 创建 Iceberg 表核心流程 Flink SQL Client 列定义 + 数据类型 PARTITIONED BY 分区 PRIMARY KEY 主键 WITH 参数:connector / catalog-type / warehouse 时间分区:years/months/days/hours 哈希分桶:bucket(n, col) 截断分区:truncate(len, col) 主键 + write.upsert.enabled=true → UPSERT 模式 Iceberg 表:支持 ACID + 时间旅行 分区数控制在几百到几千 主键 NOT ENFORCED 不检查唯一性

从这张图你可以看到,建表不是简单敲一行 DDL 就完事。列定义、分区策略、主键、WITH 参数,每个环节都有讲究。我个人建议你从简单开始,先不加分区和主键,跑通基础链路,再逐步加上这些特性。

我的习惯: 每次建新表前,我都会先问自己三个问题:数据量多大?查询模式是按时间还是按用户?需不需要更新?答案决定了分区和主键怎么选。别一上来就照搬别人的表结构。