Snowflake MigrationClickHouse Workshops

Snowflake เทียบกับ ClickHouse

จุดที่เอนจินทั้งสองต่างกันในด้านสตอเรจ, การประมวลผล และสำเนียง SQL — และสำนวน Snowflake ใดที่ไม่มีสิ่งเทียบเท่าโดยตรงใน ClickHouse

เอกสารนี้เป็นข้อมูลอ้างอิงสำหรับพาร์ตเนอร์ที่กำลังย้ายระบบจาก Snowflake มา ClickHouse ครอบคลุมความแตกต่างด้านสถาปัตยกรรมที่เป็นตัวขับการตัดสินใจด้านการออกแบบ และช่องว่างของสำเนียง SQL ทั้งหกจุดที่คุณจะพบในเวิร์กโหลดแท็กซี่ NYC


1. การเปรียบเทียบสถาปัตยกรรม

สตอเรจ

Snowflake ตัดสินใจเรื่องสตอเรจทางกายภาพทั้งหมดให้คุณ ข้อมูลถูกเก็บเป็น micro-partition แบบคอลัมน์ที่บีบอัดแล้วใน object storage บนคลาวด์ คุณเลือกขนาดของ warehouse และโครงสร้างตาราง ที่เหลือ Snowflake จัดการทั้งหมด — clustering, compaction และการจัดการไฟล์เป็นอัตโนมัติ

ClickHouse บังคับให้คุณตัดสินใจเรื่องสตอเรจทางกายภาพอย่างชัดแจ้ง เมื่อคุณสร้างตาราง คุณต้องระบุ:

  • เอนจิน (ซึ่งกำหนดว่าข้อมูลถูกเก็บ, merge และกำจัดข้อมูลซ้ำอย่างไร)
  • ORDER BY (ซึ่งกลายเป็นลำดับการเรียงทางกายภาพและ primary index)
  • ทางเลือกเพิ่มเติม: PARTITION BY, TTL, SETTINGS (compression codec, พฤติกรรมการ merge)

เหล่านี้เป็นการตัดสินใจเรื่องความถูกต้อง ไม่ใช่ปุ่มปรับจูนประสิทธิภาพ เอนจินที่ผิดสามารถให้ผลลัพธ์คิวรีที่ไม่ถูกต้องอย่างเงียบ ๆ ORDER BY ที่ผิดทำให้คิวรีที่ควรจะเร็วกลับสแกนทั้งตารางแทน

การประมวลผลคิวรี

Snowflake ใช้ MPP แบบ shared-nothing ร่วมกับ virtual warehouse warehouse คือคลัสเตอร์ของโน้ดประมวลผลที่รันคิวรี คุณจ่ายค่า warehouse ตลอดเวลาที่มันทำงาน — เวลาว่างก็กินเครดิต Auto-suspend ช่วยได้ แต่ cold-start เพิ่ม latency

ClickHouse ใช้การประมวลผลแบบ vectorized ClickHouse Cloud ปรับขนาด compute service แต่ละตัวโดยอัตโนมัติและอย่างเป็นอิสระ และลดลงถึงศูนย์เมื่อไม่มีงาน compute service หลายตัวสามารถใช้สตอเรจร่วมกันได้ (ผ่าน SharedMergeTree) — นี่คือโมเดล compute-compute separation ของ ClickHouse Cloud ที่แต่ละเซอร์วิสเป็นชั้นประมวลผลอิสระอยู่บนชั้นข้อมูลร่วมกัน

โมเดลการทำงานพร้อมกัน

Snowflake แยกเวิร์กโหลดออกจากกันด้วยการสร้าง warehouse แยกกัน ETL ใช้ TRANSFORM_WH งานวิเคราะห์ใช้ ANALYTICS_WH แต่ละ warehouse มีทรัพยากรประมวลผลของตัวเอง งาน ETL ที่ช้าจึงไม่สามารถแย่งทรัพยากรจนคิวรีวิเคราะห์อดตายได้

ClickHouse Cloud รองรับรูปแบบเดียวกันผ่าน compute-compute separation: คุณจัดสรร compute service หลายตัวที่ใช้สตอเรจร่วมกันได้ แต่ละเซอร์วิสเป็นชั้นประมวลผลอิสระที่ปรับขนาดเองได้ — ETL รันบนเซอร์วิสหนึ่ง งานวิเคราะห์แบบอินเทอร์แอกทีฟรันบนอีกเซอร์วิสหนึ่ง โดยไม่มีการแย่งทรัพยากรระหว่างกัน ภายในเซอร์วิสเดียว การแยกเวิร์กโหลดทำได้ด้วย soft quota (max_threads, priority, max_memory_usage ต่อผู้ใช้หรือต่อคิวรี) และ user profile ที่มีการจำกัดทรัพยากร สำหรับเวิร์กโหลดวิเคราะห์ส่วนใหญ่ที่คิวรีเสร็จในระดับมิลลิวินาที เซอร์วิสเดียวก็เพียงพอ และโควตาต่อคิวรีเป็นตัวเลือกที่เบากว่า

