05 ベンチマークとカットオーバー
ダッシュボードを ClickHouse 上に再構築し、7本のクエリすべてを両エンジンでベンチマークし、プロデューサーをカットオーバーし、パリティを検証し、環境を撤去します。
開始チェックポイント
モジュール04が完了していること。analytics レイヤーにデータが入り、テスト済みであること。
analytics.fact_trips はおよそ5,000万行を保持し、analytics.dim_taxi_zones、
analytics.dim_payment_type、analytics.dim_vendor、analytics.dim_date は完全に読み込まれ、
dbt test は端から端まで成功し、analytics.taxi_zones_dict は稼働して dictGet() 経由で
borough を返します。analytics.agg_hourly_zone_trips はまだ空です。これは不具合ではなく設計
どおりで、このモジュールのカットオーバーまでそのままです。Snowflake のプロデューサーはまだ動いており、
Snowflake と ClickHouse の間のギャップも開いたままです。約45分を見込んでください。
なぜ必要か
ここまでのすべてのモジュールは準備でした。モジュール03は ClickHouse が5,000万行を保持できることを 示し、モジュール04は dbt パイプラインがその上で動くことを示しました。しかしそのどちらも、それ 単体ではパートナーが「移行完了」として承認できるものではありません。それにはもう2つ必要です。 数字と、カットオーバーです。
数字とは手順2のベンチマークです。マイグレーション計画にある同じ7本のクエリを、Snowflake と ClickHouse に対して連続して実行し、それぞれ3回の中央値を取ります。それが「ClickHouse は速いはずだ」を、 パートナーが自社のステークホルダーに提示できる、具体的で裏付けのある高速化に変えるものです。
手順3のカットオーバーがもう半分です。ここまでのすべてのモジュールでは、Snowflake を正式な記録 システム、ClickHouse をその後ろで追いつく側として、2つのシステムを並走させてきました。書き込み 経路を実際に移さないマイグレーションは、コピーであってマイグレーションではありません。手順3では Snowflake のプロデューサーを停止し、モジュール01以来その継続的な書き込みが開いたままにしてきた ギャップを閉じ、代わりに新しい trip を ClickHouse に書き始めます。ClickHouse が記録システムになる 瞬間です。
これはまた、モジュール06の筆記アセスメントの前の最後の受講者モジュールでもあります。その アセスメントは、ここで作ったものを参照するオープンブック形式です。手順5で何かを撤去する前に、 ダッシュボード、ベンチマークの CSV、パリティチェックはすべて実在している必要があります。
概念 — 内部の仕組み
BI レイヤーの機構。 手順1の bash superset/add_clickhouse_connection.sh は Superset の REST API を
直接呼び出します。Superset の UI で手動のクリック操作をする必要はありません。これは ClickHouse の
接続を登録し、それからコミット済みのダッシュボードのエクスポートをインポートして、モジュール01が
作った3つの Snowflake ダッシュボードと並べて4つの ClickHouse ダッシュボードを追加します(合計7つ)。
| ダッシュボード | 対応するもの | 何を示すか |
|---|---|---|
| CH — Operations Command Center | Snowflake ダッシュボード1 | 稼働中の fact_trips データ(カットオーバー後)。同じ KPI、より速いクエリ |
| CH — Executive Weekly Report | Snowflake ダッシュボード2 | QUALIFY を ROW_NUMBER() のサブクエリに書き換えたもの |
| CH — Driver & Quality Analytics | Snowflake ダッシュボード3 | Snowflake の LATERAL FLATTEN の代わりの JSONExtractString |
| CH — Capabilities Showcase | (新規 — Snowflake に対応物なし) | 近似関数、ディクショナリ結合、SAMPLE 句 |
7本のベンチマーククエリが試すもの。 手順2は、マイグレーション計画にある同じ7本のクエリを 両エンジンに対して実行し、実時間を比較します。それぞれがモジュール02の計画にある特定の方言の ギャップやエンジンの機能を狙っています。
| クエリ | 試すもの |
|---|---|
| Q1 | borough 別の時間別売上 |
| Q2 | ローリング7日間の平均距離 |
| Q3 | 上位10件の trip — Snowflake の QUALIFY と ClickHouse の ROW_NUMBER() サブクエリ |
| Q4 | ドライバー評価 — Snowflake の LATERAL FLATTEN と ClickHouse の JSONExtractString |
| Q5 | サージ料金 — Snowflake の VARIANT と ClickHouse の String + JSONExtract* |
| Q6 | 時間別集計 — Snowflake の MERGE と ClickHouse の ReplacingMergeTree |
| Q7 | CDC / ライブデータの鮮度 |
カットオーバーのギャップ。 Snowflake のプロデューサーはモジュール01以来、毎分約60件の trip を
書き込み続けており、一度も止まっていません。モジュール03のマイグレーションスクリプトは、その
スクリプトが実行された時点の TRIPS_RAW を捉えました。そしてモジュール04は、そのスナップショットの
上に dbt パイプラインを構築しました。マイグレーションスクリプトの最後のバッチ以降に Snowflake に
書き込まれた trip はすべて、Snowflake にしか存在しません。ClickHouse の末尾が欠けているのです。
手順3の --resume による追いつきパスは、まさにそのギャップを閉じます。ClickHouse にすでにある
max(pickup_at) を読み取り、それ以降に書き込まれた行だけを取ってくるので、モジュール01以来
開いていたギャップを閉じるパスも、元のバルクマイグレーションが要した40〜50分ではなく、数秒から
数分で終わります。これを飛ばしてカットオーバーすると、ClickHouse はギャップの間に着地した trip を
そのぶんだけ恒久的に落とします。手順4はまさにそれを捕まえるために作られていますが、それも手順3が
順番どおりに実行されている場合だけです。
agg_hourly_zone_trips はここで、そしてここでのみ埋まる。 モジュール03以来、設計どおり空でした。
増分フィルターが WHERE pickup_at >= now() - INTERVAL 2 HOUR であり、稼働中のプロデューサーが
書き込んだ行にしか一致せず、このモジュールまで書き込んでいたプロデューサーは Snowflake 側のものだけ
だったからです。手順3で ClickHouse のプロデューサーが起動すると、新しい行がついにその2時間の
ウィンドウの内側に着地し、このテーブル — そしてそれを使うすべてのダッシュボードのチャート — が
ラボで初めて空でなくなります。
手順1 — ClickHouse のダッシュボードを追加する
7つのデータセット、18のチャート、4つのダッシュボードを Superset の UI で1つずつ作る完全な手動構築は、 Superset on ClickHouse に従ってください。 手動の手順を飛ばして一気にすべてインポートする場合は次のようにします。
cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
source .env && source .clickhouse_state
bash superset/add_clickhouse_connection.shコミット済みの superset/dashboards/dashboard_export_*.zip は、ClickHouse のホストが
your-instance.clickhouse.cloud に置き換えられています。 上のショートカットはインポート前に
.env から URI をパッチするので、これは透過的に処理され、置き換えに気づくことはありません。
しかし、代わりに Superset の UI から ZIP を手動でインポートすると、作成されるデータベース接続は
接続できません。 その場合はあとからその接続を編集し、実際の CLICKHOUSE_HOST と認証情報を
指すようにする必要があります。具体的な方法は
Superset on ClickHouse を参照してください。
検証:
http://localhost:8088 を開きます(admin / admin)。Dashboards の下に合計7つ表示されるはずです。
Snowflake のダッシュボード3つと、CH — が接頭辞になっているもの4つです。
手順2 — ベンチマークを実行する
7本のクエリすべてを Snowflake と ClickHouse の両方に対して連続実行し、実時間を比較します。
cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
source .env && source .clickhouse_state
./scripts/run_benchmark.sh期待される出力:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
NYC Taxi Lab — Query Benchmark: Snowflake vs ClickHouse
(median of 3 runs each)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Query Snowflake ClickHouse Speedup
────────────────────────────────────────────────────────────────────────
Q1 Hourly revenue by borough 5.0s 0.7s 6x
Q2 Rolling 7-day avg distance 5.5s 0.8s 6x
Q3 Top 10 trips (QUALIFY→subquery) 5.0s 0.7s 6x
Q4 Driver ratings (JSON flatten) 5.4s 0.8s 6x
Q5 Surge pricing (VARIANT) 5.1s 0.7s 6x
Q6 Hourly aggregation (MERGE→RMT) 5.9s 0.8s 7x
Q7 CDC/live data freshness 7.9s 0.8s 9x
────────────────────────────────────────────────────────────────────────
Total 40.1s 5.6s 7x avg
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━この数値は代表的な1回の実行であり、保証ではありません。あなた自身の数値は、ウェアハウスのサイズ、 ClickHouse Cloud のティア、そしてそのときどちらのサービスに対して何が動いているかによって 変わります。
このスクリプトはすべての実行結果を
workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse/scripts/benchmark_results_<timestamp>.csv
に書き出します。ディスク上に残しておいてください。モジュール06のアセスメントが必要とする2つの
ファイルのうちの1つなので、手順5の撤去時に削除しないでください。
手順3 — ClickHouse にカットオーバーする
マイグレーションのギャップ。 Snowflake のプロデューサーはこのラボを通してずっと動き続け、
毎分約60件の trip を書き込んできました。モジュール03のマイグレーションスクリプトは、そのスクリプトが
実行された時点の TRIPS_RAW を捉えました。それ以降に書き込まれた行は Snowflake にしか存在しません。
書き込み経路をカットオーバーする前に、そのギャップを閉じてください。
下の4つの手順をすべて、順番に実行してください。2つのシステムの整合性を保つのはこの順番です。
cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
source .env && source .clickhouse_state
source .venv/bin/activate
# Step 1: Stop the Snowflake producer (freeze the dataset)
docker stop nyc_taxi_producer
# Step 2: Catch up the delta — only migrates rows with pickup_at newer than
# what's already in ClickHouse. Runs in seconds to minutes, not the original
# 40-50 minutes, because only the gap rows move.
python scripts/02_migrate_trips.py --resume
# Step 3: Refresh the analytics tables with the newly migrated rows
cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse/dbt/nyc_taxi_dbt_ch"
dbt run
cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
# Step 4: Start the ClickHouse producer
source .env && source .clickhouse_state
./scripts/03_cutover.shStep 2 を飛ばさないでください。 --resume は ClickHouse にすでにある max(pickup_at) を
読み取り、Snowflake のクエリに WHERE PICKUP_DATETIME > <watermark> のフィルターを追加するので、
モジュール03の元のマイグレーション実行中およびそれ以降に書き込まれた行 — ClickHouse が一度も
見ていない行 — だけを転送します。これなしでカットオーバーすると、ClickHouse はそのウィンドウに
着地した trip をそのぶんだけ恒久的に落とします。Step 4 のパリティチェックはまさにそれを捕まえる
ために作られていますが、それもこの手順が先に実行されている場合だけです。
./scripts/03_cutover.sh は Type "cutover" to confirm と尋ね、それから自身の安全網として
Step 1〜3 を繰り返します。Snowflake のプロデューサーを再度停止し(すでに停止済みなら何もしません)、
dbt run をもう一度実行し、それから ClickHouse のプロデューサー(nyc_taxi_ch_producer)を
ビルドして起動します。プロデューサー起動から30秒後に、新しい行が default.trips_raw に着地して
いることを確認し、dbt をもう一度実行します。この実行が、agg_hourly_zone_trips に初めて行を
与え、モジュール04が設計どおり開けたままにしていたギャップを閉じます。
検証:
-- Most recent trip should be within the last 60 seconds
SELECT max(pickup_at) AS most_recent_trip FROM default.trips_raw;
-- Row count should be increasing — wait 60 seconds and run again
SELECT count() FROM default.trips_raw;
-- agg_hourly_zone_trips should now have rows for the first time in the lab
SELECT count() FROM analytics.agg_hourly_zone_trips;docker ps | grep nyc_taxi_ch_producer # should show runninganalytics レイヤーを新鮮に保つ。 fact_trips と agg_hourly_zone_trips は dbt の増分モデルで、
自動リフレッシュはされません。03_cutover.sh はプロデューサーの稼働を確認したあと dbt run を
1回実行しますが、新しい trip が積み上がるにつれてダッシュボードは古くなります。最新の数値が
必要になったら、いつでも dbt/nyc_taxi_dbt_ch から dbt run を再実行してください(本番なら
cron、Airflow、dbt Cloud などでスケジュールしますが、ラボではオンデマンドで十分です)。一方
analytics.mv_live_trip_feed は リフレッシュ可能な materialized view です。モジュール04の
dbt run が engine = 'ReplacingMergeTree(refreshed_at)' ですでにビルドしていますが、ラボは
そのリフレッシュ間隔を一度も有効にしません。自律的に再実行させる
MODIFY REFRESH EVERY 30 SECOND 文は、モデルファイル内のコメントとしてのみ存在します。有効化するのは
自分で実行する1文の ALTER TABLE です。それなしでは、mv_live_trip_feed は dbt がビルドした
その1回だけ更新されます。
逆方向のカットオーバー、この手順を取り消して Snowflake のプロデューサーに戻す必要がある場合:
docker stop nyc_taxi_ch_producer
cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/01-setup-snowflake/superset"
docker-compose --env-file ../.env up -d producer手順4 — パリティを検証する
--resume の追いつきパスが完了し、ClickHouse のプロデューサーが稼働したので、両システムは
パリティに達しているはずです。確認しましょう。
cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
source .env && source .clickhouse_state
bash scripts/01_verify_migration.sh期待される出力:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Migration Parity Check
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ ClickHouse default.trips_raw: 50,008,250 rows
✓ Snowflake NYC_TAXI_DB.RAW.TRIPS_RAW: 50,008,250 rows
✓ Row count parity: PASS (difference: 0 rows = 0.0000%)
✓ trip_metadata populated: 50,008,250 non-empty rows
pickup_at range: 2022-03-30 2026-03-31
✓ ClickHouse has 50,008,250 rows — migration looks complete
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━あなた自身の行数は異なります。重要なのはパリティの行です。この時点で Snowflake のプロデューサーは
停止しているので、新しい行はそちらに着地しません。行数は正確に一致するはずですし、--resume の
パス中にバッチが飛行中だった場合でも数行の差にとどまり、スクリプトが判定する0.01%のしきい値の
十分内側に収まります。
パリティチェックが失敗した場合(差が0.01%を超える場合)、ギャップが完全に閉じていません。 追いつきパスをもう一度実行して再確認してください。
cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
source .env && source .clickhouse_state
python scripts/02_migrate_trips.py --resume
bash scripts/01_verify_migration.sh手順5 — 撤去する
何かを撤去する前に、マイグレーションが正しい最終状態にあることを確認してください。
| 確認項目 | コマンド | 期待される結果 |
|---|---|---|
| 行数のパリティ | bash scripts/01_verify_migration.sh | 行数が99.9%以上一致 |
| dbt のテスト | dbt test(dbt/nyc_taxi_dbt_ch から) | すべてのテストが成功 |
| Superset のダッシュボード | http://localhost:8088 を開く | 7つのダッシュボードが見える(SF 3つ + CH 4つ) |
| ベンチマークの結果 | cat scripts/benchmark_results_<timestamp>.csv | 7本のクエリすべてに高速化の値がある |
cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
source .env && source .clickhouse_state
bash scripts/01_verify_migration.sh
cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse/dbt/nyc_taxi_dbt_ch"
dbt test4つすべてが通れば、モジュール06に必要なものは2つのファイルだけで、どちらも撤去後も残ります。
workshop_public/snowflake_migration_lab/02-plan-and-design/migration-plan.md と
workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse/scripts/benchmark_results_<timestamp>.csv
です。モジュール06はオープンブックの筆記アセスメントで、約60分かかり、それ以外は何も必要
ありません。筆記試験の間、有料の ClickHouse Cloud サービスを動かし続ける理由はありません。
両方のファイルの内容を、後から手が届く場所にコピーするか記録しておき、それからすべてを撤去して
ください。
cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
source .env && ./teardown.shこれは ClickHouse Cloud のサービスを(terraform destroy 経由で)破棄し、カットオーバーを
実施していた場合は ClickHouse の trip プロデューサーのコンテナも破棄します。
パート1の Snowflake のリソースはこのスクリプトでは撤去されません。 Snowflake 側は別途 撤去してください。
cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/01-setup-snowflake"
source .env && ./teardown.sh完了の確認方法
このモジュールのこの時点までに、次を順番に確認しているはずです。
- パリティチェックの成功 — 手順4の
01_verify_migration.shが、行数の差0.01%未満で PASS を 報告した。 - ベンチマークの CSV がディスク上にある — 手順2が
benchmark_results_<timestamp>.csvを書き出し、 7本のクエリすべてに高速化の値があり、手順5の撤去の前に保存した。 - 7つのダッシュボードが存在する — 手順1の Superset の確認で、Snowflake のダッシュボード3つと
CH —のダッシュボード4つが並んで表示された。 - ClickHouse のプロデューサーが書き込んでいる — 手順3の検証ブロックで、
default.trips_rawの 行が増え、nyc_taxi_ch_producerが動作していることが、手順5で撤去のために停止する前に確認できた。
そのいずれかが当時成立していなかった場合は、いまこれらの確認を再実行するのではなく、対応する 手順に戻ってください。手順5はすでに ClickHouse Cloud のサービスを、そしてカットオーバーを 実施していた場合はプロデューサーのコンテナも一緒に破棄しています。
終了状態
マイグレーションが完了し、測定されました。5,000万行が Snowflake から ClickHouse に移り、パリティが 検証され、7本のクエリが正面から比較されてすべてで ClickHouse が速く、BI レイヤーは元の3つの Snowflake ダッシュボードと並ぶ4つの ClickHouse ダッシュボードで再構築され、書き込み経路は Snowflake から ClickHouse へ完全に移りました。両方のクラウド環境は撤去されています。ClickHouse Cloud のサービスも、ClickHouse のプロデューサーのコンテナもなく、パート1の撤去も実行済みであれば Snowflake のウェアハウスもありません。
撤去後も残り、モジュール06に必要なすべてである2つのファイルは、
workshop_public/snowflake_migration_lab/02-plan-and-design/migration-plan.md と
workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse/scripts/benchmark_results_<timestamp>.csv
です。モジュール06はオープンブックの筆記アセスメントです。その2つのファイルだけを持ってきてください。