Snowflake MigrationClickHouse Workshops

Snowflake 与 ClickHouse 对比

两个引擎在存储、计算和 SQL 方言上的差异,以及哪些 Snowflake 惯用写法在 ClickHouse 中没有直接对应物。

本文是面向从 Snowflake 迁移到 ClickHouse 的合作伙伴的参考资料。它涵盖了驱动设计决策的架构差异,以及你在 NYC Taxi 工作负载中会遇到的六个 SQL 方言差异。


1. 架构对比

存储

Snowflake 替你做出所有物理存储决策。数据以压缩的列式微分区(micro-partition)形式存放在云对象存储中。你只需选择仓库规格和表结构;其余一切由 Snowflake 处理,聚簇、压实和文件管理都是自动的。

ClickHouse 要求你显式做出物理存储决策。创建表时,你需要指定:

  • engine(决定数据如何存储、合并和去重)
  • ORDER BY(它就是物理排序顺序和主索引)
  • 可选项:PARTITION BY、TTL、SETTINGS(压缩编解码器、合并行为)

这些是正确性决策,不是性能调优旋钮。选错引擎会静默产生错误的查询结果。选错 ORDER BY 会让本该很快的查询变成全表扫描。

查询执行

Snowflake 使用基于虚拟仓库的无共享 MPP 架构。一个仓库就是一组处理查询的计算节点。仓库运行期间你都要付费,空闲时间同样消耗 credit。自动挂起有所帮助,但冷启动会增加延迟。

ClickHouse 使用向量化执行。ClickHouse Cloud 会独立地为每个计算服务自动扩缩容,空闲时缩容到零。多个计算服务可以共享同一份存储(通过 SharedMergeTree),这就是 ClickHouse Cloud 的计算-计算分离模型,其中每个服务都是共用数据层之上的一个独立计算层。

并发模型

Snowflake 通过创建独立的仓库来隔离工作负载。ETL 使用 TRANSFORM_WH,分析使用 ANALYTICS_WH。每个仓库都有专属计算资源;一个缓慢的 ETL 作业不会拖垮分析查询。

ClickHouse Cloud 通过计算-计算分离支持同样的模式:你可以预置多个共享同一份存储的计算服务。每个服务都是一个独立的自动扩缩容计算层,ETL 跑在一个服务上,交互式分析跑在另一个上,彼此之间没有资源争用。在单个服务内部,工作负载隔离通过软配额(按用户或按查询的 max_threads、priority、max_memory_usage)和带资源限制的用户 profile 来实现。对于大多数查询在毫秒级完成的分析型工作负载,单个服务已经足够,按查询配额则是更轻量的选项。

成本模型

SnowflakeClickHouse Cloud
计算Credit(仓库运行秒数)计算单元(与存储分开计费)
存储$23/TB/月约 $0.023/GB/月(更便宜)
缩容到零仅自动挂起完整支持缩容到零
数据传输入站免费;出站收费标准云出站费率

最显著的差异是:在 Snowflake 中,无论是否有查询在跑,你都要为仓库运行时间付费。在 ClickHouse Cloud 中,查询之间计算资源会缩容到零。对于突发式分析型工作负载,ClickHouse Cloud 通常比同等配置的 Snowflake 便宜 3-8 倍。


2. SQL 方言差异

NYC Taxi 工作负载包含六种需要转换的构造。它们全部出现在 01-setup-snowflake/queries/ 的 Q1–Q7 中。

差异 1:QUALIFY

QUALIFY 是 Snowflake 的扩展语法,用于按窗口函数结果过滤行,类似于 HAVING 按聚合结果过滤。在本次迁移中,我们把 QUALIFY 视为方言差异,并用子查询重写它,这是在所有 SQL 引擎上都通用可移植的写法。

-- Snowflake
SELECT
    trip_id,
    pickup_at,
    fare_amount,
    ROW_NUMBER() OVER (PARTITION BY pickup_location_id ORDER BY fare_amount DESC) AS fare_rank
FROM fact_trips
WHERE pickup_at >= CURRENT_DATE - 7
QUALIFY fare_rank <= 10;

-- ClickHouse: wrap in a subquery
SELECT trip_id, pickup_at, fare_amount, fare_rank
FROM (
    SELECT
        trip_id,
        pickup_at,
        fare_amount,
        ROW_NUMBER() OVER (PARTITION BY pickup_location_id ORDER BY fare_amount DESC) AS fare_rank
    FROM analytics.fact_trips
    WHERE pickup_at >= today() - 7
)
WHERE fare_rank <= 10;

为什么这很重要: QUALIFY 出现在 Q3 中。子查询重写是安全、可移植的写法,无论目标 SQL 引擎是什么它都能工作,并且让窗口函数结果变得显式。任何 Snowflake 专有语法的危险之处在于假设它能无声无息地迁移过去;在宣布迁移完成之前,务必测试每一条查询。

差异 2:VARIANT 冒号路径语法

