Snowflake MigrationClickHouse Workshops

Snowflake と ClickHouse の比較

2つのエンジンがストレージ、コンピュート、SQL 方言でどう違うか — そして、どの Snowflake のイディオムに ClickHouse の直接的な等価物が存在しないか。

このドキュメントは、Snowflake から ClickHouse へ移行するパートナー向けのリファレンスです。設計上の判断を左右するアーキテクチャの違いと、NYC タクシーのワークロードで遭遇する6つの SQL 方言のギャップを扱います。


1. アーキテクチャの比較

ストレージ

Snowflake は物理ストレージに関する判断をすべて代わりに行います。データはクラウドオブジェクトストレージ上で、圧縮されたカラムナのマイクロパーティションとして保存されます。指定するのはウェアハウスのサイズとテーブル構造だけで、残りは Snowflake が処理します — クラスタリング、コンパクション、ファイル管理はすべて自動です。

ClickHouse では、物理ストレージに関する判断を明示的に下す必要があります。テーブルを作成するときに指定するのは次のとおりです。

  • エンジン(データがどう保存され、マージされ、重複排除されるかを決める)
  • ORDER BY(物理的なソート順とプライマリインデックスになる)
  • 任意で: PARTITION BY、TTL、SETTINGS(圧縮コーデック、マージの挙動)

これらは性能チューニングのつまみではなく、正しさに関わる判断です。エンジンを間違えると、クエリ結果が黙って不正になることがあります。ORDER BY を間違えると、速いはずのクエリがテーブル全体をスキャンすることになります。

クエリ実行

Snowflake は仮想ウェアハウスによるシェアードナッシング MPP を使います。ウェアハウスはクエリを処理するコンピュートノードのクラスタです。稼働している間ずっと課金され、アイドル時間もクレジットを消費します。オートサスペンドは助けになりますが、コールドスタートのレイテンシが加わります。

ClickHouse はベクトル化実行を使います。ClickHouse Cloud は各コンピュートサービスを独立にオートスケールし、アイドル時にはゼロまでスケールダウンします。複数のコンピュートサービスが同一のストレージを共有できます(SharedMergeTree 経由) — これが ClickHouse Cloud のコンピュート・コンピュート分離モデルであり、各サービスが共通のデータ層の上に立つ独立したコンピュート層になります。

同時実行モデル

Snowflake は別々のウェアハウスを作ることでワークロードを分離します。ETL は TRANSFORM_WH、分析は ANALYTICS_WH を使います。各ウェアハウスは専用のコンピュートを持つため、遅い ETL ジョブが分析クエリを飢餓状態にすることはありません。

ClickHouse Cloud は コンピュート・コンピュート分離 によって同じパターンをサポートします。同一のストレージを共有する複数のコンピュートサービスをプロビジョニングできます。各サービスは独立したオートスケーリングのコンピュート層であり、ETL は一方のサービスで、インタラクティブな分析はもう一方で動かせるため、両者の間でリソース競合が起きません。単一サービスの中では、ワークロードの分離はソフトクォータ(ユーザー単位またはクエリ単位の max_threads、priority、max_memory_usage)と、リソース制限を設定したユーザープロファイルによって実現します。クエリがミリ秒で完了するほとんどの分析ワークロードでは、単一サービスで十分であり、クエリ単位のクォータのほうが軽量な選択肢です。

コストモデル

SnowflakeClickHouse Cloud
コンピュートクレジット(ウェアハウス秒)コンピュートユニット(ストレージとは別)
ストレージ$23/TB/月約 $0.023/GB/月(より安い)
ゼロへのスケールオートサスペンドのみ完全なゼロスケールに対応
データ転送イングレスは無料、エグレスは課金標準のクラウドエグレス料金

最も大きな違いは、Snowflake ではクエリが走っているかどうかに関係なく ウェアハウスの稼働時間 に課金される点です。ClickHouse Cloud では、クエリの合間にコンピュートがゼロまでスケールダウンします。バースト的な分析ワークロードでは、ClickHouse Cloud は同等の Snowflake 構成に比べて通常3〜8倍安くなります。


2. SQL 方言のギャップ

NYC タクシーのワークロードには、書き換えが必要な構文が6つ含まれています。そのすべてが 01-setup-snowflake/queries/ の Q1〜Q7 に登場します。

ギャップ 1: QUALIFY

QUALIFY は Snowflake の拡張構文で、HAVING が集計結果で絞り込むのと同じように、window 関数の結果で行を絞り込みます。この移行では 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 エンジンに関係なく動作し、window 関数の結果を明示的にします。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 型(以前は experimental)は最近の 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

事前にフラット化するアプローチ(Option 2)は、配列のスキーマが既知で要素数に上限があるときに適しています。arrayJoin(Option 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 の重要な落とし穴: バックグラウンドでの重複排除は非同期です。マージが走るまでの間は、行の古いバージョンと新しいバージョンの両方がテーブルに存在します。キーごとにちょうど1行を返さなければならないクエリでは、必ず 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) により、マイグレーションのリトライやプロデューサーのリトライは冪等になる

カットオーバー後は、分析層の upsert を dbt の delete_insert 戦略が処理します。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(このラボ)
変更の追跡テーブル上の内部ストリームオブジェクト (TRIPS_CDC_STREAM)等価物なし — カットオーバー後はプロデューサーが ClickHouse に直接書き込む
変更イベントMETADATA$ACTION: INSERT/UPDATE/DELETEClickHouse プロデューサーからの直接 INSERT
レイテンシタスクのスケジュールで設定(最短1分)バッチ間隔で設定(デフォルト10秒)
消費側SQL タスクがストリームを読み、ターゲットへ出力Python プロデューサー (producer/producer.py)
スキーマ変更手作業での調整プロデューサーのコードがスキーマを制御

移行後は、プロデューサーが ClickHouse に直接書き込みます — Streams も Tasks も不要です。分析層の upsert は dbt の delete_insert 戦略が処理します。定期的な集計(Snowflake Tasks)には、ClickHouse ネイティブの代替として Refreshable Materialized Views があります。このラボの dbt プロジェクトには analytics.mv_live_trip_feed が1つ同梱されていますが、ラボではそのリフレッシュ間隔を有効化しません(モジュール05を参照)。


5. コストモデルの詳細

Snowflake: クレジット制

Snowflake のクレジットは約 $3(Enterprise)です。コスト = ウェアハウスサイズ × 稼働時間。SMALL のウェアハウスは1クレジット/時、MEDIUM は2クレジット/時を消費します。オートサスペンドの最短が60秒であるため、クエリが1本でも最低1/60時間分のコストがかかります。

NYC タクシーのラボ(X-Small ウェアハウス、1クレジット/時)の場合:

  • パート1のセットアップ: 約2〜4クレジット(約 $6〜12)
  • 以後8時間のセッションごと: 約4〜8クレジット/日(約 $12〜24)
  • ANALYTICS_WH のリソースモニターは月50クレジット(約 $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 タクシーのラボの場合:

  • 5,000万行 × 約300バイト/行(非圧縮) = 約15GB → ClickHouse では約8GB(圧縮後)
  • ストレージコスト: 約 $0.18/月
  • パート3のラボがアクティブな間(約2時間)のコンピュート: 約 $0.20〜0.40

パート3の合計コスト: 約 $2〜4 — 同じセッションで Snowflake なら約 $6〜12。

このコスト差は、多くの組織が Snowflake から始め(運用がより単純なため)、分析ワークロードが拡大するにつれて ClickHouse へ移行する(コストがより低く、性能がより高いため)理由を説明しています。

このページの内容

JA