Snowflake原生表迁移Iceberg:决策与权衡(上)
DataHot 速览
本文分两部分,第一部分讨论为何以及如何将 Snowflake 原生表转换为 Iceberg。Snowflake 原生表在性能优化、灾备、复制和治理方面集成最完整,适合纯 Snowflake 负载。但企业环境需要 Spark、Trino、Athena、Databricks、DuckDB 等多引擎读写同一 Iceberg 表,避免 ETL 复制带来延迟和复杂度。Iceberg 生态更复杂、功能仍有差距,通常总拥有成本更高,需要权衡。
为什么值得关注:帮助数据平台团队理解 Snowflake 原生表与 Iceberg 的取舍,适合做湖仓开放格式选型与迁移决策。
本文目录 17 节
译文
AI 逐段翻译第 1 部分(共 2 部分)——本文涵盖你为什么可能转换为 Iceberg、你需要做出的决策以及如何处理这些决策。第 2 部分涵盖转换执行、验证和切换。
为什么要转换为 Iceberg?
Snowflake 原生表与 Snowflake 平台紧密集成:它们在性能优化、灾难恢复、复制以及全套治理功能等领域提供最完整的支持。对于完全在 Snowflake 内运行的工作负载,原生表通常是正确的选择。
但企业数据环境正变得越来越复杂,因此采用像 Iceberg 这样的开放表格式可以解决真实挑战,例如让多个引擎——Apache Spark、Trino、Amazon Athena、Databricks、DuckDB 以及许多其他引擎——能够读取、并在某些配置下写入相同的 Iceberg 表,而无需依赖 Snowflake 处理查询,也无需依赖 ETL 作业复制数据及其带来的复杂性和延迟挑战。
Snowflake 对这一趋势的回应是提供被广泛认为是 Apache Iceberg 生态系统最完整的支持,无论是作为 Iceberg 目录的管理者还是消费者。

不过转换并非没有权衡。Iceberg 生态系统的管理也更加复杂,有更多选项和配置需要考虑,通常涉及更多供应商和技术。它也不如“传统”Snowflake 表那样成熟和广为人知,这通常意味着工程师需要更长的学习和采用曲线。
该Apache Iceberg 项目由一个委员会管理,虽然这对于它所吸引的广泛供应商支持至关重要,但也意味着新功能和能力的采用可能比原生表更晚。
随着时间的推移,随着 Iceberg 格式的演进和 Snowflake 功能的发布,原生表与 Iceberg 表之间的功能差距不断缩小,但尚未为零。其中一些限制将在本文余下部分讨论,但你可以在此处和此处找到更多信息。
所有这些意味着,如果你没有利用其独特的互操作性优势,总拥有成本通常往往高于原生表。
因此,虽然在新项目和用例中开始使用 Iceberg 表可能非常简单,但将现有表转换为 Iceberg 需要更多规划。本文将涵盖你应该做出的设计选择,第 2 部分将聚焦于执行转换、验证结果以及切换消费者的策略。
转换前评估
选择从哪里开始
采用 Iceberg 时的第一反应通常是转换所有内容。但根据我的经验,应优先采用更有针对性的方法。
大多数数据平台都有多种表类型:原始摄取表、中间转换表、暂存区,最后是干净、宽表、面向消费者的数据集的展示层。转换为 Iceberg 的价值通常来自该管道末端的表——即外部引擎、BI 工具、机器学习管道或下游团队真正想要查询的表。转换其他所有内容会让你承担 Iceberg 的复杂性和维护开销,却几乎得不到回报。有时转换摄取表可能会带来新机会,但并非总是如此。
更好的做法是确定哪些表通过采用可互操作格式能释放最大价值,并从那开始。问问自己:你的外部引擎实际需要哪些表?哪些数据集能够启用你今天因数据被锁定在专有格式中而无法构建的用例——机器学习管道、跨平台分析层、数据网格产品?那些就是你的候选对象。
在项目管理和降低风险方面也有优势,以“波次”进行迁移可以实现更简单的回滚,涉及更少的利益相关者,并有助于改进和微调转换过程,从而使后续波次更具可重复性。

