Snowflake MigrationClickHouse Workshops

05 벤치마크와 컷오버

ClickHouse에서 대시보드를 재구축하고, 7개 쿼리 전부를 양쪽 엔진에서 벤치마크하고, 프로듀서를 컷오버하고, 패리티를 검증하고, 정리한다.

시작 지점

모듈 04 완료 상태: analytics 레이어가 채워지고 테스트를 통과했다. analytics.fact_trips가 약 5천만 행을 담고 있고, analytics.dim_taxi_zones, analytics.dim_payment_type, analytics.dim_vendor, analytics.dim_date가 완전히 적재되었고, dbt test가 처음부터 끝까지 통과하고, analytics.taxi_zones_dict가 살아 있으며 dictGet()으로 자치구를 반환한다. analytics.agg_hourly_zone_trips는 여전히 비어 있다 — 결함이 아니라 설계상 그렇고 — 이 모듈의 컷오버까지 그 상태로 남는다. Snowflake 프로듀서는 여전히 실행 중이고, Snowflake와 ClickHouse 사이의 공백도 여전히 열려 있다. 약 45분을 예상하라.

이유

여기까지의 모든 모듈은 준비 작업이었다. 모듈 03은 ClickHouse가 5천만 행을 담을 수 있음을 증명했고, 모듈 04는 dbt 파이프라인이 그 위에서 돌아감을 증명했다. 그 어느 것도 단독으로는 파트너가 "마이그레이션 완료"라고 승인할 만한 것이 아니다 — 그러려면 두 가지가 더 필요하다. 숫자, 그리고 컷오버다.

숫자는 2단계의 벤치마크다. 마이그레이션 계획에 있는 그 일곱 개 쿼리를 Snowflake와 ClickHouse에 연달아 실행하고, 각각 세 번 실행한 값의 중앙값을 취한다. 그것이 "ClickHouse가 더 빠를 것이다"를, 파트너가 자기 이해관계자 앞에 내놓을 수 있는 구체적이고 방어 가능한 속도 향상 수치로 바꿔 준다.

3단계의 컷오버가 나머지 절반이다. 지금까지의 모든 모듈은 두 시스템을 나란히 돌려 왔다. Snowflake가 기록 시스템이고 ClickHouse가 그 뒤를 따라잡는 구조였다. 쓰기 경로를 실제로 옮기지 않는 마이그레이션은 마이그레이션이 아니라 복사다. 3단계는 Snowflake 프로듀서를 멈추고, 모듈 01부터 지속적인 쓰기가 열어 두었던 공백을 닫고, 새 트립을 대신 ClickHouse에 쓰기 시작한다 — ClickHouse가 기록 시스템이 되는 순간이다.

이 모듈은 또한 모듈 06의 서면 평가 전 마지막 학습 모듈이며, 그 평가는 여기서 만든 결과물에 대한 오픈북이다. 대시보드, 벤치마크 CSV, 패리티 확인은 5단계에서 무엇이든 정리하기 전에 모두 실물로 존재해야 한다.

개념 — 내부 동작

BI 레이어, 기계적으로. 1단계에서 bash superset/add_clickhouse_connection.sh는 Superset REST API를 직접 호출한다 — Superset UI에서 수동으로 클릭할 것이 없다. 이 스크립트는 ClickHouse 연결을 등록하고, 커밋된 대시보드 익스포트를 임포트해, 모듈 01이 만든 세 개의 Snowflake 대시보드 옆에 네 개의 ClickHouse 대시보드를 추가한다(총 일곱 개).

대시보드대응 대상무엇을 보여 주는가
CH — Operations Command CenterSnowflake Dashboard 1라이브 fact_trips 데이터(컷오버 이후). 동일한 KPI, 더 빠른 쿼리
CH — Executive Weekly ReportSnowflake Dashboard 2ROW_NUMBER() 서브쿼리로 다시 쓴 QUALIFY
CH — Driver & Quality AnalyticsSnowflake Dashboard 3Snowflake의 LATERAL FLATTEN 대신 JSONExtractString
CH — Capabilities Showcase(신규 — Snowflake 대응물 없음)근사 함수, 딕셔너리 조인, SAMPLE 절