โมเดลค่าใช้จ่าย

SnowflakeClickHouse Cloud
การประมวลผลเครดิต (warehouse-วินาที)Compute unit (แยกจากสตอเรจ)
สตอเรจ$23/TB/เดือน~$0.023/GB/เดือน (ถูกกว่า)
Scale-to-zeroAuto-suspend เท่านั้นรองรับ scale-to-zero เต็มรูปแบบ
การถ่ายโอนข้อมูลIngress ฟรี; egress คิดเงินอัตรา egress มาตรฐานของคลาวด์

ความแตกต่างที่สำคัญที่สุด: ใน Snowflake คุณจ่ายค่า เวลาที่ warehouse ทำงาน ไม่ว่าจะมีคิวรีทำงานอยู่หรือไม่ ใน ClickHouse Cloud การประมวลผลจะลดลงถึงศูนย์ระหว่างคิวรี สำหรับเวิร์กโหลดวิเคราะห์ที่มาเป็นช่วงพุ่ง ClickHouse Cloud มักถูกกว่าคอนฟิก Snowflake ที่เทียบเท่ากัน 3-8 เท่า


2. ช่องว่างของสำเนียง SQL

เวิร์กโหลดแท็กซี่ NYC มีโครงสร้าง 6 อย่างที่ต้องแปล ทุกอย่างปรากฏอยู่ใน Q1–Q7 ใน 01-setup-snowflake/queries/

ช่องว่างที่ 1: QUALIFY

QUALIFY เป็นส่วนขยายของ Snowflake ที่กรองแถวด้วยผลของ window function คล้ายกับที่ HAVING กรองด้วยผลของ aggregate สำหรับการย้ายระบบครั้งนี้ เราถือว่า QUALIFY เป็นช่องว่างของสำเนียงภาษาและเขียนใหม่ด้วย subquery — นี่คือรูปแบบที่พอร์ตไปใช้ได้ทั่วไปกับเอนจิน 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 การเขียนใหม่เป็น subquery คือรูปแบบที่ปลอดภัยและพอร์ตได้ — มันทำงานได้ไม่ว่าเอนจิน SQL เป้าหมายจะเป็นตัวใด และทำให้ผลของ window function ชัดเจน อันตรายของไวยากรณ์เฉพาะของ Snowflake อยู่ที่การสมมติว่ามันย้ายไปได้เองอย่างเงียบ ๆ จงทดสอบทุกคิวรีเสมอก่อนจะประกาศว่าการย้ายระบบเสร็จสมบูรณ์

ช่องว่างที่ 2: ไวยากรณ์ colon-path ของ VARIANT

ชนิดข้อมูล VARIANT ของ Snowflake ใช้สัญกรณ์ colon-path ในการเข้าถึงฟิลด์ซ้อน: 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 เมื่อคุณต้องการอ็อบเจกต์หรืออาร์เรย์ซ้อนออกมาเป็นสตริงเพื่อประมวลผลต่อ

ทำไมไม่ใช้ชนิดข้อมูล JSON ของ ClickHouse? ชนิดข้อมูล JSON (ก่อนหน้านี้เป็น experimental) มีให้ใช้ใน ClickHouse เวอร์ชันใหม่ ๆ แต่มีความหมายเชิงพฤติกรรมต่างออกไปและยังไม่แข็งแรงพอสำหรับใช้งานจริงในทุกกรณี สำหรับแล็บย้ายระบบ String + JSONExtract* เป็นตัวเลือกที่ปลอดภัยและเข้าใจกันดี

ช่องว่างที่ 3: LATERAL FLATTEN

LATERAL FLATTEN ของ Snowflake แตกอาร์เรย์ที่อยู่ในคอลัมน์ 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

วิธี pre-flatten (ตัวเลือกที่ 2) เหมาะกว่าเมื่ออาร์เรย์มีสคีมาที่รู้แน่ชัดและมีขอบเขตจำกัด ส่วน arrayJoin (ตัวเลือกที่ 1) เหมาะกว่าสำหรับคิวรีเฉพาะกิจหรือเมื่อความยาวอาร์เรย์ไม่แน่นอน

ช่องว่างที่ 4: MERGE INTO

MERGE INTO ของ Snowflake เป็นกลไก 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

กลยุทธ์ incremental แบบ delete_insert ใน dbt-clickhouse เป็นสิ่งเทียบเท่าเชิงความหมายที่ใกล้เคียง MERGE INTO มากที่สุดสำหรับโมเดลวิเคราะห์ มันลบแถวที่มีอยู่เดิมซึ่งตรงกับคีย์ใด ๆ ในแบตช์ที่เข้ามา แล้วแทรกแถวที่เข้ามาทั้งหมด — เป็นอะตอมมิกต่อพาร์ทิชัน

