Snowflake MigrationClickHouse Workshops

03 Provisioning dan migrasi

Provisioning ClickHouse Cloud dengan Terraform, buat tabel target dari rencana Anda, dan pindahkan 50 juta baris dengan skrip migrasi Python yang dapat dilanjutkan.

Titik awal

Modul 02 selesai: migration-plan.md sudah terisi dengan setiap kotak di Completion Checklist-nya tercentang, dan producer Snowflake masih berjalan. setup.sh di modul ini memeriksa keberadaan file tersebut dan memberi peringatan jika hilang atau belum lengkap, tetapi ia tidak pernah memblokir — tidak ada di sini yang menghentikan Anda melanjutkan tanpanya, hanya pemahaman Anda sendiri tentang dua modul berikutnya yang terganggu. Siapkan sekitar 60 menit total, sekitar 40-50 menit di antaranya adalah transfer data tanpa pengawasan yang bisa Anda tinggalkan berjalan di latar belakang. Di sini juga pengeluaran trial ClickHouse Cloud mulai: provisioning layanan dan mengerjakan modul ini menghabiskan kurang lebih $1-2 kredit trial (seluruh lab berjalan sekitar $2-4 total).

Mengapa

Modul ini adalah tempat rencana menjadi nyata. Setiap keputusan yang Anda tulis ke migration-plan.md di modul 02 — engine MergeTree mana per tabel, kunci ORDER BY yang diturunkan dari workload query yang sesungguhnya, bagaimana konstruksi khas Snowflake diterjemahkan — diketikkan langsung ke DDL tabel di sini, bukan diturunkan ulang dari nol. ClickHouse tidak punya indeks yang bisa Anda tempelkan belakangan: jika sebuah kunci ORDER BY ternyata salah setelah 50 juta baris duduk di sebuah tabel, perbaikannya adalah reload penuh, bukan ALTER singkat.

Itu juga sebabnya gerbang lunak itu penting meski tidak bisa menghentikan Anda. Jika Anda menjalankan modul ini tanpa rencana yang lengkap, secara mekanis Anda tetap akan berhasil — dbt run tetap akan membuat fact_trips sebagai ReplacingMergeTree, skrip migrasi tetap akan memindahkan 50 juta baris — tetapi Anda tidak akan tahu mengapa engine itu dan bukan MergeTree biasa, mengapa sort key berbentuk seperti itu, atau bagaimana membenarkan percepatan benchmark ~6-9x yang ditunjukkan modul 04 nanti. Tabel penyelarasan keputusan di bawah memetakan setiap pilihan yang diimplementasikan modul ini kembali ke pertanyaan worksheet yang dijawabnya, sehingga Anda bisa memeriksa rencana Anda sendiri terhadapnya sebelum melakukan provisioning apa pun.

Konsep — di balik layar

Arsitektur target. Snowflake terus menulis perjalanan baru melalui producer perjalanan sementara skrip Python satu kali mem-backfill 50 juta baris yang sudah ada ke ClickHouse — kedua sistem berjalan berdampingan selama migrasi berlangsung, bukan cutover.

Aliran data migrasi: producer perjalanan Snowflake terus menulis ke TRIPS_RAW sementara skrip Python satu kali memindahkan 50 juta baris dalam batch 100.000 baris ke ClickHouse Cloud

Di sisi ClickHouse, trips_raw adalah tabel pendaratan tempat skrip menulis. dbt lalu membangun view staging dan sisa lapisan analytics di atasnya — skema yang dibuat modul ini, tetapi belum diisi selain trips_raw:

Sisi target ClickHouse: trips_raw pada ReplacingMergeTree mengisi view staging bangunan dbt, tabel fact dan dimensi, agregat per jam, dan sebuah dictionary zona

Legenda warna diagram:

  • Hijau — sumber data (producer perjalanan, sebelum dan sesudah cutover)
  • Biru — tabel Snowflake
  • Oranye — model dan pipeline dbt
  • Merah — tabel dan materialized view ClickHouse
  • Sian — dashboard Apache Superset
  • Panah putus-putus — aliran pasca-cutover

Mengapa skrip Python alih-alih konektor native. Ada beberapa metode untuk memindahkan data dari Snowflake ke ClickHouse. Lab ini memakai skrip batch Python — inilah alasannya, dibandingkan dengan alternatif lain:

MetodeCara kerjanyaMengapa tidak dipakai di sini
ClickPipes (sumber Snowflake)Konektor native ClickHouse Cloud — zero-ETL, UI terkelolaSnowflake bukan sumber ClickPipes yang didukung. ClickPipes mendukung Kafka, S3, Kinesis, PostgreSQL CDC, MySQL CDC, dan object storage.
Ekspor S3 → ClickPipes S3COPY INTO @stage mengekspor Parquet/CSV ke S3; konektor ClickPipes S3 memuatnya ke ClickHouseButuh bucket S3, role IAM, stage Snowflake, dan akun AWS. Menambah ~3 langkah setup sebelum data bergerak sama sekali. Layak di produksi tetapi terlalu banyak infrastruktur untuk sebuah lab.
Ekspor S3 → clickhouse-clientEkspor S3 yang sama, tetapi dimuat dengan INSERT INTO ... SELECT FROM s3(...)Prasyarat S3 yang sama. Juga menuntut partner mengelola pemecahan file dan kemampuan melanjutkan secara manual.
Snowflake → Kafka → ClickHouseStream CDC Snowflake mengisi topic Kafka; konektor ClickPipes Kafka meng-ingest-nyaPipeline streaming penuh — cocok untuk kebutuhan latensi di bawah satu menit di produksi. Cluster Kafka jauh terlalu berat untuk lingkungan lab.
Skrip Python (lab ini)snowflake-connector-python membaca dalam batch cursor 100 ribu baris; clickhouse-connect menyisipkan langsungTanpa infrastruktur tambahan di luar paket yang sudah dibutuhkan lab. Dapat dilanjutkan lewat --resume (watermark max(pickup_at)). Output progres real-time. ~40-50 mnt untuk 50 juta baris pada ~20 ribu baris/s — dapat diterima untuk latihan migrasi satu kali.

Mengapa skrip Python adalah pilihan tepat untuk lab ini:

  • Tidak butuh akun AWS. Pendekatan berbasis S3 menuntut pembuatan bucket, kebijakan IAM, dan external stage Snowflake — tiga langkah setup yang tidak berhubungan dengan ClickHouse.
  • Mandiri. Kedua paket (snowflake-connector-python, clickhouse-connect) dipasang ke venv yang sama dengan dbt. Tanpa layanan baru, tanpa kredensial baru.
  • Dapat dilanjutkan. --resume membuat skrip aman untuk diinterupsi dan dijalankan ulang. ReplacingMergeTree(_synced_at) memastikan sisipan duplikat saat percobaan ulang otomatis terdeduplikasi.
  • Transparan. Partner bisa membaca skripnya, memahami pemetaan kolomnya, dan mengadaptasinya untuk skema mereka sendiri — yang lebih mendidik daripada mengklik lewat wizard UI.

Menangani jeda migrasi. Producer Snowflake terus berjalan sementara skrip migrasi berjalan (~40-50 mnt). Setiap perjalanan yang ditulis ke Snowflake dalam jendela itu tidak ada di ClickHouse. Lab ini menutup jeda tersebut dengan pendekatan dua tahap pada saat cutover, yang dibahas langsung oleh modul 05:

  1. Hentikan producer Snowflake untuk membekukan dataset.
  2. Jalankan python scripts/02_migrate_trips.py --resume — hanya baris delta yang ditransfer (hitungan detik, bukan menit).
  3. Jalankan producer ClickHouse.

Dedup ReplacingMergeTree(_synced_at) yang sama yang menangani percobaan ulang migrasi juga menangani hal ini: jika ada baris yang bertumpang tindih antara eksekusi di modul ini dan tahap --resume berikutnya, _synced_at yang lebih baru menang.

Kapan Anda akan memilih S3 di produksi. Jika dataset > 500 juta baris, atau jika biaya query warehouse Snowflake untuk full table scan signifikan, jalur ekspor S3 lebih disukai: Snowflake mengekspor Parquet terkompresi secara paralel (jauh lebih cepat daripada satu cursor), dan ClickHouse juga bisa memuat dari S3 secara paralel. Pendekatan skrip Python di sini bekerja baik untuk skala lab.

Penyelarasan keputusan. Tabel di bawah adalah daftar keputusan yang sama dari Worksheet 1 (pemilihan engine), 2 (sort key), dan 3 (terjemahan skema) di migration-plan.md, disilangkan dengan apa yang sebenarnya dibangun lab ini — bandingkan dengan rencana Anda sendiri sebelum melakukan provisioning apa pun:

KeputusanYang Diimplementasikan Lab IniMengapa
engine trips_rawReplacingMergeTree(_synced_at)Skrip migrasi Python memakai INSERT batch yang mungkin diulang jika terinterupsi. _synced_at DateTime DEFAULT now() diset pada setiap INSERT, jadi baris yang diulang datang lebih lambat dan punya nilai _synced_at lebih tinggi — baris yang lebih baru menang saat dedup RMT, sehingga percobaan ulang bersifat idempoten. Percobaan ulang producer pasca-cutover juga aman karena alasan yang sama. stg_trips melakukan query dengan FINAL untuk menjamin satu baris per perjalanan.
engine fact_tripsReplacingMergeTree(updated_at)Perjalanan bisa dikoreksi (penyesuaian tarif); updated_at adalah kolom versi
engine agg_hourly_zone_tripsReplacingMergeTree(updated_at)Perhitungan ulang bergulir = pola upsert
engine tabel dim_*MergeTree()Reload penuh pada setiap eksekusi dbt; tanpa upsert
ORDER BY fact_trips(toStartOfMonth(pickup_at), pickup_at, trip_id)Q1-Q7 semuanya memfilter pada pickup_at; trip_id menjamin keunikan di level daun
ORDER BY agg_hourly_zone_trips(hour_bucket, zone_id)Kedua kolom muncul di semua query agregasi
VARIANT →String + JSONExtract*Mempertahankan JSON mentah; ekstraksi terjadi saat query
QUALIFY →Subquery yang membungkus ROW_NUMBER()ClickHouse sudah punya klausa QUALIFY native sejak v24.5, tetapi bentuk subquery diajarkan karena portabel ke versi ClickHouse dan engine SQL yang lebih tua atau tidak punya QUALIFY
MERGE INTO →Inkremental delete_insert di dbtStrategi upsert idiomatis dbt-clickhouse; menghindari penulisan ulang seluruh tabel

Langkah 1 — Provisioning cluster ClickHouse

setup.sh melakukan satu hal: menjalankan terraform apply dan menulis detail koneksi ke .clickhouse_state. Ia juga memeriksa ulang keberadaan migration-plan.md sebelum melakukan provisioning apa pun — lihat bagian Mengapa di atas — tetapi pemeriksaan itu hanya memberi peringatan, tidak pernah memblokir.

cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"

# Configure credentials
cp .env.example .env
vim .env
# Fill in: CLICKHOUSE_ORG_ID, CLICKHOUSE_TOKEN_KEY, CLICKHOUSE_TOKEN_SECRET, CLICKHOUSE_PASSWORD

# Provision
source .env && ./setup.sh

.env masuk gitignore — jangan pernah commit file itu.

Output yang diharapkan: Terraform membuat 2 resource (layanan + daftar akses IP) dalam sekitar 2-3 menit:

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:
clickhouse_host = "abc123xyz.us-east-1.aws.clickhouse.cloud"
clickhouse_port = 8443

Host dan port disimpan ke .clickhouse_state. Source file itu di terminal mana pun untuk mengambil koneksinya:

source .clickhouse_state

Verifikasi:

curl "https://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}/?query=SELECT+1" \
  --user "default:${CLICKHOUSE_PASSWORD}"
# Expected: 1

Langkah 2 — Buat tabel kosong

Pertama, buat trips_raw secara manual dengan engine yang benar. Skrip migrasi memuat data ke tabel ini pada Langkah 3 — tabel itu harus sudah ada dengan ReplacingMergeTree agar kolom versinya sudah siap sebelum baris apa pun masuk.

-- Run in the ClickHouse SQL console (cloud.clickhouse.com -> SQL console)
CREATE TABLE IF NOT EXISTS default.trips_raw (
    trip_id               String,
    vendor_id             UInt8,
    pickup_at             DateTime64(3, 'UTC'),
    dropoff_at            DateTime64(3, 'UTC'),
    passenger_count       UInt8,
    trip_distance_miles   Float32,
    pickup_location_id    UInt16,
    dropoff_location_id   UInt16,
    payment_type_id       UInt8,
    rate_code_id          UInt8,
    store_fwd_flag        String,
    fare_amount_usd       Float32,
    extra_amount_usd      Float32,
    mta_tax_usd           Float32,
    tip_amount_usd        Float32,
    tolls_amount_usd      Float32,
    total_amount_usd      Float32,
    ingested_at           DateTime64(3, 'UTC'),
    trip_metadata         String,
    _synced_at            DateTime DEFAULT now()
)
ENGINE = ReplacingMergeTree(_synced_at)
ORDER BY (pickup_at, trip_id);