일곱 개 벤치마크 쿼리가 다루는 것. 2단계는 마이그레이션 계획에 있는 그 일곱 개 쿼리를 양쪽 엔진에 실행하고 실제 소요 시간을 비교한다. 각 쿼리는 모듈 02 계획에 나온 특정 방언 격차나 엔진 기능을 겨냥한다.

쿼리다루는 것
Q1자치구별 시간당 매출
Q2롤링 7일 평균 거리
Q3상위 10개 트립 — Snowflake QUALIFY 대 ClickHouse ROW_NUMBER() 서브쿼리
Q4드라이버 평점 — Snowflake LATERAL FLATTEN 대 ClickHouse JSONExtractString
Q5서지 요금 — Snowflake VARIANT 대 ClickHouse String + JSONExtract*
Q6시간별 집계 — Snowflake MERGE 대 ClickHouse ReplacingMergeTree
Q7CDC / 라이브 데이터 신선도

컷오버 공백. Snowflake 프로듀서는 모듈 01부터 분당 약 60건의 트립을 써 왔고, 한 번도 멈추지 않았다. 모듈 03의 마이그레이션 스크립트는 그 스크립트가 실행된 시점의 TRIPS_RAW를 포착했고, 모듈 04는 그 스냅샷 위에 dbt 파이프라인을 구축했다. 마이그레이션 스크립트의 마지막 배치 이후 Snowflake에 쓰인 모든 트립은 Snowflake에만 존재한다 — ClickHouse의 꼬리가 빠져 있다. 3단계의 --resume 따라잡기 패스가 정확히 그 공백을 닫는다. 이미 ClickHouse에 있는 max(pickup_at)을 읽어 그 이후에 쓰인 행만 가져오므로, 모듈 01부터 열려 있던 공백을 닫는 패스가 원래 대량 마이그레이션에 걸린 40-50분이 아니라 수 초에서 수 분 걸린다. 이를 건너뛰고 그냥 컷오버하면 ClickHouse는 그 공백에 떨어진 트립을 영구히 잃는다 — 4단계가 잡아내도록 만들어진 조용한 패리티 실패인데, 3단계를 순서대로 실행했을 때에만 잡힌다.

agg_hourly_zone_trips는 여기서, 오직 여기서 채워진다. 이 테이블은 설계상 모듈 03부터 비어 있었다. 증분 필터가 WHERE pickup_at >= now() - INTERVAL 2 HOUR이라서 라이브 프로듀서가 쓴 행만 매칭하는데, 이 모듈까지는 쓰고 있던 프로듀서가 Snowflake 쪽뿐이었다. 3단계가 ClickHouse 프로듀서를 시작하면 새 행이 드디어 그 2시간 윈도우 안에 떨어지고, 이 테이블과 이를 사용하는 모든 대시보드 차트가 이 랩에서 처음으로 빈 상태를 벗어난다.

1단계 — ClickHouse 대시보드 추가

7개의 데이터셋, 18개의 차트, 4개의 대시보드를 Superset UI에서 하나씩 만드는 전체 수동 구축 과정은 Superset on ClickHouse를 따르라. 수동 단계를 건너뛰고 한 번에 전부 임포트하려면:

cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
source .env && source .clickhouse_state
bash superset/add_clickhouse_connection.sh

커밋된 superset/dashboards/dashboard_export_*.zip은 ClickHouse 호스트가 your-instance.clickhouse.cloud로 가려져 있다. 위의 단축 경로는 임포트 전에 .env의 값으로 URI를 패치하므로 이 과정은 투명하다 — 가려진 것을 알아채지도 못한다. 하지만 대신 Superset UI에서 ZIP을 수동으로 임포트하면 생성되는 데이터베이스 연결은 연결되지 않는다 — 이후 그 연결을 편집해 실제 CLICKHOUSE_HOST와 자격 증명을 가리키게 해야 한다. 정확한 방법은 Superset on ClickHouse를 참고하라.

검증:

http://localhost:8088 (admin / admin)을 열어라. Dashboards 아래에 총 7개가 보여야 한다 — Snowflake 대시보드 3개와 CH — 접두어가 붙은 4개다.

2단계 — 벤치마크 실행