一旦你确定了哪些表在范围内,本节余下的问题将帮助你为每个表做出正确的设计决策。
谁将消费你的表?
哪些引擎或目录可能需要消费你的 Iceberg 表,以及预期它们将执行哪些操作?不同引擎具有不同的能力和限制,你在决定例如所使用的 Iceberg 版本、分区等时需要将这些考虑在内。
这一点尤其重要,因为其中若干选择在事后无法轻易更改。
我建议为范围内的每个表完成一个简单的消费者矩阵,例如:
注意: 尝试发现你的环境中当前正在使用的具体版本。例如,Trino 从 481 版本起才添加对 variant 的支持。
有了这些信息,你应该能够查明每个消费者(在其仍在使用的最旧版本下)如何支持:
- Iceberg 格式版本——仅 V2,还是也包括 V3。这为你可创建的表版本设定了上限;如果任何引擎仅支持 V2,那么无论 Snowflake 支持什么,该表都停留在 V2。
- 数据类型——特别是:无类型 VARIANT 和半结构化列(Iceberg V3 原生支持;V2 需要重新转换为结构化类型);timestamptz(Iceberg 仅存储 UTC 时刻,丢失偏移量);以及结构化类型(struct、list、map),大多数成熟引擎都能处理,但较旧版本可能无法处理。
- 分区规范裁剪——引擎是否主动使用 Iceberg 分区规范来跳过文件,还是简单地忽略它并进行全扫描。这决定了分区与聚簇之间的选择:如果引擎无法基于分区规范进行裁剪,那么相比 Snowflake 原生聚簇可能几乎没有优势。
- Parquet 编解码器——引擎能否解码 Snowflake 的 OPTIMIZED 编码(可能使用 LZ4_RAW)。如果不能,该表就需要 STORAGE_SERIALIZATION_POLICY = COMPATIBLE。
- 写操作 — 如果外部引擎需要回写到表(更新、删除、合并),请确认它使用 位置删除(V2)或 删除向量(V3)— 而不是相等删除。Snowflake 不支持相等删除文件。大多数现代引擎(Spark、Trino、Flink)可以配置为优先使用位置删除,但默认值因版本而异 — 如果写入在范围内,请明确验证这一点。
这也会引导你在执行完整迁移之前想要执行的冒烟测试。
如果你计划通过 Snowflake Horizon IRC — Polaris REST 端点 — 暴露表,还要确认每个连接引擎的源 IP 或 VPC 端点被 Snowflake 账户网络策略允许。这是独立于表格式选择的访问前提条件,但值得尽早解决。
数据类型兼容性
Iceberg 的类型系统比 Snowflake 的更窄,有些类型根本没有直接对应项。在你转换任何内容之前,值得了解哪些列需要关注。
对 SNOWFLAKE.ACCOUNT_USAGE.COLUMNS 运行此查询,以识别每个需要做出决定的列:
SELECT
table_catalog AS database_name,
table_schema,
table_name,
column_name,
data_type
FROM snowflake.account_usage.columns
WHERE data_type IN (
'VARIANT', 'OBJECT', 'ARRAY',
'GEOGRAPHY', 'GEOMETRY', 'VECTOR',
'TIMESTAMP_TZ'
)
AND table_schema != 'INFORMATION_SCHEMA'
AND deleted IS NULL
AND table_catalog = '<database>'
ORDER BY database_name, table_schema, table_name, ordinal_position;VECTOR 在 V2 或 V3 中都没有 Iceberg 对应项 — 在与负责人商定替代方案之前,不要开始转换包含此类型的表。原生向量类型将在 V4 中推出,因此这些类型有了一条路径。
TIMESTAMP_* 纳秒仅在 V3 中受支持。如果你的使用方只能使用 V2,你需要确认以微秒精度工作是否可以接受。
TIMESTAMP_TZ 在两个版本的 Iceberg 表中都不受支持,需要显式转换,本文稍后会讨论。
GEOGRAPHY、GEOMETRY 和 VARIANT 仅在 Iceberg V3 中原生受支持。
仅支持结构化 OBJECT 和 ARRAY(如 OBJECT(id INT, name STRING) 或 ARRAY(INTEGER))。它们能干净地映射到 Iceberg 的 struct 和 list。非结构化类型应转换为 Iceberg V3 上的 VARIANT。
有关完整的 Snowflake 到 Iceberg 类型映射,请参见 Apache Iceberg™ 表的数据类型。
默认值和序列
Iceberg 没有服务器端生成值的概念。如果你的任何列使用 CURRENT_TIMESTAMP()、seq.NEXTVAL 或任何其他非常量默认值,这些默认值将受到转换的影响。
在 Iceberg V3 中,使用不受支持的默认值创建表将引发硬错误。当目标为 V2 时,Snowflake 在其元数据上保留默认值,但它们仅在 Snowflake 执行写入时应用(因为 Iceberg 不支持它)。
要找到它们,请使用此查询:
SELECT
table_catalog AS database_name,
table_schema,
table_name,
column_name,
data_type,
column_default
FROM snowflake.account_usage.columns
WHERE column_default IS NOT NULL
AND table_schema != 'INFORMATION_SCHEMA'
AND deleted IS NULL
AND table_catalog = '<database>'
ORDER BY database_name, table_schema, table_name, ordinal_position;这主要是与表负责人的沟通,而不是技术修复。如果默认值将不受支持,任何依赖这些默认值的管道可能需要显式提供它。
另请参见:Iceberg v3 默认值支持。
TIMESTAMP_TZ
这仅适用于 TIMESTAMP_TZ 列。TIMESTAMP_LTZ 和 TIMESTAMP_NTZ 直接映射到 Iceberg 的 timestamptz 和 timestamp 类型,因此它们不需要任何操作。
问题在于时区偏移。Snowflake 的 TIMESTAMP_TZ 值存储瞬间 加上 记录时的偏移(例如 10:00 +05:00)。Iceberg 的 timestamptz 仅存储 UTC 瞬间 — 在 V2 或 V3 中都没有地方保留偏移。Snowflake 不会替你做出这个决定:在两个版本中,创建带有 TIMESTAMP_TZ 列的 Iceberg 表都会失败并报 Unsupported data type。
因此,对于每个 TIMESTAMP_TZ 列,你需要决定偏移会发生什么:
-- 1. Keep the instant, drop the offset (maps to Iceberg timestamptz)
-- Usually the right default: the moment in time is preserved exactly.
CAST("MY_COL" AS TIMESTAMP_LTZ(6)) AS "MY_COL" -- V2 or V3 (timestamptz)
CAST("MY_COL" AS TIMESTAMP_LTZ(9)) AS "MY_COL" -- V3 only, nanoseconds (timestamptz_ns)
-- 2. Keep the local wall-clock time, drop the offset (maps to Iceberg timestamp)
CAST("MY_COL" AS TIMESTAMP_NTZ(6)) AS "MY_COL" -- V2 or V3 (timestamp)
CAST("MY_COL" AS TIMESTAMP_NTZ(9)) AS "MY_COL" -- V3 only, nanoseconds (timestamp_ns)
-- 3. Keep the offset as text (maps to Iceberg string)
-- When the offset itself carries meaning, e.g. where an event happened.
TO_CHAR("MY_COL", 'YYYY-MM-DD HH24:MI:SS.FF9 TZH:TZM') AS "MY_COL"这里没有通用的正确答案 — 这取决于使用方如何处理该列。在运行转换之前,与表负责人商定选择。
排序规则
Snowflake 中的排序规则是查询引擎的概念,而不是文件格式的概念。Parquet 文件没有排序规则元数据。因此,虽然当查询通过 Snowflake 运行时,Snowflake 将继续强制执行你设置的任何排序规则,但任何直接读取 Parquet 文件的外部引擎将始终使用默认的字节序(UTF-8)比较 — 无论如何都是如此。
在实践中,这在你拥有外部引擎过滤或排序的带排序规则的字符串列时最为重要。同一表上的 Snowflake 查询与 Spark 或 Athena 查询之间的结果可能会无声地不同,这是一种容易遗漏且难以诊断的 bug。
运行此查询以查找任何具有非默认排序规则的表:
SELECT
table_catalog AS database_name,
table_schema,
table_name,
column_name,
data_type,
collation_name
FROM snowflake.account_usage.columns
WHERE collation_name IS NOT NULL
AND collation_name != ''
AND table_schema != 'INFORMATION_SCHEMA'
AND deleted IS NULL
AND table_catalog = '<database>'
ORDER BY table_schema, table_name, ordinal_position;如果被标记的表都没有外部引擎使用方,则无需操作。如果有,请向表负责人提出并记录该变更。
另请参见:Snowflake 管理的 Iceberg 表的排序规则。
流和变更数据捕获
如果范围内的任何表上有流,转换需要稍微多做一些规划。流确实在 Snowflake 管理的 Iceberg 表上工作,但你可以使用哪种 类型 的流取决于表的管理方式以及你选择的 Iceberg 版本。

