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.
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:
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:
| Metode | Cara kerjanya | Mengapa tidak dipakai di sini |
|---|---|---|
| ClickPipes (sumber Snowflake) | Konektor native ClickHouse Cloud — zero-ETL, UI terkelola | Snowflake bukan sumber ClickPipes yang didukung. ClickPipes mendukung Kafka, S3, Kinesis, PostgreSQL CDC, MySQL CDC, dan object storage. |
| Ekspor S3 → ClickPipes S3 | COPY INTO @stage mengekspor Parquet/CSV ke S3; konektor ClickPipes S3 memuatnya ke ClickHouse | Butuh 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-client | Ekspor 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 → ClickHouse | Stream CDC Snowflake mengisi topic Kafka; konektor ClickPipes Kafka meng-ingest-nya | Pipeline 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 langsung | Tanpa 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.
--resumemembuat 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:
- Hentikan producer Snowflake untuk membekukan dataset.
- Jalankan
python scripts/02_migrate_trips.py --resume— hanya baris delta yang ditransfer (hitungan detik, bukan menit). - 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:
| Keputusan | Yang Diimplementasikan Lab Ini | Mengapa |
|---|---|---|
engine trips_raw | ReplacingMergeTree(_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_trips | ReplacingMergeTree(updated_at) | Perjalanan bisa dikoreksi (penyesuaian tarif); updated_at adalah kolom versi |
engine agg_hourly_zone_trips | ReplacingMergeTree(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 dbt | Strategi 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 = 8443Host dan port disimpan ke .clickhouse_state. Source file itu di terminal mana pun untuk
mengambil koneksinya:
source .clickhouse_stateVerifikasi:
curl "https://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}/?query=SELECT+1" \
--user "default:${CLICKHOUSE_PASSWORD}"
# Expected: 1Langkah 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: truenyc_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 ClickHouseLalu 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 pointDiharapkan: ~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: ReplacingMergeTreeKeenam 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.pyOutput 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: 1Kondisi 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.
Worksheet 5: Desain model dbt
Konfigurasikan materialisasi, engine, dan strategi inkremental untuk setiap model dbt, dengan umpan balik langsung pada setiap jawaban.
04 Bangun ulang pipeline dbt
Bangun ulang pipeline Medallion di ClickHouse dengan dbt-clickhouse — model inkremental delete_insert, ReplacingMergeTree, materialized view yang refreshable — dan buat dictionary zona.