จุดที่พลาดง่ายที่สุดของ ReplacingMergeTree: การกำจัดข้อมูลซ้ำเบื้องหลังเป็นแบบอะซิงโครนัส ระหว่างการ merge แถวทั้งเวอร์ชันเก่าและใหม่จะอยู่ในตารางพร้อมกัน จงใช้ FINAL เสมอในคิวรีที่ต้องคืนค่าหนึ่งแถวต่อหนึ่งคีย์อย่างแน่นอน ดู เอนจิน MergeTree สำหรับความหมายเชิงพฤติกรรมของการกำจัดข้อมูลซ้ำแบบครบถ้วน

ช่องว่างที่ 5: Snowflake Streams (CDC)

Snowflake Streams ติดตามการเปลี่ยนแปลงระดับแถว (INSERT, UPDATE, DELETE) บนตาราง มันเปิดคอลัมน์ระบบ METADATA$ACTION, METADATA$ISUPDATE และ METADATA$ROW_ID ให้ใช้ ClickHouse ไม่มีกลไกภายในที่เทียบเท่า

สิ่งเทียบเท่าใน ClickHouse: การสลับ producer โดยตรง

ClickHouse ไม่มีกลไก CDC ภายในที่เทียบเท่า Snowflake Streams สำหรับการย้ายระบบครั้งนี้ รูปแบบที่ใช้ง่ายกว่าคอนเนกเตอร์ CDC:

  • โหลดข้อมูลก้อนใหญ่ก่อน — scripts/02_migrate_trips.py อ่านแถวข้อมูลย้อนหลังทั้งหมดจาก Snowflake เป็นแบตช์แล้วแทรกเข้า ClickHouse
  • แล้วจึงสลับ producer — scripts/03_cutover.sh หยุด producer ฝั่ง Snowflake และเริ่ม producer ของ ClickHouse ที่เขียนตรงเข้า ClickHouse Cloud
  • ไม่ต้องมีหน้าต่าง CDC — สคริปต์ย้ายข้อมูลจัดการการโหลดข้อมูลย้อนหลัง และ producer รับช่วงการเขียนข้อมูลสด; ReplacingMergeTree(_synced_at) บน trips_raw ทำให้การรีทรายของสคริปต์ย้ายข้อมูลหรือของ producer เป็น idempotent

หลังสลับแล้ว กลยุทธ์ delete_insert ของ dbt จัดการ upsert ให้ชั้นวิเคราะห์ 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: DateTime ของ ClickHouse มีความละเอียดระดับวินาที ใช้ DateTime64(3, 'UTC') เพื่อความละเอียดระดับมิลลิวินาที (ให้ตรงกับ TIMESTAMP_NTZ ของ Snowflake) เลข 3 คือสเกลย่อยวินาที; 'UTC' คือไทม์โซน


3. ตัวเลือกในการเคลื่อนย้ายข้อมูล

วิธีใช้เมื่อไรหมายเหตุ
สคริปต์ย้ายข้อมูล Python (scripts/02_migrate_trips.py)โหลดข้อมูลก้อนใหญ่จาก Snowflake → ClickHouseเชื่อมต่อโดยตรงผ่าน snowflake-connector-python + clickhouse-connect; รันต่อจากที่ค้างได้; ไม่ต้องมีเซอร์วิสเพิ่ม — ใช้ในแล็บนี้
ClickPipesKafka, S3, Kinesis, PostgreSQL CDC, MySQL CDCคอนเนกเตอร์แบบ managed; ไม่รองรับ Snowflake เป็นต้นทาง
remoteSecure()ดึงข้อมูลเฉพาะกิจจาก ClickHouse เซอร์วิสอื่นใช้กับต้นทาง Snowflake ไม่ได้
การส่งต่อผ่าน object storageการโหลดข้อมูลก้อนใหญ่ครั้งเดียวexport จาก Snowflake → S3 → ฟังก์ชันตาราง S3 ของ ClickHouse; ต้องมีบัญชี AWS และการตั้งค่า IAM
JDBC/ODBCไปป์ไลน์ ETL ที่เขียนเองยืดหยุ่นแต่ต้องมีการออร์เคสเตรตที่เขียนเอง

สำหรับแล็บนี้ สคริปต์ย้ายข้อมูล Python เป็นตัวเลือกที่ถูกต้อง: ไม่ต้องใช้เซอร์วิสคลาวด์เพิ่ม (ไม่มี S3 ไม่มี Kafka), ดีบักได้ทั้งหมด และใช้แพ็กเกจ (snowflake-connector-python, clickhouse-connect) ที่พาร์ตเนอร์ติดตั้งไว้แล้วสำหรับขั้นตอนอื่นของแล็บ


