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 ที่มีการจำกัดทรัพยากร สำหรับเวิร์กโหลดวิเคราะห์ส่วนใหญ่ที่คิวรีเสร็จในระดับมิลลิวินาที เซอร์วิสเดียวก็เพียงพอ และโควตาต่อคิวรีเป็นตัวเลือกที่เบากว่า
โมเดลค่าใช้จ่าย
| Snowflake | ClickHouse Cloud | |
|---|---|---|
| การประมวลผล | เครดิต (warehouse-วินาที) | Compute unit (แยกจากสตอเรจ) |
| สตอเรจ | $23/TB/เดือน | ~$0.023/GB/เดือน (ถูกกว่า) |
| Scale-to-zero | Auto-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 ใช้ชื่อฟังก์ชันวันที่ต่างกัน ส่วนใหญ่เป็นการแทนที่แบบตรงตัว
| Snowflake | ClickHouse | หมายเหตุ |
|---|---|---|
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_DATE | today() | |
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; รันต่อจากที่ค้างได้; ไม่ต้องมีเซอร์วิสเพิ่ม — ใช้ในแล็บนี้ |
| ClickPipes | Kafka, 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 + Tasks | ClickHouse (แล็บนี้) | |
|---|---|---|
| การติดตามการเปลี่ยนแปลง | อ็อบเจกต์ stream ภายในบนตาราง (TRIPS_CDC_STREAM) | ไม่มีสิ่งเทียบเท่า — producer เขียนตรงเข้า ClickHouse หลังการสลับ |
| อีเวนต์การเปลี่ยนแปลง | METADATA$ACTION: INSERT/UPDATE/DELETE | INSERT ตรงจาก 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 (ค่าใช้จ่ายต่ำกว่า + ประสิทธิภาพสูงกว่า) เมื่อเวิร์กโหลดวิเคราะห์ของพวกเขาขยายตัว