Snowflake 的 VARIANT 类型使用冒号路径记法访问嵌套字段:column:field.subfield::TYPE。ClickHouse 把半结构化数据存为 String,并在查询时用 JSONExtract* 函数提取。

-- Snowflake
SELECT
    trip_metadata:driver.rating::FLOAT  AS driver_rating,
    trip_metadata:app.version::STRING   AS app_version,
    trip_metadata:surge_multiplier::FLOAT AS surge
FROM trips_raw;

-- ClickHouse
SELECT
    JSONExtractFloat(trip_metadata, 'driver', 'rating')   AS driver_rating,
    JSONExtractString(trip_metadata, 'app', 'version')    AS app_version,
    JSONExtractFloat(trip_metadata, 'surge_multiplier')   AS surge
FROM default.trips_raw;

完整的 JSONExtract* 函数族:JSONExtractFloat、JSONExtractInt、JSONExtractString、JSONExtractBool、JSONExtractKeys、JSONExtractArrayRaw、JSONExtractRaw。当你需要把嵌套对象或数组以字符串形式取出以便进一步处理时,使用 JSONExtractRaw。

为什么不用 ClickHouse 的 JSON 类型? JSON 类型(此前为实验性)在较新的 ClickHouse 版本中已可用,但其语义不同,且尚未在所有使用场景下达到生产级成熟度。对于迁移实验课来说,String + JSONExtract* 是安全且被充分理解的选择。

差异 3:LATERAL FLATTEN

Snowflake 的 LATERAL FLATTEN 把 VARIANT 列内部的数组展开成多行。ClickHouse 没有直接对应物。

-- Snowflake: explode a VARIANT array into rows
SELECT t.trip_id, f.value:stop_name::STRING AS stop_name
FROM trips_raw t,
LATERAL FLATTEN(input => t.trip_metadata:route_stops) f;

-- ClickHouse Option 1: JSONExtract into Array, then arrayJoin
SELECT
    trip_id,
    arrayJoin(JSONExtract(trip_metadata, 'route_stops', 'Array(String)')) AS stop_name
FROM default.trips_raw;

-- ClickHouse Option 2: Pre-flatten the column during dbt staging
-- In stg_trips.sql, extract all array elements to separate columns
-- or use the dbt model to reshape the data at load time

当数组具有有界、已知的结构时,首选预展开方案(方案 2)。对于临时查询或数组长度可变的情况,首选 arrayJoin(方案 1)。

差异 4:MERGE INTO

Snowflake 的 MERGE INTO 是主要的 upsert 机制。ClickHouse 没有 MERGE 语句。正确的 ClickHouse 等价做法取决于表引擎。

-- Snowflake
MERGE INTO fact_trips t
USING staging_trips s ON t.trip_id = s.trip_id
WHEN MATCHED THEN UPDATE SET t.fare_amount = s.fare_amount, t.updated_at = s.updated_at
WHEN NOT MATCHED THEN INSERT VALUES (s.trip_id, s.pickup_at, ...);

-- ClickHouse with ReplacingMergeTree: just INSERT
-- RMT deduplicates by the ORDER BY key during background merges.
-- Use FINAL at query time to get the latest version:
INSERT INTO analytics.fact_trips SELECT * FROM staging_trips;

SELECT * FROM analytics.fact_trips FINAL WHERE trip_id = '...';

-- ClickHouse with dbt delete_insert incremental:
-- dbt handles the upsert by: DELETE WHERE key IN (new batch), then INSERT
-- This is the recommended approach for the analytics layer

对于分析型模型,dbt-clickhouse 中的 delete_insert 增量策略是在语义上最接近 MERGE INTO 的做法。它会删除与来批数据中任一键匹配的已有行,然后插入全部来批行,按分区原子地完成。

ReplacingMergeTree 的关键陷阱: 后台去重是异步的。在两次合并之间,一行的旧版本和新版本会同时存在于表中。对于必须每个键只返回一行的查询,务必使用 FINAL。完整的去重语义参见 MergeTree 引擎。

差异 5:Snowflake Streams(CDC)

Snowflake Streams 跟踪表上的行级变更(INSERT、UPDATE、DELETE)。它们暴露 METADATA$ACTION、METADATA$ISUPDATE 和 METADATA$ROW_ID 系统列。ClickHouse 没有等价的内部机制。

ClickHouse 的对应做法:生产者直接切换

ClickHouse 没有与 Snowflake Streams 等价的内部 CDC 机制。在本次迁移中,模式比使用 CDC 连接器更简单:

  • 先批量加载,scripts/02_migrate_trips.py 分批从 Snowflake 读取所有历史行并插入到 ClickHouse
  • 然后切换生产者,scripts/03_cutover.sh 停止 Snowflake 生产者,启动一个直接写入 ClickHouse Cloud 的 ClickHouse 生产者
  • 不需要 CDC 窗口,迁移脚本负责历史数据加载,生产者接管实时写入;trips_raw 上的 ReplacingMergeTree(_synced_at) 使任何迁移重试或生产者重试都是幂等的

切换完成后,dbt 的 delete_insert 策略负责分析层的 upsert。Snowflake Streams 和 Tasks 被完全弃用。