일곱 개 쿼리 전부를 Snowflake와 ClickHouse에 연달아 실행하고 실제 소요 시간을 비교한다.

cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
source .env && source .clickhouse_state
./scripts/run_benchmark.sh

예상 출력:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  NYC Taxi Lab — Query Benchmark: Snowflake vs ClickHouse
  (median of 3 runs each)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Query                                   Snowflake     ClickHouse    Speedup
────────────────────────────────────────────────────────────────────────
Q1  Hourly revenue by borough           5.0s          0.7s          6x
Q2  Rolling 7-day avg distance          5.5s          0.8s          6x
Q3  Top 10 trips (QUALIFY→subquery)     5.0s          0.7s          6x
Q4  Driver ratings (JSON flatten)       5.4s          0.8s          6x
Q5  Surge pricing (VARIANT)             5.1s          0.7s          6x
Q6  Hourly aggregation (MERGE→RMT)      5.9s          0.8s          7x
Q7  CDC/live data freshness             7.9s          0.8s          9x
────────────────────────────────────────────────────────────────────────
Total                                   40.1s         5.6s          7x avg
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

이 수치는 대표적인 실행 한 번의 결과이며 보장값이 아니다 — 웨어하우스 크기, ClickHouse Cloud 티어, 그리고 그 시점에 어느 서비스에서 무엇이 함께 돌고 있는지에 따라 본인의 숫자는 달라진다.

스크립트는 모든 실행 결과를 workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse/scripts/benchmark_results_<timestamp>.csv에 쓴다. 디스크에 보관하라 — 모듈 06의 평가에 필요한 두 파일 중 하나이므로, 5단계의 정리 과정에서 삭제하지 마라.

3단계 — ClickHouse로 컷오버

마이그레이션 공백. Snowflake 프로듀서는 이 랩 내내 실행되며 분당 약 60건의 트립을 써 왔다. 모듈 03의 마이그레이션 스크립트는 그 스크립트가 실행된 시점의 TRIPS_RAW를 포착했다 — 그 이후에 쓰인 행은 Snowflake에만 존재한다. 쓰기 경로를 컷오버하기 전에 그 공백을 닫아라.

아래 네 단계를 순서대로 모두 실행하라 — 두 시스템을 일관되게 유지하는 것이 바로 그 순서다.

cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
source .env && source .clickhouse_state
source .venv/bin/activate

# Step 1: Stop the Snowflake producer (freeze the dataset)
docker stop nyc_taxi_producer

# Step 2: Catch up the delta — only migrates rows with pickup_at newer than
# what's already in ClickHouse. Runs in seconds to minutes, not the original
# 40-50 minutes, because only the gap rows move.
python scripts/02_migrate_trips.py --resume

# Step 3: Refresh the analytics tables with the newly migrated rows
cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse/dbt/nyc_taxi_dbt_ch"
dbt run
cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"

# Step 4: Start the ClickHouse producer
source .env && source .clickhouse_state
./scripts/03_cutover.sh

2단계를 건너뛰지 마라. --resume은 이미 ClickHouse에 있는 max(pickup_at)을 읽어 Snowflake 쿼리에 WHERE PICKUP_DATETIME > <watermark> 필터를 붙이므로, 모듈 03의 원래 마이그레이션 실행 도중과 그 이후에 쓰인 행 — ClickHouse가 한 번도 본 적 없는 행 — 만 전송한다. 이것 없이 컷오버하면 ClickHouse는 그 구간에 떨어진 트립을 영구히 잃는다. 4단계의 패리티 확인은 정확히 그것을 잡아내도록 만들어졌지만, 이 단계를 먼저 실행했을 때에만 그렇다.

./scripts/03_cutover.sh는 Type "cutover" to confirm을 묻고, 자체 안전망으로 1-3단계를 반복한다. Snowflake 프로듀서를 다시 중지하고(이미 했다면 아무 일도 하지 않는다), dbt run을 한 번 더 실행하고, ClickHouse 프로듀서(nyc_taxi_ch_producer)를 빌드해 시작한다. 프로듀서가 시작된 뒤 30초 후에 새 행이 default.trips_raw에 떨어지고 있는지 확인하고 dbt를 한 번 더 실행한다 — 그 실행이 드디어 agg_hourly_zone_trips에 첫 행을 채우며, 모듈 04가 설계상 남겨 둔 공백을 닫는다.