由于转换原生表会产生一个 Snowflake 管理的 表,标准流和仅追加流都可供你使用。但如果此表上有外部写入,则适用两个限制:
- 当变更来自外部引擎时,仅追加流无法被消费。 这适用于 V2 和 V3。
- 在 V2 上,标准流将外部引擎的更新表示为 DELETE 后跟 INSERT,而不是更新。任何根据 METADATA$ISUPDATE 分支的下游使用方都会误读这些行。V3 正确处理了这一点,前提是外部引擎按照 V3 规范写入 — 新行的 _row_id = NULL,并且写时复制期间复制的行保留其 _row_id。
第二点值得在 Iceberg 版本 决策中权衡:如果你同时有流 和 外部写入者,V3 的表现明显更好。
第 2 部分将介绍作为迁移一部分重新创建流的过程。
表交换
如果你的任何 ETL 管道使用 ALTER TABLE … SWAP WITH(例如,原子性地用暂存表替换生产表),该工作流在转换后将失效。Iceberg 表不支持 SWAP WITH。
这是蓝绿部署或加载并交换(load-and-swap)流水线中的常见模式,因此值得尽早审计。你可以通过在查询历史中搜索作用域内表上的交换操作来找到受影响的表:
SELECT
query_text,
user_name,
start_time
FROM snowflake.account_usage.query_history
WHERE query_text ILIKE '%SWAP%WITH%'
AND execution_status = 'SUCCESS'
AND start_time >= DATEADD(month, -3, CURRENT_TIMESTAMP())
ORDER BY start_time DESC;如果你发现存在活跃的交换模式,那么该流水线需要在转换前或转换期间进行重构。最常见的替代方案有:
- INSERT OVERWRITE——原子性地替换目标中的所有行(或某个分区子集)。当目标是完全刷新时,它在精神上最接近交换操作。
- CREATE OR REPLACE ICEBERG TABLE … AS SELECT——从头重新创建表。更简单,但相对于并发读取者而言不是原子的,并且会重置元数据(流、授权)。
- 基于视图的路由模式——维护两个底层表,将一个视图指向“活跃”的那个,然后在加载完成后切换视图定义。
每种方案在原子性、授权保留和下游影响方面都有不同的权衡。关键点是把它暴露出来,并在转换时间之前决定采用哪种方法。
选择你的转换技术
在决定如何复制数据时,有两个重要因素需要考虑。被复制的数据量以及数据集中存在的数据类型。
被转换的数据量是决定你将如何处理转换的重要因素。如果你的总数据量大约在 1 到 2 TB 左右,直接的方法是 CREATE ICEBERG TABLE … AS SELECT——Snowflake 在一个语句中处理所有事情。
对于更大的表,与将数据导出为 Parquet 并通过以下方式直接添加到 Iceberg 表相比,那种更简单的方法在成本或时间上可能并不划算 COPY INTO ... LOAD_MODE=ADD_FILES_COPY:它在大规模下显著更快,并且完成所需的积分(credits)少得多。
快速的规模盘点有助于你做出这个判断:
SELECT
table_schema,
table_name,
ROUND(bytes / 1024 / 1024 / 1024, 2) AS size_gb,
row_count
FROM snowflake.account_usage.tables
WHERE table_catalog = '<database>'
ORDER BY size_gb DESC;不幸的是,转换为 Parquet 的中间步骤可能会引入精度损失,或对某些数据类型直接失败,尤其是当目标是 Iceberg V3 时,因为 Parquet 对以下类型没有相同的支持:
- VARIANT
- GEOGRAPHY
- GEOMETRY
- TIMESTAMP_NTZ(丢失微秒精度)
- OBJECT 和 ARRAY 数据类型也可能很棘手,并且需要显式转换
你可能已经注意到,前面评估中提到的数据类型存在很强的重叠。对于具有这些数据类型的表,我们建议尽可能采用 CREATE ICEBERG TABLE … AS SELECT方法。
两条路径的执行细节将在第 2 部分中介绍——在这个阶段,决策只是哪条路径适用于哪张表。
Iceberg 版本(V2 与 V3)
Snowflake 对 Apache Iceberg V3 的实现于 2026 年 5 月 7 日正式 GA,对于新工作而言,V3 通常就是正确的选择。但版本决定并非仅由 Snowflake 做出——如果你的任何外部引擎消费方尚不支持 V3,那么无论 Snowflake 支持什么,该表都应保持使用 V2。
按顺序逐一检查这些标准,并在第一个匹配处停止:

主要需要注意的是,外部引擎对 V3 的支持会按自己的时间表推进。一个声称支持 V3 的产品,可能并不支持你团队今天运行的那个版本——务必对照实际使用的版本进行验证。
另外,请注意“V3 支持”并不是全有或全无的命题,不同供应商对 Iceberg V3 规范中特定功能的支持程度各不相同,我们强烈建议你在承诺进行转换之前进行彻底测试。
参见:对 Apache Iceberg™ v3 的支持以获取完整功能列表和受支持的 Snowflake 功能。
分区与聚簇
这可能是整个过程中最微妙的决策,而且事后更改很可能非常耗时,因为分区和聚簇是互斥的。
这两种策略在底层的工作方式不同:分区使用 Iceberg 分区规范将分区列值写入 Iceberg 清单文件——大多数引擎读取该结构以跳过不相关的清单,而无需扫描所有文件。
聚簇不会创建分区规范,也完全不影响目录结构。它将具有相似聚簇键范围的文件放在一起,并将 最小/最大列统计信息写入 Iceberg 清单条目中。Snowflake 的查询引擎大量使用这些统计信息进行文件级剪枝;虽然外部引擎也会利用它们,但当清单级统计信息过滤可用时,一些引擎会应用更多优化。
分区的写入开销通常高于聚簇,因为后者是异步运行的。因此,如果符合以下情况,则优先使用聚簇:
- 你的表具有高容量/高频率的更新(尤其是流式)并且
- 你的关键工作负载运行在 Snowflake 上,或运行在能够基于列统计信息进行剪枝的外部消费方上
否则,分区很可能在更广泛的消费方中保证良好的性能。
关于聚簇键选择的指导,请参见 聚簇键与聚簇表。
存储序列化策略
STORAGE_SERIALIZATION_POLICY控制 Snowflake 如何编码和压缩它写入的 Parquet 文件,并且在创建表时即固定下来。默认值 OPTIMIZED 使用 Snowflake 特定的编码,可能包含 LZ4_RAW 之类的编解码器。这对于 Snowflake 内的查询性能和存储效率非常出色,但并非所有外部引擎都支持该编解码器。另一方面,COMPATIBLE 使用标准 Parquet 编解码器(如 Snappy),任何兼容 Iceberg 的引擎都可以读取。
所以规则很简单:如果任何外部引擎会读取某张给定表,就使用 COMPATIBLE。如果它仅由 Snowflake 使用,OPTIMIZED 就可以。
在 schema 或 database 级别设置它,这样你创建的每张表都会自动继承它——这远比必须在每张表上记住它要好得多:
ALTER SCHEMA <target_schema> SET STORAGE_SERIALIZATION_POLICY = COMPATIBLE;
ALTER DATABASE <database_name> SET STORAGE_SERIALIZATION_POLICY = COMPATIBLE;然后在开始之前验证实际生效的内容:
SHOW PARAMETERS LIKE 'STORAGE_SERIALIZATION_POLICY' IN SCHEMA <target_schema>;存储:Snowflake 管理 vs 客户管理
Snowflake 管理的 Iceberg 表支持两种存储选项。选择取决于谁拥有并运营该存储桶。
Snowflake 存储 (EXTERNAL_VOLUME = SNOWFLAKE_MANAGED)——Snowflake 为你存储和管理所有数据及元数据文件。外部引擎仍可通过 Horizon Catalog 读取这些表,但无需设置外部卷、定义 IAM 信任关系或维护存储桶。永久表像原生表一样获得 Fail-safe(7 天恢复)保护。
代价是:仅支持 AWS 和 Azure(不支持 GCP),不支持客户管理的加密密钥(CMK/Tri-Secret Secure),且你的 Snowflake 存储消耗仍由 Snowflake 计费。
外部卷存储 ——你自带 S3、ADLS 或 GCS 存储桶。你掌控加密、生命周期和位置。在三大云平台上均可使用。没有 Fail-safe(备份和版本控制由你自己负责)。你的云服务商向你收取存储费用;Snowflake 仅收取计算费用。代价是:设置更复杂(外部卷 + IAM 信任),且你需要自行负责保护数据文件。
如果你想要最简单的路径,且不需要 GCP、CMK,也不需要通过非 Iceberg 工具直接访问存储桶,请选择 Snowflake 存储。
如果你已有数据湖架构,需要直接访问原始文件,或有围绕密钥管理的合规要求,请选择外部卷。
如果你选择外部卷路线,外部卷 是指向你的云存储桶的命名 Snowflake 对象。在创建任何 Iceberg 表之前,你都需要先创建它,且执行转换的角色需要对其拥有 USAGE 权限。
SHOW GRANTS ON EXTERNAL VOLUME <external_volume_name>;也值得确认基础位置模式——默认情况下 Snowflake 使用 ${TABLE_CATALOG}/${TABLE_SCHEMA}/${TABLE_NAME},这通常没问题,但有些团队出于组织或成本分摊的原因更倾向于自定义前缀。
Iceberg 数据库默认值
在完成上述分析后,你可能会发现大多数表可能共享相同的设置。例如,你可能希望大多数 Iceberg 表使用 'COMPATIBLE' 作为序列化策略。在这种情况下,为简化开发并促进一致性,Snowflake 允许你在数据库或模式级别设置默认值,这样单个 DDL 语句就可以省略它们(某些参数也支持账户级默认值,但对于转换项目来说,数据库是实际可行的范围)。
关键参数:
ALTER DATABASE <db> SET CATALOG = 'SNOWFLAKE';
ALTER DATABASE <db> SET EXTERNAL_VOLUME = '<external_volume_name>';
ALTER DATABASE <db> SET ICEBERG_VERSION_DEFAULT = 3;
ALTER SCHEMA <db>.<schema> SET STORAGE_SERIALIZATION_POLICY = 'COMPATIBLE';一旦在数据库级别设置了 CATALOG = 'SNOWFLAKE',你还可以将 Iceberg 设为该范围内的 默认表格式:
ALTER DATABASE <db> SET DEFAULT_METADATA_WRITE_FORMAT = 'ICEBERG';这样一来,普通的 CREATE TABLE 就会生成 Iceberg 表——无需 ICEBERG 关键字。当迁移那些生成标准 DDL 且难以添加 Snowflake 特定语法的工具或管道时,这非常有用。CREATE ICEBERG TABLE 仍像以前一样可用。
有几点需要注意:将默认值设为 ICEBERG 不会转换现有的原生表。当默认值处于活动状态时,除非你临时将其改回 SNOWFLAKE,否则无法在该范围内创建标准的原生表。无论该设置如何,临时表始终以原生方式创建。
在开始转换之前设置这些默认值,只需五分钟,非常值得。它能消除一类复制粘贴错误,并使第 2 部分中的每条转换语句更简短、更易读。
转换前总结
在进入第 2 部分之前,你应该对以下每一项都有明确的答案。我们强烈建议你通过创建一个具有代表性的样本并测试你的消费者能否按预期操作它来验证这些决策。

本系列的第 2 部分将于稍后发布,涵盖转换执行、验证和切换。
如果你正在规划大规模转换,Snowflake 服务交付团队可提供专业工具和经验丰富的指导来加速这一过程。请联系你的 Snowflake 客户团队了解更多信息。
将 Snowflake 原生表转换为 Iceberg——第 1 部分 最初发表于 Snowflake Builders Blog: Data Engineers, App Developers, AI, & Data Science(Medium),人们在那里通过突出显示和回应这个故事来继续对话。
这篇内容对你有用吗?
反馈只用于改善内容筛选,不等同于收藏