差异 6:日期/时间函数

Snowflake 和 ClickHouse 的日期函数名称不同。大多数是机械式替换。

SnowflakeClickHouse说明
DATE_TRUNC('hour', ts)toStartOfHour(ts)另有:toStartOfDay、toStartOfMonth、toStartOfWeek
DATE_TRUNC('day', ts)toDate(ts)
DATEADD('day', n, ts)ts + INTERVAL n DAY或 addDays(ts, n)
DATEDIFF('minute', t1, t2)dateDiff('minute', t1, t2)函数名小写
CURRENT_DATEtoday()
CURRENT_TIMESTAMP()now()
TO_TIMESTAMP(epoch, 9)fromUnixTimestamp64Nano(epoch)CH 中单位是显式的
YEAR(ts)toYear(ts)
MONTH(ts)toMonth(ts)
EXTRACT(epoch FROM ts)toUnixTimestamp(ts)

DateTime 与 DateTime64: ClickHouse 的 DateTime 精度为秒。需要毫秒精度时(对应 Snowflake 的 TIMESTAMP_NTZ)使用 DateTime64(3, 'UTC')。其中 3 是亚秒级标度;'UTC' 是时区。


3. 数据迁移方式选项

方式何时使用说明
Python 迁移脚本(scripts/02_migrate_trips.py)Snowflake → ClickHouse 的批量加载通过 snowflake-connector-python + clickhouse-connect 直连;可续跑;无需额外服务,本实验课采用
ClickPipesKafka、S3、Kinesis、PostgreSQL CDC、MySQL CDC托管连接器;不支持以 Snowflake 作为源
remoteSecure()从另一个 ClickHouse 服务临时拉取不适用于 Snowflake 源
对象存储中转大规模一次性加载从 Snowflake 导出 → S3 → ClickHouse S3 表函数;需要 AWS 账号和 IAM 配置
JDBC/ODBC自定义 ETL 管道灵活,但需要自建编排

对于本实验课,Python 迁移脚本是正确的选择:它不需要额外的云服务(不用 S3,不用 Kafka),完全可调试,并且使用的包(snowflake-connector-python、clickhouse-connect)合作伙伴在其他实验步骤中已经安装好了。


4. CDC 架构对比

Snowflake Streams + TasksClickHouse(本实验课)
变更跟踪表上的内部 stream 对象(TRIPS_CDC_STREAM)无等价物,切换后生产者直接写入 ClickHouse
变更事件METADATA$ACTION:INSERT/UPDATE/DELETE来自 ClickHouse 生产者的直接 INSERT
延迟可配置的 task 调度(最短 1 分钟)可配置的批次间隔(默认 10 秒)
消费方式SQL task 读取 stream 并写入目标Python 生产者(producer/producer.py)
结构变更手工协调由生产者代码控制结构

迁移后,生产者直接写入 ClickHouse,不需要 Streams 或 Tasks。dbt 的 delete_insert 策略负责分析层的 upsert。周期性聚合(Snowflake Tasks)在 ClickHouse 中的原生替代是可刷新 materialized view,本实验课的 dbt 项目提供了一个 analytics.mv_live_trip_feed,不过实验课并未开启它的刷新间隔(参见模块 05)。


5. 成本模型深入解析

Snowflake:基于 credit

一个 Snowflake credit 约合 $3(Enterprise 版)。成本 = 仓库规格 × 运行时间。SMALL 仓库每小时消耗 1 credit,MEDIUM 消耗 2。自动挂起最短为 60 秒,这意味着即便只跑一条查询,也至少要花掉 1/60 小时的费用。

对于 NYC Taxi 实验课(X-Small 仓库,1 credit/小时):

  • 第 1 部分的搭建:约 2–4 credit(约 $6–12)
  • 后续每次 8 小时会话:约 4–8 credit/天(约 $12–24)
  • ANALYTICS_WH 的资源监控器上限为每月 50 credit(约 $150)

ClickHouse Cloud:计算与存储分开

ClickHouse Cloud 对计算和存储分别计费:

  • 计算:Development 层活跃时约 $0.10/小时,空闲时缩容到零
  • 存储:约 $0.023/GB/月(显著低于 Snowflake 的 $23/TB)
  • ClickPipes:受支持的数据源(Kafka、S3、Kinesis、PostgreSQL CDC、MySQL CDC,不含 Snowflake)已包含在 Cloud 订阅中

对于 NYC Taxi 实验课:

  • 5000 万行 × 每行约 300 字节,未压缩约 15GB → 在 ClickHouse 中压缩后约 8GB
  • 存储成本:约 $0.18/月
  • 第 3 部分实验活跃期间(约 2 小时)的计算:约 $0.20–0.40

第 3 部分总成本:约 $2–4,而 Snowflake 同样一次会话约需 $6–12。

这一成本差异解释了为什么许多组织从 Snowflake 起步(运维更简单),随着分析型工作负载增长再迁移到 ClickHouse(成本更低 + 性能更高)。

本页内容

ZH