4. การเปรียบเทียบสถาปัตยกรรม CDC

Snowflake Streams + TasksClickHouse (แล็บนี้)
การติดตามการเปลี่ยนแปลงอ็อบเจกต์ stream ภายในบนตาราง (TRIPS_CDC_STREAM)ไม่มีสิ่งเทียบเท่า — producer เขียนตรงเข้า ClickHouse หลังการสลับ
อีเวนต์การเปลี่ยนแปลงMETADATA$ACTION: INSERT/UPDATE/DELETEINSERT ตรงจาก producer ของ ClickHouse
Latencyตั้งกำหนดเวลา task ได้ (ต่ำสุด 1 นาที)ตั้งช่วงเวลาแบตช์ได้ (ค่าเริ่มต้น 10 s)
การบริโภคข้อมูลtask SQL อ่าน stream แล้วส่งไปยังเป้าหมายproducer Python (producer/producer.py)
การเปลี่ยนแปลงสคีมาประสานงานด้วยมือโค้ดของ producer ควบคุมสคีมา

หลังการย้ายระบบ producer เขียนตรงเข้า ClickHouse — ไม่ต้องใช้ Streams หรือ Tasks กลยุทธ์ delete_insert ของ dbt จัดการ upsert ให้ชั้นวิเคราะห์ การรวมข้อมูลตามรอบเวลา (Snowflake Tasks) มีสิ่งแทนที่ที่เป็นของ ClickHouse เองคือ Refreshable Materialized View — โปรเจกต์ dbt ของแล็บนี้มีมาให้หนึ่งตัว คือ analytics.mv_live_trip_feed แม้ว่าแล็บจะไม่เปิดช่วงเวลารีเฟรชของมัน (ดูโมดูล 05)


5. เจาะลึกโมเดลค่าใช้จ่าย

Snowflake: คิดตามเครดิต

เครดิต Snowflake หนึ่งหน่วยราคาราว $3 (Enterprise) ค่าใช้จ่าย = warehouse_size × เวลาที่ทำงาน warehouse ขนาด SMALL กิน 1 เครดิต/ชั่วโมง ขนาด MEDIUM กิน 2 Auto-suspend ที่ต่ำสุด 60 วินาที หมายความว่าแค่คิวรีเดียวก็คิดค่าใช้จ่ายอย่างน้อย 1/60 ของชั่วโมง

สำหรับแล็บแท็กซี่ NYC (warehouse ขนาด X-Small, 1 เครดิต/ชม.):

  • การตั้งค่า Part 1: 2–4 เครดิต ($6–12)
  • ต่อเนื่องต่อเซสชัน 8 ชม.: 4–8 เครดิต/วัน ($12–24)
  • resource monitor ของ ANALYTICS_WH จำกัดไว้ที่ 50 เครดิต/เดือน (~$150)

ClickHouse Cloud: แยกการประมวลผลกับสตอเรจ

ClickHouse Cloud คิดค่าใช้จ่ายแยกกันระหว่างการประมวลผลและสตอเรจ:

  • การประมวลผล: ระดับ Development อยู่ที่ราว $0.10/ชม. เมื่อทำงาน และลดลงถึงศูนย์เมื่อไม่มีงาน
  • สตอเรจ: ~$0.023/GB/เดือน (ถูกกว่า $23/TB ของ Snowflake อย่างมาก)
  • ClickPipes: รวมอยู่ในการสมัครใช้งาน Cloud สำหรับต้นทางที่รองรับ (Kafka, S3, Kinesis, PostgreSQL CDC, MySQL CDC — ไม่ใช่ Snowflake)

สำหรับแล็บแท็กซี่ NYC:

  • 50M แถว × ~300 ไบต์/แถว แบบไม่บีบอัด = ~15GB → ~8GB เมื่อบีบอัดใน ClickHouse
  • ค่าสตอเรจ: ~$0.18/เดือน
  • ค่าประมวลผลระหว่างทำแล็บ Part 3 (~2 ชม.): ~$0.20–0.40

ค่าใช้จ่ายรวมของ Part 3: ~$2–4 เทียบกับ ~$6–12 ของ Snowflake สำหรับเซสชันเดียวกัน

ความต่างของค่าใช้จ่ายอธิบายว่าทำไมหลายองค์กรจึงเริ่มจาก Snowflake (ดำเนินงานง่ายกว่า) แล้วย้ายมา ClickHouse (ค่าใช้จ่ายต่ำกว่า + ประสิทธิภาพสูงกว่า) เมื่อเวิร์กโหลดวิเคราะห์ของพวกเขาขยายตัว

ในหน้านี้

TH