검증:

-- Most recent trip should be within the last 60 seconds
SELECT max(pickup_at) AS most_recent_trip FROM default.trips_raw;

-- Row count should be increasing — wait 60 seconds and run again
SELECT count() FROM default.trips_raw;

-- agg_hourly_zone_trips should now have rows for the first time in the lab
SELECT count() FROM analytics.agg_hourly_zone_trips;
docker ps | grep nyc_taxi_ch_producer   # should show running

analytics 레이어를 신선하게 유지하기. fact_trips와 agg_hourly_zone_trips는 dbt 증분 모델이며 자동으로 갱신되지 않는다. 03_cutover.sh는 프로듀서가 살아 있음을 확인한 뒤 dbt run을 한 번 실행하지만, 새 트립이 쌓이면서 대시보드는 낡아 간다. 최신 숫자가 필요할 때마다 dbt/nyc_taxi_dbt_ch에서 dbt run을 다시 실행하라(프로덕션이라면 cron, Airflow, dbt Cloud로 스케줄링하겠지만 랩에서는 온디맨드로 충분하다). 반면 analytics.mv_live_trip_feed는 갱신 가능한 materialized view다 — 모듈 04의 dbt run이 이미 engine = 'ReplacingMergeTree(refreshed_at)'로 만들었다 — 그러나 이 랩은 그 갱신 주기를 켜지 않는다. 스스로 재실행되게 만들어 주는 MODIFY REFRESH EVERY 30 SECOND 문은 모델 파일 안에 주석으로만 존재한다. 활성화하려면 직접 실행하는 한 줄짜리 ALTER TABLE 문이 필요하다. 그것 없이는 mv_live_trip_feed가 dbt가 만든 그 한 번만 갱신된다.

이 단계를 되돌려 Snowflake 프로듀서로 돌아가야 한다면, 역방향 컷오버:

docker stop nyc_taxi_ch_producer
cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/01-setup-snowflake/superset"
docker-compose --env-file ../.env up -d producer

4단계 — 패리티 검증

--resume 따라잡기 패스가 실행되고 ClickHouse 프로듀서가 동작 중이므로 두 시스템은 이제 패리티 상태여야 한다. 그것을 확인하라.

cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
source .env && source .clickhouse_state
bash scripts/01_verify_migration.sh

예상 출력:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Migration Parity Check
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✓ ClickHouse default.trips_raw: 50,008,250 rows

  ✓ Snowflake NYC_TAXI_DB.RAW.TRIPS_RAW: 50,008,250 rows
  ✓ Row count parity: PASS  (difference: 0 rows = 0.0000%)

  ✓ trip_metadata populated: 50,008,250 non-empty rows
  pickup_at range: 2022-03-30   2026-03-31

  ✓ ClickHouse has 50,008,250 rows — migration looks complete
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

본인의 행 수는 다를 것이다. 중요한 것은 패리티 줄이다. 이 시점에는 Snowflake 프로듀서가 멈춰 있어 새 행이 그쪽에 떨어지지 않으므로 행 수가 정확히 일치해야 하고, --resume 패스 도중 배치가 아직 전송 중이었다면 몇 행 이내로 차이 날 수 있는데 이는 스크립트가 검사하는 0.01% 임계값 안에 충분히 들어온다.

패리티 확인이 실패하면(차이가 0.01%보다 크면) 공백이 완전히 닫히지 않았다는 뜻이다 — 따라잡기 패스를 다시 실행하고 재확인하라.

cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
source .env && source .clickhouse_state
python scripts/02_migrate_trips.py --resume
bash scripts/01_verify_migration.sh

5단계 — 정리

무엇이든 정리하기 전에 마이그레이션이 올바른 최종 상태인지 확인하라.

