Snowflake vs ClickHouse
Hai engine khác nhau ở đâu về lưu trữ, compute và phương ngữ SQL — và những idiom Snowflake nào không có tương đương trực tiếp trong ClickHouse.
Tài liệu này là tài liệu tham chiếu cho các đối tác di chuyển từ Snowflake sang ClickHouse. Nó bao quát những khác biệt kiến trúc chi phối các quyết định thiết kế và sáu khoảng cách phương ngữ SQL bạn sẽ gặp trong workload NYC Taxi.
1. So sánh kiến trúc
Lưu trữ
Snowflake đưa ra mọi quyết định lưu trữ vật lý thay cho bạn. Dữ liệu được lưu dưới dạng micro-partition cột đã nén trong object storage trên cloud. Bạn chọn kích thước warehouse và cấu trúc bảng; Snowflake xử lý phần còn lại — clustering, compaction và quản lý file đều tự động.
ClickHouse yêu cầu bạn đưa ra các quyết định lưu trữ vật lý một cách tường minh. Khi tạo một bảng, bạn chỉ định:
- engine (quyết định cách dữ liệu được lưu, merge và loại trùng)
- ORDER BY (trở thành thứ tự sắp xếp vật lý và primary index)
- Tùy chọn: PARTITION BY, TTL, SETTINGS (codec nén, hành vi merge)
Đây là các quyết định về tính đúng đắn, không phải các núm tinh chỉnh hiệu năng. Chọn sai engine có thể tạo ra kết quả truy vấn sai một cách âm thầm. Chọn sai ORDER BY có thể khiến những truy vấn đáng lẽ nhanh lại phải quét toàn bộ bảng.
Thực thi truy vấn
Snowflake dùng MPP shared-nothing với virtual warehouse. Một warehouse là một cluster các node compute xử lý truy vấn. Bạn trả tiền cho warehouse trong suốt thời gian nó chạy — thời gian rỗi vẫn tốn credit. Auto-suspend có giúp, nhưng cold-start làm tăng độ trễ.
ClickHouse dùng thực thi vector hóa. ClickHouse Cloud tự động scale từng compute service một cách độc lập và scale về zero khi rỗi. Nhiều compute service có thể dùng chung một lớp lưu trữ (qua SharedMergeTree) — đây là mô hình tách rời compute-compute của ClickHouse Cloud, trong đó mỗi service là một tầng compute độc lập trên cùng một lớp dữ liệu.
Mô hình đồng thời
Snowflake cô lập các workload bằng cách tạo các warehouse riêng. ETL dùng TRANSFORM_WH, phân tích dùng ANALYTICS_WH. Mỗi warehouse có compute riêng; một job ETL chậm không thể bỏ đói một truy vấn phân tích.
ClickHouse Cloud hỗ trợ cùng mô hình đó qua tách rời compute-compute: bạn có thể cấp phát nhiều compute service dùng chung một lớp lưu trữ. Mỗi service là một tầng compute tự động scale độc lập — ETL chạy trên một service, phân tích tương tác trên service khác, không tranh chấp tài nguyên với nhau. Trong phạm vi một service duy nhất, việc cô lập workload được thực hiện bằng soft quota (max_threads, priority, max_memory_usage theo từng user hoặc từng truy vấn) và các user profile có giới hạn tài nguyên. Với phần lớn workload phân tích mà truy vấn hoàn tất trong vài millisecond, một service duy nhất là đủ và quota theo truy vấn là lựa chọn nhẹ hơn.
Mô hình chi phí
| Snowflake | ClickHouse Cloud | |
|---|---|---|
| Compute | Credit (warehouse-giây) | Compute unit (tách khỏi lưu trữ) |
| Lưu trữ | $23/TB/tháng | ~$0.023/GB/tháng (rẻ hơn) |
| Scale-to-zero | Chỉ có auto-suspend | Hỗ trợ scale-to-zero hoàn toàn |
| Truyền dữ liệu | Ingress miễn phí; egress có phí | Mức egress cloud tiêu chuẩn |
Khác biệt quan trọng nhất: ở Snowflake, bạn trả tiền cho thời gian warehouse bất kể có truy vấn đang chạy hay không. Ở ClickHouse Cloud, compute scale về zero giữa các truy vấn. Với các workload phân tích dạng bùng nổ, ClickHouse Cloud thường rẻ hơn 3-8 lần so với một cấu hình Snowflake tương đương.
2. Khoảng cách phương ngữ SQL
Workload NYC Taxi chứa sáu cấu trúc cần dịch. Cả sáu đều xuất hiện trong Q1–Q7 tại 01-setup-snowflake/queries/.
Khoảng cách 1: QUALIFY
QUALIFY là một phần mở rộng của Snowflake, lọc dòng theo kết quả của window function, tương tự như HAVING lọc theo kết quả của hàm tổng hợp. Trong cuộc di chuyển này, chúng ta xem QUALIFY là một khoảng cách phương ngữ và viết lại bằng subquery — đây là mẫu khả chuyển phổ dụng, hoạt động trên mọi 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;Vì sao điều này quan trọng: QUALIFY xuất hiện ở Q3. Cách viết lại bằng subquery là mẫu an toàn và khả chuyển — nó hoạt động bất kể engine SQL đích là gì và làm cho kết quả window function trở nên tường minh. Nguy hiểm với bất kỳ cú pháp riêng của Snowflake là giả định rằng nó chuyển sang được một cách âm thầm; luôn kiểm thử mọi truy vấn trước khi tuyên bố cuộc di chuyển đã hoàn tất.
Khoảng cách 2: Cú pháp colon-path của VARIANT
Kiểu VARIANT của Snowflake dùng ký hiệu colon-path để truy cập trường lồng nhau: column:field.subfield::TYPE. ClickHouse lưu dữ liệu bán cấu trúc dưới dạng String và trích xuất tại thời điểm truy vấn bằng các hàm 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;Toàn bộ họ JSONExtract*: JSONExtractFloat, JSONExtractInt, JSONExtractString, JSONExtractBool, JSONExtractKeys, JSONExtractArrayRaw, JSONExtractRaw. Dùng JSONExtractRaw khi bạn cần một object hoặc array lồng nhau dưới dạng string để xử lý tiếp.
Vì sao không dùng kiểu
JSONcủa ClickHouse? KiểuJSON(trước đây là thực nghiệm) đã có trong các phiên bản ClickHouse gần đây nhưng có ngữ nghĩa khác và chưa được tôi luyện cho production ở mọi tình huống sử dụng. Với một lab di chuyển,String+JSONExtract*là lựa chọn an toàn và dễ hiểu.
Khoảng cách 3: LATERAL FLATTEN
LATERAL FLATTEN của Snowflake bung một array bên trong một cột VARIANT thành các dòng. ClickHouse không có tương đương trực tiếp.
-- 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 timeCách làm phẳng trước (Option 2) được ưu tiên khi array có schema đã biết và bị chặn về số phần tử. arrayJoin (Option 1) được ưu tiên cho các truy vấn ad-hoc hoặc khi độ dài array thay đổi.
Khoảng cách 4: MERGE INTO
MERGE INTO của Snowflake là cơ chế upsert chính. ClickHouse không có câu lệnh MERGE. Tương đương đúng trong ClickHouse phụ thuộc vào engine của bảng.
-- 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 layerChiến lược incremental delete_insert trong dbt-clickhouse là tương đương ngữ nghĩa gần nhất với MERGE INTO cho các model phân tích. Nó xóa các dòng đang tồn tại khớp với bất kỳ khóa nào trong batch đến, rồi chèn toàn bộ dòng đến — nguyên tử theo từng partition.
Điểm dễ sập bẫy quan trọng với ReplacingMergeTree: Việc loại trùng ở nền là bất đồng bộ. Giữa các lần merge, cả phiên bản cũ và mới của một dòng đều tồn tại trong bảng. Luôn dùng
FINALtrong các truy vấn buộc phải trả về đúng một dòng cho mỗi khóa. Xem MergeTree engines để biết đầy đủ ngữ nghĩa loại trùng.
Khoảng cách 5: Snowflake Streams (CDC)
Snowflake Streams theo dõi các thay đổi ở mức dòng (INSERT, UPDATE, DELETE) trên một bảng. Chúng phơi ra các cột hệ thống METADATA$ACTION, METADATA$ISUPDATE và METADATA$ROW_ID. ClickHouse không có cơ chế nội bộ tương đương.
Tương đương trong ClickHouse: cắt chuyển producer trực tiếp
ClickHouse không có cơ chế CDC nội bộ tương đương Snowflake Streams. Trong cuộc di chuyển này, mô hình đơn giản hơn một CDC connector:
- Nạp hàng loạt trước —
scripts/02_migrate_trips.pyđọc toàn bộ dòng lịch sử từ Snowflake theo batch và chèn vào ClickHouse - Sau đó cắt chuyển producer —
scripts/03_cutover.shdừng producer Snowflake và khởi động một producer ClickHouse ghi trực tiếp vào ClickHouse Cloud - Không cần cửa sổ CDC — script di chuyển xử lý phần nạp lịch sử, và producer đảm nhiệm các lượt ghi trực tiếp;
ReplacingMergeTree(_synced_at)trêntrips_rawlàm cho mọi lần thử lại của việc di chuyển hoặc của producer đều idempotent
Sau khi cắt chuyển, chiến lược delete_insert của dbt xử lý upsert cho lớp phân tích. Snowflake Streams và Tasks được loại bỏ hoàn toàn.
Khoảng cách 6: Các hàm ngày/giờ
Snowflake và ClickHouse có tên hàm ngày khác nhau. Phần lớn là thay thế máy móc.
| Snowflake | ClickHouse | Ghi chú |
|---|---|---|
DATE_TRUNC('hour', ts) | toStartOfHour(ts) | Ngoài ra: toStartOfDay, toStartOfMonth, toStartOfWeek |
DATE_TRUNC('day', ts) | toDate(ts) | |
DATEADD('day', n, ts) | ts + INTERVAL n DAY | Hoặc addDays(ts, n) |
DATEDIFF('minute', t1, t2) | dateDiff('minute', t1, t2) | Tên hàm viết thường |
CURRENT_DATE | today() | |
CURRENT_TIMESTAMP() | now() | |
TO_TIMESTAMP(epoch, 9) | fromUnixTimestamp64Nano(epoch) | Đơn vị tường minh trong CH |
YEAR(ts) | toYear(ts) | |
MONTH(ts) | toMonth(ts) | |
EXTRACT(epoch FROM ts) | toUnixTimestamp(ts) |
DateTime vs DateTime64:
DateTimecủa ClickHouse có độ chính xác đến giây. DùngDateTime64(3, 'UTC')cho độ chính xác millisecond (tương ứngTIMESTAMP_NTZcủa Snowflake). Số3là thang dưới giây;'UTC'là múi giờ.
3. Các phương án di chuyển dữ liệu
| Phương pháp | Khi nào dùng | Ghi chú |
|---|---|---|
Script di chuyển Python (scripts/02_migrate_trips.py) | Nạp hàng loạt cho Snowflake → ClickHouse | Kết nối trực tiếp qua snowflake-connector-python + clickhouse-connect; có thể tiếp tục sau khi gián đoạn; không cần thêm service nào — được dùng trong lab này |
| ClickPipes | Kafka, S3, Kinesis, PostgreSQL CDC, MySQL CDC | Connector được quản lý; không hỗ trợ Snowflake làm nguồn |
remoteSecure() | Kéo dữ liệu ad-hoc từ một service ClickHouse khác | Không áp dụng cho nguồn Snowflake |
| Chuyển tiếp qua object storage | Các lần nạp lớn một lần | Export Snowflake → S3 → hàm bảng S3 của ClickHouse; cần tài khoản AWS và thiết lập IAM |
| JDBC/ODBC | Pipeline ETL tùy biến | Linh hoạt nhưng cần tự dàn dựng orchestration |
Với lab này, script di chuyển Python là lựa chọn đúng: nó không cần thêm service cloud nào (không S3, không Kafka), hoàn toàn có thể debug, và dùng các package (snowflake-connector-python, clickhouse-connect) mà đối tác đã cài cho các bước khác của lab.
4. So sánh kiến trúc CDC
| Snowflake Streams + Tasks | ClickHouse (lab này) | |
|---|---|---|
| Theo dõi thay đổi | Đối tượng stream nội bộ trên bảng (TRIPS_CDC_STREAM) | Không có tương đương — producer ghi trực tiếp vào ClickHouse sau khi cắt chuyển |
| Sự kiện thay đổi | METADATA$ACTION: INSERT/UPDATE/DELETE | INSERT trực tiếp từ producer ClickHouse |
| Độ trễ | Lịch task cấu hình được (tối thiểu 1 phút) | Khoảng batch cấu hình được (mặc định 10 s) |
| Tiêu thụ | Task SQL đọc stream, phát ra bảng đích | Producer Python (producer/producer.py) |
| Thay đổi schema | Phối hợp thủ công | Code producer kiểm soát schema |
Sau khi di chuyển, producer ghi trực tiếp vào ClickHouse — không cần Streams hay Tasks. Chiến lược delete_insert của dbt xử lý upsert cho lớp phân tích. Việc tổng hợp định kỳ (Snowflake Tasks) có một thay thế thuần ClickHouse là Refreshable Materialized View — project dbt của lab này có sẵn một cái, analytics.mv_live_trip_feed, tuy lab không bật khoảng refresh của nó lên (xem module 05).
5. Đi sâu vào mô hình chi phí
Snowflake: dựa trên credit
Một credit Snowflake tốn khoảng $3 (Enterprise). Chi phí = warehouse_size × thời gian chạy. Một warehouse SMALL tiêu thụ 1 credit/giờ. MEDIUM tiêu thụ 2. Auto-suspend tối thiểu 60 giây nghĩa là ngay cả một truy vấn đơn lẻ cũng tốn ít nhất 1/60 giờ.
Với lab NYC Taxi (warehouse X-Small, 1 credit/giờ):
- Chuẩn bị Phần 1:
2–4 credit ($6–12) - Chi phí thường xuyên mỗi phiên 8 giờ:
4–8 credit/ngày ($12–24) - Resource monitor của ANALYTICS_WH chặn ở 50 credit/tháng (~$150)
ClickHouse Cloud: compute và lưu trữ tách rời
ClickHouse Cloud tính phí riêng cho compute và lưu trữ:
- Compute: tầng Development khoảng $0.10/giờ khi hoạt động, scale về zero khi rỗi
- Lưu trữ: ~$0.023/GB/tháng (rẻ hơn đáng kể so với $23/TB của Snowflake)
- ClickPipes: bao gồm trong gói đăng ký Cloud cho các nguồn được hỗ trợ (Kafka, S3, Kinesis, PostgreSQL CDC, MySQL CDC — không có Snowflake)
Với lab NYC Taxi:
- 50 triệu dòng × ~300 byte/dòng chưa nén = ~15GB → ~8GB sau nén trong ClickHouse
- Chi phí lưu trữ: ~$0.18/tháng
- Compute trong lúc chạy Phần 3 của lab (~2 giờ): ~$0.20–0.40
Tổng chi phí Phần 3: ~$2–4 so với ~$6–12 của Snowflake cho cùng một phiên.
Khác biệt chi phí này giải thích vì sao nhiều tổ chức bắt đầu với Snowflake (vận hành đơn giản hơn) rồi di chuyển sang ClickHouse (chi phí thấp hơn + hiệu năng cao hơn) khi workload phân tích của họ mở rộng.