_synced_at diset otomatis pada setiap INSERT. Jika skrip migrasi terinterupsi dan dijalankan ulang dengan --resume, baris duplikat untuk trip_id yang sama mungkin ada sesaat — RMT menyimpan baris yang lebih baru (_synced_at lebih tinggi). stg_trips melakukan query trips_raw FINAL untuk memaksa deduplikasi sebelum model hilir mana pun melihat datanya.

Selanjutnya, isi data referensi zona. Ini data statis (265 zona NYC TLC) yang dibaca stg_taxi_zones milik dbt sebagai source.

cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
source .env && source .clickhouse_state

clickhouse-client --host "${CLICKHOUSE_HOST}" --port 9440 --secure \
  --user default --password "${CLICKHOUSE_PASSWORD}" \
  --multiquery < scripts/00_seed_zones.sql

(Atau tempelkan isi scripts/00_seed_zones.sql langsung ke SQL console ClickHouse sebagai gantinya.)

Konfigurasikan profil dbt. dbt_project.yml proyek ini mendeklarasikan profile: 'nyc_taxi_ch'. Tanpa profil yang cocok di ~/.dbt/profiles.yml, dbt run langsung gagal dengan Could not find profile named 'nyc_taxi_ch'. Templatnya ada di workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse/dbt/nyc_taxi_dbt_ch/profiles.yml.example.

Modul 01 sudah menulis ~/.dbt/profiles.yml dengan profil nyc_taxi: untuk Snowflake, dan loop refresh di Langkah 4-nya terus melakukan query terhadap profil itu selama producer Snowflake berjalan. Jangan ganti file itu dengan templat ClickHouse — menimpanya dengan profiles.yml.example akan menghapus profil nyc_taxi: dan merusak loop refresh modul 01. Sebaliknya, buka templatnya dan gabungkan blok nyc_taxi_ch:-nya ke dalam ~/.dbt/profiles.yml yang sudah ada sebagai profil tingkat atas kedua, berdampingan dengan nyc_taxi::

nyc_taxi:        # from module 01 — leave this one alone
  target: dev
  outputs:
    dev:
      type: snowflake
      # ...

nyc_taxi_ch:      # add this block
  target: dev
  outputs:
    dev:
      type: clickhouse
      schema: nyc_taxi_ch
      host: "{{ env_var('CLICKHOUSE_HOST') }}"
      port: 8443
      user: "{{ env_var('CLICKHOUSE_USER', 'default') }}"
      password: "{{ env_var('CLICKHOUSE_PASSWORD') }}"
      secure: true

nyc_taxi_ch: membaca CLICKHOUSE_HOST, CLICKHOUSE_USER, dan CLICKHOUSE_PASSWORD dari environment melalui env_var(), jadi .env dan .clickhouse_state harus di-source sebelum perintah dbt apa pun di modul ini — dbt run di bawah sudah melakukannya. Seperti profil Snowflake, ~/.dbt/profiles.yml menyimpan kredensial dan masuk gitignore; jangan pernah commit file itu, dan penggabungan ini menambahkan satu set kredensial kedua ke file yang sudah menyimpan satu set.

Verifikasi:

cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse/dbt/nyc_taxi_dbt_ch"
source "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse/.clickhouse_state"
source "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse/.env"

dbt debug
# Expected: "All checks passed!" — confirms dbt found the nyc_taxi_ch profile and
# connected to ClickHouse

Lalu jalankan dbt run untuk membuat tabel analytics dan view staging:

cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse/dbt/nyc_taxi_dbt_ch"
source "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse/.clickhouse_state"
source "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse/.env"

dbt deps   # install packages (first run only)
dbt run    # creates analytics tables and staging views; all empty at this point

Diharapkan: ~8 model dibuat dalam waktu kurang dari 2 menit (semua tabel kosong).

Verifikasi:

# Check analytics tables were created
curl "https://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}/?query=SHOW+TABLES+IN+analytics" \
  --user "default:${CLICKHOUSE_PASSWORD}"
# Expected: agg_hourly_zone_trips, dim_date, dim_payment_type, dim_vendor, dim_taxi_zones, fact_trips

# Check staging views were created
curl "https://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}/?query=SHOW+TABLES+IN+staging" \
  --user "default:${CLICKHOUSE_PASSWORD}"
# Expected: stg_trips, stg_taxi_zones

# Check trips_raw exists with the correct engine
curl "https://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}/?query=SELECT+engine+FROM+system.tables+WHERE+database%3D%27default%27+AND+name%3D%27trips_raw%27" \
  --user "default:${CLICKHOUSE_PASSWORD}"
