Snowflake MigrationClickHouse Workshops

Snowflake vs ClickHouse

Di mana kedua engine berbeda dalam storage, compute, dan dialek SQL — serta idiom Snowflake mana yang tidak punya padanan langsung di ClickHouse.

Dokumen ini adalah referensi bagi partner yang bermigrasi dari Snowflake ke ClickHouse. Ia mencakup perbedaan arsitektur yang mendorong keputusan desain serta enam kesenjangan dialek SQL yang akan Anda temui pada workload NYC Taxi.


1. Perbandingan Arsitektur

Storage

Snowflake mengambil semua keputusan storage fisik untuk Anda. Data disimpan sebagai micro-partition kolumnar terkompresi di object storage cloud. Anda memilih ukuran warehouse dan struktur tabel; Snowflake menangani sisanya — clustering, compaction, dan pengelolaan file berjalan otomatis.

ClickHouse menuntut Anda mengambil keputusan storage fisik secara eksplisit. Saat membuat tabel, Anda menentukan:

  • engine (yang menentukan bagaimana data disimpan, di-merge, dan dideduplikasi)
  • ORDER BY (yang menjadi urutan sortir fisik dan primary index)
  • Opsional: PARTITION BY, TTL, SETTINGS (codec kompresi, perilaku merge)

Ini adalah keputusan kebenaran, bukan sekadar tombol tuning performa. Engine yang salah bisa menghasilkan hasil query yang diam-diam tidak benar. ORDER BY yang salah bisa membuat query yang seharusnya cepat justru memindai seluruh tabel.

Eksekusi Query

Snowflake memakai MPP shared-nothing dengan virtual warehouse. Sebuah warehouse adalah klaster node compute yang memproses query. Anda membayar warehouse selama ia berjalan — waktu idle pun menghabiskan kredit. Auto-suspend membantu, tetapi cold-start menambah latensi.

ClickHouse memakai eksekusi vektorisasi. ClickHouse Cloud melakukan auto-scale pada setiap compute service secara independen dan menskalakan ke nol saat idle. Beberapa compute service dapat berbagi storage yang sama (melalui SharedMergeTree) — inilah model pemisahan compute-compute milik ClickHouse Cloud, di mana setiap service adalah tier compute independen di atas satu lapisan data bersama.

Model Konkurensi

Snowflake mengisolasi workload dengan membuat warehouse terpisah. ETL memakai TRANSFORM_WH, analitik memakai ANALYTICS_WH. Setiap warehouse punya compute khusus; job ETL yang lambat tidak bisa membuat query analitik kelaparan.

ClickHouse Cloud mendukung pola yang sama melalui pemisahan compute-compute: Anda bisa menyediakan beberapa compute service yang berbagi storage yang sama. Setiap service adalah tier compute autoscaling yang independen — ETL berjalan di satu service, analitik interaktif di service lain, tanpa kontensi resource di antara keduanya. Di dalam satu service, isolasi workload dicapai lewat kuota lunak (max_threads, priority, max_memory_usage per pengguna atau per query) dan profil pengguna dengan batas resource. Untuk sebagian besar workload analitik yang query-nya selesai dalam milidetik, satu service sudah cukup dan kuota per query adalah opsi yang lebih ringan.

Model Biaya

SnowflakeClickHouse Cloud
ComputeKredit (warehouse-detik)Unit compute (terpisah dari storage)
Storage$23/TB/bulan~$0,023/GB/bulan (lebih murah)
Scale-to-zeroHanya auto-suspendScale-to-zero penuh didukung
Transfer dataIngress gratis; egress ditagihTarif egress cloud standar

Perbedaan paling signifikan: di Snowflake, Anda membayar waktu warehouse terlepas dari ada atau tidaknya query yang berjalan. Di ClickHouse Cloud, compute mengecil hingga nol di antara query. Untuk workload analitik yang bursty, ClickHouse Cloud biasanya 3-8x lebih murah daripada konfigurasi Snowflake yang setara.


2. Kesenjangan Dialek SQL

Workload NYC Taxi memuat enam konstruksi yang perlu diterjemahkan. Setiap satu di antaranya muncul di Q1–Q7 dalam 01-setup-snowflake/queries/.

Kesenjangan 1: QUALIFY

QUALIFY adalah ekstensi Snowflake yang memfilter baris berdasarkan hasil window function, mirip cara HAVING memfilter berdasarkan hasil agregat. Untuk migrasi ini, kami memperlakukan QUALIFY sebagai kesenjangan dialek dan menulisnya ulang memakai subquery — inilah pola portabel universal yang berfungsi di semua engine 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;

Mengapa ini penting: QUALIFY muncul di Q3. Penulisan ulang dengan subquery adalah pola yang aman dan portabel — ia berfungsi terlepas dari engine SQL target dan membuat hasil window function menjadi eksplisit. Bahaya dari sintaks apa pun yang spesifik Snowflake adalah mengasumsikan ia berpindah tanpa masalah; selalu uji setiap query sebelum menyatakan migrasi selesai.

