Snowflake MigrationClickHouse Workshops

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í

SnowflakeClickHouse Cloud
ComputeCredit (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-zeroChỉ có auto-suspendHỗ trợ scale-to-zero hoàn toàn
Truyền dữ liệuIngress 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 JSON của ClickHouse? Kiểu JSON (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 time

Cá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 layer

Chiế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 FINAL trong 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.sh dừ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ên trips_raw là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.

SnowflakeClickHouseGhi 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 DAYHoặc addDays(ts, n)
DATEDIFF('minute', t1, t2)dateDiff('minute', t1, t2)Tên hàm viết thường
CURRENT_DATEtoday()
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: DateTime của ClickHouse có độ chính xác đến giây. Dùng DateTime64(3, 'UTC') cho độ chính xác millisecond (tương ứng TIMESTAMP_NTZ của Snowflake). Số 3 là 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ápKhi nào dùngGhi chú
Script di chuyển Python (scripts/02_migrate_trips.py)Nạp hàng loạt cho Snowflake → ClickHouseKế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
ClickPipesKafka, S3, Kinesis, PostgreSQL CDC, MySQL CDCConnector đượ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ácKhông áp dụng cho nguồn Snowflake
Chuyển tiếp qua object storageCác lần nạp lớn một lầnExport Snowflake → S3 → hàm bảng S3 của ClickHouse; cần tài khoản AWS và thiết lập IAM
JDBC/ODBCPipeline ETL tùy biếnLinh 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 + TasksClickHouse (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 đổiMETADATA$ACTION: INSERT/UPDATE/DELETEINSERT 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 đíchProducer Python (producer/producer.py)
Thay đổi schemaPhối hợp thủ côngCode 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.

Trên trang này

VI