확인 항목명령예상
행 수 패리티bash scripts/01_verify_migration.sh행 수 일치율 99.9% 이상
dbt 테스트dbt test (dbt/nyc_taxi_dbt_ch에서)모든 테스트 통과
Superset 대시보드http://localhost:8088 열기대시보드 7개 표시(SF 3개 + CH 4개)
벤치마크 결과cat scripts/benchmark_results_<timestamp>.csv7개 쿼리 모두에 속도 향상 값이 있다
cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
source .env && source .clickhouse_state
bash scripts/01_verify_migration.sh

cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse/dbt/nyc_taxi_dbt_ch"
dbt test

네 항목 모두 통과하면, 모듈 06에 필요한 모든 것은 두 파일이며 둘 다 정리 후에도 남는다. workshop_public/snowflake_migration_lab/02-plan-and-design/migration-plan.md와 workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse/scripts/benchmark_results_<timestamp>.csv다. 모듈 06은 약 60분짜리 오픈북 지필 평가이며 그 외에는 아무것도 필요하지 않다 — 서면 시험을 치는 동안 유료 ClickHouse Cloud 서비스를 계속 켜 둘 이유가 없다. 두 파일의 내용을 접근 가능한 곳에 복사하거나 적어 둔 다음, 전부 정리하라.

cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse"
source .env && ./teardown.sh

이 명령은 ClickHouse Cloud 서비스(terraform destroy를 통해)와, 컷오버를 수행했다면 ClickHouse 트립 프로듀서 컨테이너를 파괴한다.

Part 1의 Snowflake 리소스는 이 스크립트로 정리되지 않는다. Snowflake 쪽은 별도로 정리하라.

cd "$(git rev-parse --show-toplevel)/workshop_public/snowflake_migration_lab/01-setup-snowflake"
source .env && ./teardown.sh

완료 확인 방법

이 모듈의 이 지점까지 오면 다음을 순서대로 확인했어야 한다.

  • 패리티 확인 통과 — 4단계의 01_verify_migration.sh가 행 수 차이 0.01% 미만으로 PASS를 보고했다.
  • 디스크의 벤치마크 CSV — 2단계가 7개 쿼리 모두에 속도 향상 값을 담은 benchmark_results_<timestamp>.csv를 썼고, 5단계 정리 전에 보관해 두었다.
  • 대시보드 7개 존재 — 1단계의 Superset 확인에서 Snowflake 대시보드 3개와 CH — 대시보드 4개가 나란히 보였다.
  • ClickHouse 프로듀서 쓰기 중 — 3단계의 검증 블록에서 5단계가 정리를 위해 중지하기 전에 default.trips_raw의 행이 늘어나고 nyc_taxi_ch_producer가 실행 중임이 보였다.

그중 어느 것이 그 시점에 성립하지 않았다면, 지금 이 확인들을 다시 실행하기보다 해당 단계로 돌아가라 — 5단계가 이미 ClickHouse Cloud 서비스를, 그리고 컷오버가 있었다면 프로듀서 컨테이너까지 파괴했다.

종료 상태

마이그레이션이 완료되고 측정되었다. 5천만 행이 Snowflake에서 ClickHouse로 옮겨져 패리티가 검증되고, 일곱 개 쿼리가 정면으로 벤치마크되어 모두에서 ClickHouse가 더 빠르고, BI 레이어가 원래의 Snowflake 대시보드 3개 옆에 ClickHouse 대시보드 4개로 재구축되고, 쓰기 경로가 Snowflake에서 ClickHouse로 완전히 넘어갔다. 두 클라우드 환경 모두 정리되었다 — ClickHouse Cloud 서비스도, ClickHouse 프로듀서 컨테이너도 없고, Part 1의 정리까지 실행했다면 Snowflake 웨어하우스도 없다.

정리 후에도 남는 두 파일이 모듈 06에 필요한 전부다. workshop_public/snowflake_migration_lab/02-plan-and-design/migration-plan.md와 workshop_public/snowflake_migration_lab/03-migrate-to-clickhouse/scripts/benchmark_results_<timestamp>.csv다. 모듈 06은 오픈북 서면 평가다 — 그 두 파일만 가져오면 된다.

이 페이지의 내용

Track your progress?

Optional. We email a link to confirm your address; progress records once you open it.

Please use your work email address, not a personal one.

Progress tracking also requires accepting the current Terms of Service in Privacy settings.

KO