Kesenjangan 2: Sintaks Colon-Path VARIANT

Tipe VARIANT milik Snowflake memakai notasi colon-path untuk mengakses field bersarang: column:field.subfield::TYPE. ClickHouse menyimpan data semi-terstruktur sebagai String dan mengekstraknya saat query memakai fungsi 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;

Keluarga JSONExtract* lengkapnya: JSONExtractFloat, JSONExtractInt, JSONExtractString, JSONExtractBool, JSONExtractKeys, JSONExtractArrayRaw, JSONExtractRaw. Gunakan JSONExtractRaw ketika Anda perlu objek atau array bersarang sebagai string untuk diproses lebih lanjut.

Mengapa bukan tipe JSON ClickHouse? Tipe JSON (sebelumnya eksperimental) tersedia di versi ClickHouse terkini tetapi punya semantik berbeda dan belum diperkeras untuk produksi pada semua kasus penggunaan. Untuk lab migrasi, String + JSONExtract* adalah pilihan yang aman dan dipahami.

Kesenjangan 3: LATERAL FLATTEN

LATERAL FLATTEN milik Snowflake membongkar array di dalam kolom VARIANT menjadi baris. ClickHouse tidak punya padanan langsung.

-- 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

Pendekatan pre-flatten (Opsi 2) lebih disukai ketika array punya skema yang terbatas dan diketahui. arrayJoin (Opsi 1) lebih disukai untuk query ad-hoc atau ketika panjang array bervariasi.

Kesenjangan 4: MERGE INTO

MERGE INTO milik Snowflake adalah mekanisme upsert utamanya. ClickHouse tidak punya pernyataan MERGE. Padanan ClickHouse yang benar bergantung pada engine tabel.

-- 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

Strategi inkremental delete_insert di dbt-clickhouse adalah padanan semantik terdekat dari MERGE INTO untuk model analitik. Ia menghapus baris yang sudah ada yang cocok dengan kunci mana pun dalam batch masuk, lalu menyisipkan seluruh baris masuk — secara atomik per partisi.

Gotcha kunci pada ReplacingMergeTree: Deduplikasi latar belakang bersifat asinkron. Di antara merge, baik versi lama maupun baru dari sebuah baris sama-sama ada di tabel. Selalu gunakan FINAL pada query yang harus mengembalikan tepat satu baris per kunci. Lihat Engine MergeTree untuk semantik deduplikasi lengkapnya.

Kesenjangan 5: Snowflake Streams (CDC)

Snowflake Streams melacak perubahan tingkat baris (INSERT, UPDATE, DELETE) pada sebuah tabel. Ia mengekspos kolom sistem METADATA$ACTION, METADATA$ISUPDATE, dan METADATA$ROW_ID. ClickHouse tidak punya mekanisme internal yang setara.

Padanan di ClickHouse: cutover producer langsung

ClickHouse tidak punya mekanisme CDC internal yang setara dengan Snowflake Streams. Untuk migrasi ini, polanya lebih sederhana daripada sebuah konektor CDC:

  • Bulk load dulu — scripts/02_migrate_trips.py membaca semua baris historis dari Snowflake secara batch dan menyisipkannya ke ClickHouse
  • Lalu cutover producer-nya — scripts/03_cutover.sh menghentikan producer Snowflake dan menjalankan producer ClickHouse yang menulis langsung ke ClickHouse Cloud
  • Tidak perlu jendela CDC — skrip migrasi menangani muatan historis, dan producer mengambil alih penulisan live; ReplacingMergeTree(_synced_at) pada trips_raw membuat percobaan ulang migrasi maupun percobaan ulang producer bersifat idempoten

Pasca-cutover, strategi delete_insert dbt menangani upsert untuk lapisan analitik. Snowflake Streams dan Tasks dipensiunkan sepenuhnya.

Kesenjangan 6: Fungsi Tanggal/Waktu

Snowflake dan ClickHouse punya nama fungsi tanggal yang berbeda. Sebagian besar hanyalah substitusi mekanis.

SnowflakeClickHouseCatatan
DATE_TRUNC('hour', ts)toStartOfHour(ts)Juga: toStartOfDay, toStartOfMonth, toStartOfWeek
DATE_TRUNC('day', ts)toDate(ts)
DATEADD('day', n, ts)ts + INTERVAL n DAYAtau addDays(ts, n)
DATEDIFF('minute', t1, t2)dateDiff('minute', t1, t2)Nama fungsi huruf kecil
CURRENT_DATEtoday()
CURRENT_TIMESTAMP()now()
TO_TIMESTAMP(epoch, 9)fromUnixTimestamp64Nano(epoch)Unit eksplisit di CH
YEAR(ts)toYear(ts)
MONTH(ts)toMonth(ts)
EXTRACT(epoch FROM ts)toUnixTimestamp(ts)