# Expected: ReplacingMergeTree

Keenam tabel analytics dan kedua view staging sekarang ada, tetapi semuanya masih kosong — dbt run hanya membuat skemanya. Satu-satunya tabel yang berisi data setelah langkah ini adalah trips_raw, dan ia pun belum punya apa-apa; itu datang berikutnya.

Langkah 3 — Migrasikan data

Muat semua baris dari Snowflake NYC_TAXI_DB.RAW.TRIPS_RAW ke ClickHouse default.trips_raw menggunakan skrip migrasi batch Python.

cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
source .env && source .clickhouse_state
source .venv/bin/activate
python scripts/02_migrate_trips.py

Output yang diharapkan (sekitar 40-50 menit untuk 50 juta baris):

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  NYC Taxi Migration: Snowflake -> ClickHouse
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  Rows to migrate: 50,000,000
  Batch size:      100,000

  Rows inserted    Elapsed      ETA                    Rate
  -------------------- ------------ ---------------------- ---------------
  100,000              0m 07s       56m 14s remaining      13,945 rows/s
  200,000              0m 14s       55m 28s remaining      14,021 rows/s
  ...

Producer Snowflake terus menulis ke TRIPS_RAW selama skrip ini berjalan, jadi ClickHouse tertinggal kurang lebih sepanjang durasi transfer ini — jeda itu diharapkan dan ditangani di modul 05, bukan di sini.

Jika skrip terinterupsi, jalankan ulang dengan --resume untuk melanjutkan dari checkpoint terakhir:

python scripts/02_migrate_trips.py --resume

--resume membaca max(pickup_at) dari ClickHouse dan melewati baris yang sudah dimuat, jadi selalu aman untuk menginterupsi skrip ini dan menjalankannya kembali — Anda tidak akan pernah berakhir dengan muatan sebagian yang tak bisa dipulihkan.

Cara memverifikasi bahwa Anda sudah selesai

cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
source .env && source .clickhouse_state

# Row count in trips_raw
curl "https://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}/?query=SELECT+count()+FROM+default.trips_raw" \
  --user "default:${CLICKHOUSE_PASSWORD}"
# Expected: approximately 50000000

# .clickhouse_state was written by setup.sh
ls -la .clickhouse_state

# Service is reachable
curl "https://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}/?query=SELECT+1" \
  --user "default:${CLICKHOUSE_PASSWORD}"
# Expected: 1

Kondisi akhir

Layanan ClickHouse Cloud hidup dan dapat dijangkau, dan .clickhouse_state sudah ditulis ke disk oleh setup.sh dengan CLICKHOUSE_HOST dan CLICKHOUSE_PORT. Semua tabel target dan view staging ada — default.trips_raw, kedua view staging (stg_trips, stg_taxi_zones), keenam tabel analytics (fact_trips, agg_hourly_zone_trips, dim_taxi_zones, dim_payment_type, dim_vendor, dim_date), dan materialized view yang refreshable analytics.mv_live_trip_feed — total tujuh objek di skema analytics, dibangun oleh dbt run modul ini. Setiap tabel analytics masih kosong kecuali mv_live_trip_feed, yang sudah menyimpan satu baris snapshot yang dihasilkan dbt run ketika membangun view tersebut. Selain itu hanya default.trips_raw yang berisi data: kurang lebih 50 juta baris, dipindahkan oleh skrip migrasi Python pada Langkah 3.

Producer Snowflake masih berjalan. Ia tidak pernah dihentikan di modul ini dan tidak berhenti di sini. Setiap perjalanan yang ditulis ke TRIPS_RAW milik Snowflake setelah batch terakhir skrip migrasi adalah baris yang tidak dimiliki ClickHouse, jadi ClickHouse sekarang tertinggal dari Snowflake kurang lebih sepanjang jendela migrasi (~40-50 menit, ditambah berapa pun waktu setup modul ini). Jeda itu nyata dan terus bertambah selama producer terus berjalan. Jangan tutup jeda itu di modul ini. Cutover di modul 05 menutupnya dengan sengaja, dalam langkah dua tahap terkendali yang mengukur besar jeda sebelum menghilangkannya — menghentikan producer atau menjalankan ulang skrip migrasi sekarang akan menghapus persis hal yang dibangun modul 05 untuk didemonstrasikan.

Di halaman ini

Track your progress?

Optional. We email a link to confirm your address; progress records once you open it.

Please use your work email address, not a personal one.

Progress tracking also requires accepting the current Terms of Service in Privacy settings.

ID