DateTime vs DateTime64: DateTime milik ClickHouse berpresisi detik. Gunakan DateTime64(3, 'UTC') untuk presisi milidetik (menyamai TIMESTAMP_NTZ milik Snowflake). Angka 3 adalah skala sub-detik; 'UTC' adalah zona waktunya.


3. Opsi Pemindahan Data

MetodeKapan dipakaiCatatan
Skrip migrasi Python (scripts/02_migrate_trips.py)Bulk load untuk Snowflake → ClickHouseKoneksi langsung via snowflake-connector-python + clickhouse-connect; bisa dilanjutkan; tanpa service tambahan — dipakai di lab ini
ClickPipesKafka, S3, Kinesis, PostgreSQL CDC, MySQL CDCKonektor terkelola; tidak mendukung Snowflake sebagai sumber
remoteSecure()Tarikan ad-hoc dari service ClickHouse lainTidak berlaku untuk sumber Snowflake
Relay object storageMuatan besar sekali jalanEkspor Snowflake → S3 → fungsi tabel S3 ClickHouse; butuh akun AWS dan penyiapan IAM
JDBC/ODBCPipeline ETL kustomFleksibel tetapi butuh orkestrasi kustom

Untuk lab ini, skrip migrasi Python adalah pilihan yang benar: ia tidak butuh layanan cloud tambahan (tanpa S3, tanpa Kafka), sepenuhnya bisa di-debug, dan memakai paket (snowflake-connector-python, clickhouse-connect) yang sudah dipasang partner untuk langkah lab lainnya.


4. Perbandingan Arsitektur CDC

Snowflake Streams + TasksClickHouse (lab ini)
Pelacakan perubahanObjek stream internal pada tabel (TRIPS_CDC_STREAM)Tidak ada padanan — producer menulis langsung ke ClickHouse pasca-cutover
Event perubahanMETADATA$ACTION: INSERT/UPDATE/DELETEINSERT langsung dari producer ClickHouse
LatensiJadwal task yang dapat dikonfigurasi (min 1 mnt)Interval batch yang dapat dikonfigurasi (default 10 s)
KonsumsiTask SQL membaca stream, mengirim ke targetProducer Python (producer/producer.py)
Perubahan skemaKoordinasi manualKode producer mengendalikan skema

Pasca-migrasi, producer menulis langsung ke ClickHouse — tidak perlu Streams atau Tasks. Strategi delete_insert dbt menangani upsert untuk lapisan analitik. Agregasi periodik (Snowflake Tasks) punya pengganti native ClickHouse berupa Refreshable Materialized View — proyek dbt lab ini menyertakan satu, analytics.mv_live_trip_feed, meskipun lab tidak mengaktifkan interval refresh-nya (lihat modul 05).


5. Menyelami Model Biaya

Snowflake: Berbasis Kredit

Satu kredit Snowflake berharga ~$3 (Enterprise). Biaya = ukuran_warehouse × waktu_berjalan. Warehouse SMALL menghabiskan 1 kredit/jam. MEDIUM menghabiskan 2. Auto-suspend minimum 60 detik berarti bahkan satu query saja berbiaya setidaknya 1/60 jam.

Untuk lab NYC Taxi (warehouse X-Small, 1 kredit/jam):

  • Setup Bagian 1: 2–4 kredit ($6–12)
  • Berjalan per sesi 8 jam: 4–8 kredit/hari ($12–24)
  • Resource monitor ANALYTICS_WH membatasi pada 50 kredit/bulan (~$150)

ClickHouse Cloud: Compute + Storage Terpisah

ClickHouse Cloud menagih compute dan storage secara terpisah:

  • Compute: tier Development sekitar ~$0,10/jam saat aktif, menskalakan ke nol saat idle
  • Storage: ~$0,023/GB/bulan (jauh lebih murah daripada $23/TB milik Snowflake)
  • ClickPipes: termasuk dalam langganan Cloud untuk sumber yang didukung (Kafka, S3, Kinesis, PostgreSQL CDC, MySQL CDC — bukan Snowflake)

Untuk lab NYC Taxi:

  • 50 juta baris × ~300 byte/baris tanpa kompresi = ~15GB → ~8GB terkompresi di ClickHouse
  • Biaya storage: ~$0,18/bulan
  • Compute selama lab Bagian 3 aktif (~2 jam): ~$0,20–0,40

Total biaya Bagian 3: ~$2–4 dibanding ~$6–12 milik Snowflake untuk sesi yang sama.

Perbedaan biaya inilah yang menjelaskan mengapa banyak organisasi mulai dengan Snowflake (operasi lebih sederhana) lalu bermigrasi ke ClickHouse (biaya lebih rendah + performa lebih tinggi) saat workload analitik mereka membesar.

Di halaman ini

ID